rspec書いてたらテストデータが残り続けてしまう事象に遭遇。digったらMySQLの暗黙的コミットに辿り着いた。
久しぶりの記事投稿となります。
※前段は私の個人的な話なのでお急ぎの方は読み飛ばしてもらって結構です。
個人的なブログ投稿ポリシーとして「zennには下手なことは書けない😅」と思っています。というのも、技術者として未熟者だという自負があり、投稿に対するハードルがかなり高くなっていました。
しかし、"自分なりに"ではありますが、このポリシーによって記事の質を担保できる気がしているので、今後もこのポリシーを大切にしていきたいです。
そんな私ですが、今回は、「少しは誰かのお役に立てるかもしれない」と思って執筆してます。
久しぶりにzennに投稿できることが嬉しい。この機会を増やせるよう精進していきたいですね。
また、私事ではありますが、今年の4月から晴れてエンジニアとして働き始める予定です。現在はありがたいことに内定先で内定者アルバイトをさせていただいています。
いっちょ前に内定者インタビューなんかも書いてもらったので、これからエンジニアを目指す方の1サンプルとして見てもらえたら嬉しいです。
内定者インタビュー
長くなりましたが本題に移りたいと思います。
RSpecはよしなにテストデータを削除してくれる...んだよね?
テストツールにrspecを使用している方は、以下のコードを目にしたことはあるでしょうか?
RSpec.configure do |config|
config.use_transactional_fixtures = true
このuse_transactional_fixtures
がtrue
であれば、example(it)のたびにデータが削除されるようになります。
このあたりについては、より詳しく書いている方がいらっしゃるため以下を参照していただければと思います。
RSpec with Railsでテスト時のデータはどのように削除されているか
Relish/rspec/transactions
ただ今回の場合、上記を設定しているにも関わらずテストデータが削除されずに残ってしまったため頭を抱えてしまいました。
このままだと、テストを一度実行した後毎回rails db:migrate:reset
を行ってデータをリセットするという手間が増えてしまいます。暫定対応として上記操作を行っていたものの、さすがにイライラしてきたので根本原因を突き止めてやろうと思いました。
問題を引き起こしているコードをまずは突き止める
実装箇所を徐々にコメントアウトしながらテストを回し続け、以下コードを削除したところ、"テストデータが残らない"という結果を得ました🙌
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} AUTO_INCREMENT = 1")
railsではDBとのやりとりはActiveRecordを介して行います。
Model.find(id)
と書くと、実際にはSQLのSELECT文が実行され、データベース上の該当テーブルから検索にマッチしたデータを取得してきてくれます。
SELECT `テーブル名`.* FROM `テーブル名` WHERE `テーブル名`.`id` = 1 LIMIT 1
ActiveRecordのメソッドのみで対応しきれないことを実現したいときは、生のSQLを実行してなんとかします。
ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} AUTO_INCREMENT = 1")
ALTER TABLE
・・・テーブル定義を変更するSQL
こちらのコードでは、自動採番の連番リセットを行っています。
(上述のコードの前にdelete_all
でデータの一括削除を行っており、データは消えても連番の状態が残ってしまうため)
今回はこいつが原因だったのですが、なぜ上記の実行の有無がテストデータの残存の有無と関連があるのか?引き続き調査してみることに👀
そういえばテストログ見てなかったじゃん!
はじめはrspecのトランザクションの設定関連が怪しいなと思っていたのですが、そもそもテスト実行後ちゃんとロールバックされているか確認してなかったなと思い、テスト実行を行い正常パターン(テストデータが削除される)と異常パターン(テストデータが残り続ける)でロールバックの有無を比較してみることにしました。
結論、どちらもロールバックされていました。
この時点で結構落ち込みました。(もうわからん、無理かも...)
(0.1ms) SAVEPOINT active_record_1
# 省略
(0.1ms) RELEASE SAVEPOINT active_record_1
(0.1ms) SAVEPOINT active_record_1
# 省略
(0.1ms) RELEASE SAVEPOINT active_record_1
(0.1ms) ROLLBACK
上記コードを見て、そもそもSAVEPOINT
などの理解が足りてないなと思い、調べてみました。
SAVEPOINT
ステートメントは、identifier
の名前を持つ名前付きのトランザクションセーブポイントを設定します。現在のトランザクションに同じ名前を持つセーブポイントが含まれている場合、古いセーブポイントは削除され、新しいセーブポイントが設定されます。
セーブポイントを指定しないROLLBACK
を実行した場合は、現在のトランザクションのすべてのセーブポイントが削除されます
RELEASE SAVEPOINT
ステートメントは、指定されたセーブポイントを現在のトランザクションの一連のセーブポイントから削除します。コミットまたはロールバックは発生しません。そのセーブポイントが存在しない場合はエラーになります。
13.3.4 SAVEPOINT、ROLLBACK TO SAVEPOINT、および RELEASE SAVEPOINT 構文
この時点で、ロールバックとトランザクションの理解が深まったところで、ロールバックはされてるが適用されていないのでは? と思いました。
すると以下の記事に辿り着きました。
MySQLROLLBACKが効かないケース
これはきたのでは!?
MySQLの暗黙的コミットと暗黙的コミットを発生させるステートメント
まず確認として、MySQLはDBではなくて、DBMS(データベース管理システム)なのでDBに問い合わせをするためのシステムであること。
内部ではInnoDBというストレージエンジンを使用しているようです。(MySQL 5.5~)
そのInnoDBはAutoCommitモードというモードがデフォルトで設定されているようです。
AutoCommitモード
・・・明示的にトランザクションの開始宣言をしなければ勝手にコミットされるということです。
コミットとは?MySQLの公式リファレンスにはこう書いていました。
COMMIT は、現在のトランザクションをコミットして、その変更を永続的なものにします。
START TRANSACTION、COMMIT、および ROLLBACK 構文
怪しい...
上記で、ロールバックはされてるが適用されていないのでは? という仮説を立てていましたが、まさにロールバックできないSQL文というものが存在するらしいです。以下はその一例になります。
- ALTER TABLE
- CREATE INDEX
- DROP DATABASE
- DROP INDEX
- DROP TABLE
- LOAD MASTER DATA
- LOCK TABLES
- RENAME TABLE
- TRUNCATE TABSE
- START TRANSACTION
- BEGIN
もうここまでくるとお分かりですよね?
今回のテストデータが残り続けていた原因は、「ロールバックできないSQL文を意図せず実行していたから」ということになります。
ロールバックできないSQLの正体が、暗黙的コミットと暗黙的コミットを発生させるステートメント です。
暗黙的コミットと暗黙的コミットを発生させるステートメント の解説
このステートメントを実行する前に COMMIT を実行したかのように、現在のセッション内でアクティブなすべてのトランザクションを暗黙的に終了します。 MySQL 5.5.3 の時点では、これらのステートメントのほとんどが、実行後に暗黙的なコミットも発生させます。
トランザクションが終了されてしまうため、ROLLBACK
を実行したところで無駄なわけです。なんせ復活させる場所がなくなっているので。
まとめ
今回はrspecから派生してデータベースの領域の勉強になりました。
道中でActiveRecordのコードリーディングもできたので楽しかったな。
読んでいただきありがとうございました。
内容に間違いがありましたら、ご指摘いただければと思います。
以下参考文献です。
【参考文献】
RSpec with Railsでテスト時のデータはどのように削除されているか
Relish/rspec/transactions
Active Recordのメソッドと実行されるSQL一覧
13.3.4 SAVEPOINT、ROLLBACK TO SAVEPOINT、および RELEASE SAVEPOINT 構文
MySQLROLLBACKが効かないケース
START TRANSACTION、COMMIT、および ROLLBACK 構文
暗黙的なコミットを発生させるステートメント
Discussion