Rails & MySQL: トランザクション分離レベルをグローバルに設定する

MySQL (InnoDB) のトランザクション分離レベルは、デフォルトで REPEATABLE READ です。この設定では、トランザクションの最初のクエリでデータベースのスナップショットを取ってしまうので、他のトランザクションがコミットした変更が見えません。Web アプリケーション開発では結構使いづらい分離レベルです。特に理由がないかぎり READ COMMITTED を採用したいところです。

Rails 3.x 時代までは、


ActiveRecord::Base.connection.
execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED')
ActiveRecord::Base.transaction do
# ...
end

のように書かなければなりませんでしたが、Rails 4 でトランザクションごとに分離レベルを指定できるようになりました:


ActiveRecord::Base.transaction(isolation: :read_committed) do
# ...
end

これは大変嬉しいことです。しかし、毎回こんな風に書くのは依然として面倒ですよね。それに、RSpec でテストをするときに config.use_transactional_fixtures = true という設定を採用していれば、こんなエラーに見舞われることでしょう:


ActiveRecord::TransactionIsolationError:
cannot set transaction isolation in a nested transaction

各エグザンプルは全体がトランザクションで囲まれているので、テスト中はアプリケーションの中でトランザクションの分離レベルを変更できないのです。

そこで、トランザクション分離レベルをグローバルに設定できないか、ということになります。

一番簡単なのは MySQL サーバのインスタンス全体でグローバルに変更することです:


mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

しかし、これを実行するには SUPER 権限が必要ですし、MySQL サーバを他のアプリケーションと共有している場合には、気軽に実施できません。

Rails アプリケーション単位でグローバルに分離レベルを設定するにはどうすればいいでしょうか。

すぐに思いつくのは、新規ファイル config/initializers/isolation_level.rb を作って、そこにこんな風に書いておくことです:


ActiveRecord::Base.connection.execute('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')

これは開発環境ではうまく行きますが、本番環境では多分ダメです。というのは、データベースへの再接続が行われることがあるからです。例えば、Unicorn の workers を fork する時とか。

というわけで、私はモンキーパッチを当てることにしました:


class ActiveRecord::ConnectionAdapters::ConnectionPool
def new_connection_with_isolation_level
conn = new_connection_without_isolation_level
conn.execute('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')
conn
end
alias_method_chain :new_connection, :isolation_level
end

このコードを config/initializers/isolation_level.rb に書いておくわけです。

こういうことはあまりしたくないのですが、他に方法が思いつきませんでした。もし、ご存じの方がいらっしゃいましたら、是非教えてください。コメントでもメールでも結構です!

config/database.yml


production:
adapter: mysql2
encoding: utf8
transaction_isolation_level: read_committed
...

といった感じで設定できるといいのですけどね。

[訂正] この記事を最初に投稿したとき、私は次のようなモンキーパッチを紹介したのですが、実際には全然動きませんでした。


class ActiveRecord::Base
class << self
def establish_connection_with_transaction_isolation_level(*args)
establish_connection_without_transaction_isolation_level(*args)
connection.execute('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')
end

alias_method_chain :establish_connection, :transaction_isolation_level
end
end

テスト環境で動いたように見えたので、急いで記事を書いたのが敗因です。訂正します。(2013-11-22)