From 392eeecc11a291e406db927a18b75f41b2658253 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 21 Sep 2012 16:14:42 +0100 Subject: Support for specifying transaction isolation level If your database supports setting the isolation level for a transaction, you can set it like so: Post.transaction(isolation: :serializable) do # ... end Valid isolation levels are: * `:read_uncommitted` * `:read_committed` * `:repeatable_read` * `:serializable` You should consult the documentation for your database to understand the semantics of these different levels: * http://www.postgresql.org/docs/9.1/static/transaction-iso.html * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html An `ActiveRecord::TransactionIsolationError` will be raised if: * The adapter does not support setting the isolation level * You are joining an existing open transaction * You are creating a nested (savepoint) transaction The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. However, support is disabled for mysql versions below 5, because they are affected by a bug (http://bugs.mysql.com/bug.php?id=39170) which means the isolation level gets persisted outside the transaction. --- .../abstract/database_statements.rb | 63 +++++++++++++++++++--- .../connection_adapters/abstract/transaction.rb | 27 ++++++---- 2 files changed, 75 insertions(+), 15 deletions(-) (limited to 'activerecord/lib/active_record/connection_adapters/abstract') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 32e3c7f5d8..793f58d4d3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -155,10 +155,47 @@ module ActiveRecord # # active_record_1 now automatically released # end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error! # end + # + # == Transaction isolation + # + # If your database supports setting the isolation level for a transaction, you can set + # it like so: + # + # Post.transaction(isolation: :serializable) do + # # ... + # end + # + # Valid isolation levels are: + # + # * :read_uncommitted + # * :read_committed + # * :repeatable_read + # * :serializable + # + # You should consult the documentation for your database to understand the + # semantics of these different levels: + # + # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html + # * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # + # An ActiveRecord::TransactionIsolationError will be raised if: + # + # * The adapter does not support setting the isolation level + # * You are joining an existing open transaction + # * You are creating a nested (savepoint) transaction + # + # The mysql, mysql2 and postgresql adapters support setting the transaction + # isolation level. However, support is disabled for mysql versions below 5, + # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] + # which means the isolation level gets persisted outside the transaction. def transaction(options = {}) - options.assert_valid_keys :requires_new, :joinable + options.assert_valid_keys :requires_new, :joinable, :isolation if !options[:requires_new] && current_transaction.joinable? + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" + end + yield else within_new_transaction(options) { yield } @@ -168,10 +205,10 @@ module ActiveRecord end def within_new_transaction(options = {}) #:nodoc: - begin_transaction(options) + transaction = begin_transaction(options) yield rescue Exception => error - rollback_transaction + rollback_transaction if transaction raise ensure begin @@ -191,9 +228,7 @@ module ActiveRecord end def begin_transaction(options = {}) #:nodoc: - @transaction = @transaction.begin - @transaction.joinable = options.fetch(:joinable, true) - @transaction + @transaction = @transaction.begin(options) end def commit_transaction #:nodoc: @@ -217,6 +252,22 @@ module ActiveRecord # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end + def transaction_isolation_levels + { + read_uncommitted: "READ UNCOMMITTED", + read_committed: "READ COMMITTED", + repeatable_read: "REPEATABLE READ", + serializable: "SERIALIZABLE" + } + end + + # Begins the transaction with the isolation level set. Raises an error by + # default; adapters that support setting the isolation level should implement + # this method. + def begin_isolated_db_transaction(isolation) + raise ActiveRecord::TransactionIsolationError, "adapter does not support setting transaction isolation" + end + # Commits the transaction (and turns on auto-committing). def commit_db_transaction() end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 2117eae5cb..4cca94e40b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -13,8 +13,8 @@ module ActiveRecord 0 end - def begin - RealTransaction.new(connection, self) + def begin(options = {}) + RealTransaction.new(connection, self, options) end def closed? @@ -38,13 +38,13 @@ module ActiveRecord attr_reader :parent, :records attr_writer :joinable - def initialize(connection, parent) + def initialize(connection, parent, options = {}) super connection @parent = parent @records = [] @finishing = false - @joinable = true + @joinable = options.fetch(:joinable, true) end # This state is necesarry so that we correctly handle stuff that might @@ -66,11 +66,11 @@ module ActiveRecord end end - def begin + def begin(options = {}) if finishing? parent.begin else - SavepointTransaction.new(connection, self) + SavepointTransaction.new(connection, self, options) end end @@ -120,9 +120,14 @@ module ActiveRecord end class RealTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent) + def initialize(connection, parent, options = {}) super - connection.begin_db_transaction + + if options[:isolation] + connection.begin_isolated_db_transaction(options[:isolation]) + else + connection.begin_db_transaction + end end def perform_rollback @@ -137,7 +142,11 @@ module ActiveRecord end class SavepointTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent) + def initialize(connection, parent, options = {}) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + super connection.create_savepoint end -- cgit v1.2.3