Permalink
Browse files

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.
  • Loading branch information...
jonleighton committed Sep 21, 2012
1 parent 834d6da commit 392eeecc11a291e406db927a18b75f41b2658253
View
@@ -1,5 +1,40 @@
## Rails 4.0.0 (unreleased) ##
* 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.
*Jon Leighton*
* `ActiveModel::ForbiddenAttributesProtection` is included by default
in Active Record models. Check the docs of `ActiveModel::ForbiddenAttributesProtection`
for more details.
@@ -155,10 +155,47 @@ def supports_statement_cache?
# # 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:
#
# * <tt>:read_uncommitted</tt>
# * <tt>:read_committed</tt>
# * <tt>:repeatable_read</tt>
# * <tt>:serializable</tt>
#
# 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 <tt>ActiveRecord::TransactionIsolationError</tt> 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 @@ def transaction(options = {})
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 @@ def transaction_open?
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 @@ def add_transaction_record(record)
# 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
@@ -13,8 +13,8 @@ def number
0
end
def begin
RealTransaction.new(connection, self)
def begin(options = {})
RealTransaction.new(connection, self, options)
end
def closed?
@@ -38,13 +38,13 @@ class OpenTransaction < Transaction #:nodoc:
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 @@ def number
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 @@ def open?
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 @@ def perform_commit
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
@@ -167,6 +167,11 @@ def supports_explain?
false
end
# Does this adapter support setting the isolation level for a transaction?
def supports_transaction_isolation?
false
end
# QUOTING ==================================================
# Returns a bind substitution value given a +column+ and list of current
@@ -169,6 +169,14 @@ def supports_index_sort_order?
true
end
# MySQL 4 technically support transaction isolation, but it is affected by a bug
# where the transaction level gets persisted for the whole session:
#
# http://bugs.mysql.com/bug.php?id=39170
def supports_transaction_isolation?
version[0] >= 5
end
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -269,6 +277,13 @@ def begin_db_transaction
# Transactions aren't supported
end
def begin_isolated_db_transaction(isolation)
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
begin_db_transaction
rescue
# Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
rescue
@@ -205,6 +205,11 @@ def begin_db_transaction
execute "BEGIN"
end
def begin_isolated_db_transaction(isolation)
begin_db_transaction
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
end
# Commits a transaction.
def commit_db_transaction
execute "COMMIT"
@@ -370,6 +370,10 @@ def supports_partial_index?
true
end
def supports_transaction_isolation?
true
end
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
@@ -195,4 +195,7 @@ def initialize(model)
class ImmutableRelation < ActiveRecordError
end
class TransactionIsolationError < ActiveRecordError
end
end
Oops, something went wrong.

0 comments on commit 392eeec

Please sign in to comment.