Skip to content

Commit 392eeec

Browse files
committed
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.
1 parent 834d6da commit 392eeec

9 files changed

Lines changed: 263 additions & 15 deletions

File tree

activerecord/CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
11
## Rails 4.0.0 (unreleased) ##
22

3+
* Support for specifying transaction isolation level
4+
5+
If your database supports setting the isolation level for a transaction, you can set
6+
it like so:
7+
8+
Post.transaction(isolation: :serializable) do
9+
# ...
10+
end
11+
12+
Valid isolation levels are:
13+
14+
* `:read_uncommitted`
15+
* `:read_committed`
16+
* `:repeatable_read`
17+
* `:serializable`
18+
19+
You should consult the documentation for your database to understand the
20+
semantics of these different levels:
21+
22+
* http://www.postgresql.org/docs/9.1/static/transaction-iso.html
23+
* https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
24+
25+
An `ActiveRecord::TransactionIsolationError` will be raised if:
26+
27+
* The adapter does not support setting the isolation level
28+
* You are joining an existing open transaction
29+
* You are creating a nested (savepoint) transaction
30+
31+
The mysql, mysql2 and postgresql adapters support setting the transaction
32+
isolation level. However, support is disabled for mysql versions below 5,
33+
because they are affected by a bug (http://bugs.mysql.com/bug.php?id=39170)
34+
which means the isolation level gets persisted outside the transaction.
35+
36+
*Jon Leighton*
37+
338
* `ActiveModel::ForbiddenAttributesProtection` is included by default
439
in Active Record models. Check the docs of `ActiveModel::ForbiddenAttributesProtection`
540
for more details.

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,47 @@ def supports_statement_cache?
155155
# # active_record_1 now automatically released
156156
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
157157
# end
158+
#
159+
# == Transaction isolation
160+
#
161+
# If your database supports setting the isolation level for a transaction, you can set
162+
# it like so:
163+
#
164+
# Post.transaction(isolation: :serializable) do
165+
# # ...
166+
# end
167+
#
168+
# Valid isolation levels are:
169+
#
170+
# * <tt>:read_uncommitted</tt>
171+
# * <tt>:read_committed</tt>
172+
# * <tt>:repeatable_read</tt>
173+
# * <tt>:serializable</tt>
174+
#
175+
# You should consult the documentation for your database to understand the
176+
# semantics of these different levels:
177+
#
178+
# * http://www.postgresql.org/docs/9.1/static/transaction-iso.html
179+
# * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
180+
#
181+
# An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if:
182+
#
183+
# * The adapter does not support setting the isolation level
184+
# * You are joining an existing open transaction
185+
# * You are creating a nested (savepoint) transaction
186+
#
187+
# The mysql, mysql2 and postgresql adapters support setting the transaction
188+
# isolation level. However, support is disabled for mysql versions below 5,
189+
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
190+
# which means the isolation level gets persisted outside the transaction.
158191
def transaction(options = {})
159-
options.assert_valid_keys :requires_new, :joinable
192+
options.assert_valid_keys :requires_new, :joinable, :isolation
160193

161194
if !options[:requires_new] && current_transaction.joinable?
195+
if options[:isolation]
196+
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
197+
end
198+
162199
yield
163200
else
164201
within_new_transaction(options) { yield }
@@ -168,10 +205,10 @@ def transaction(options = {})
168205
end
169206

170207
def within_new_transaction(options = {}) #:nodoc:
171-
begin_transaction(options)
208+
transaction = begin_transaction(options)
172209
yield
173210
rescue Exception => error
174-
rollback_transaction
211+
rollback_transaction if transaction
175212
raise
176213
ensure
177214
begin
@@ -191,9 +228,7 @@ def transaction_open?
191228
end
192229

193230
def begin_transaction(options = {}) #:nodoc:
194-
@transaction = @transaction.begin
195-
@transaction.joinable = options.fetch(:joinable, true)
196-
@transaction
231+
@transaction = @transaction.begin(options)
197232
end
198233

199234
def commit_transaction #:nodoc:
@@ -217,6 +252,22 @@ def add_transaction_record(record)
217252
# Begins the transaction (and turns off auto-committing).
218253
def begin_db_transaction() end
219254

255+
def transaction_isolation_levels
256+
{
257+
read_uncommitted: "READ UNCOMMITTED",
258+
read_committed: "READ COMMITTED",
259+
repeatable_read: "REPEATABLE READ",
260+
serializable: "SERIALIZABLE"
261+
}
262+
end
263+
264+
# Begins the transaction with the isolation level set. Raises an error by
265+
# default; adapters that support setting the isolation level should implement
266+
# this method.
267+
def begin_isolated_db_transaction(isolation)
268+
raise ActiveRecord::TransactionIsolationError, "adapter does not support setting transaction isolation"
269+
end
270+
220271
# Commits the transaction (and turns on auto-committing).
221272
def commit_db_transaction() end
222273

activerecord/lib/active_record/connection_adapters/abstract/transaction.rb

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ def number
1313
0
1414
end
1515

16-
def begin
17-
RealTransaction.new(connection, self)
16+
def begin(options = {})
17+
RealTransaction.new(connection, self, options)
1818
end
1919

2020
def closed?
@@ -38,13 +38,13 @@ class OpenTransaction < Transaction #:nodoc:
3838
attr_reader :parent, :records
3939
attr_writer :joinable
4040

41-
def initialize(connection, parent)
41+
def initialize(connection, parent, options = {})
4242
super connection
4343

4444
@parent = parent
4545
@records = []
4646
@finishing = false
47-
@joinable = true
47+
@joinable = options.fetch(:joinable, true)
4848
end
4949

5050
# This state is necesarry so that we correctly handle stuff that might
@@ -66,11 +66,11 @@ def number
6666
end
6767
end
6868

69-
def begin
69+
def begin(options = {})
7070
if finishing?
7171
parent.begin
7272
else
73-
SavepointTransaction.new(connection, self)
73+
SavepointTransaction.new(connection, self, options)
7474
end
7575
end
7676

@@ -120,9 +120,14 @@ def open?
120120
end
121121

122122
class RealTransaction < OpenTransaction #:nodoc:
123-
def initialize(connection, parent)
123+
def initialize(connection, parent, options = {})
124124
super
125-
connection.begin_db_transaction
125+
126+
if options[:isolation]
127+
connection.begin_isolated_db_transaction(options[:isolation])
128+
else
129+
connection.begin_db_transaction
130+
end
126131
end
127132

128133
def perform_rollback
@@ -137,7 +142,11 @@ def perform_commit
137142
end
138143

139144
class SavepointTransaction < OpenTransaction #:nodoc:
140-
def initialize(connection, parent)
145+
def initialize(connection, parent, options = {})
146+
if options[:isolation]
147+
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
148+
end
149+
141150
super
142151
connection.create_savepoint
143152
end

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ def supports_explain?
167167
false
168168
end
169169

170+
# Does this adapter support setting the isolation level for a transaction?
171+
def supports_transaction_isolation?
172+
false
173+
end
174+
170175
# QUOTING ==================================================
171176

172177
# Returns a bind substitution value given a +column+ and list of current

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ def supports_index_sort_order?
169169
true
170170
end
171171

172+
# MySQL 4 technically support transaction isolation, but it is affected by a bug
173+
# where the transaction level gets persisted for the whole session:
174+
#
175+
# http://bugs.mysql.com/bug.php?id=39170
176+
def supports_transaction_isolation?
177+
version[0] >= 5
178+
end
179+
172180
def native_database_types
173181
NATIVE_DATABASE_TYPES
174182
end
@@ -269,6 +277,13 @@ def begin_db_transaction
269277
# Transactions aren't supported
270278
end
271279

280+
def begin_isolated_db_transaction(isolation)
281+
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
282+
begin_db_transaction
283+
rescue
284+
# Transactions aren't supported
285+
end
286+
272287
def commit_db_transaction #:nodoc:
273288
execute "COMMIT"
274289
rescue

activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ def begin_db_transaction
205205
execute "BEGIN"
206206
end
207207

208+
def begin_isolated_db_transaction(isolation)
209+
begin_db_transaction
210+
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
211+
end
212+
208213
# Commits a transaction.
209214
def commit_db_transaction
210215
execute "COMMIT"

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ def supports_partial_index?
370370
true
371371
end
372372

373+
def supports_transaction_isolation?
374+
true
375+
end
376+
373377
class StatementPool < ConnectionAdapters::StatementPool
374378
def initialize(connection, max)
375379
super

activerecord/lib/active_record/errors.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,7 @@ def initialize(model)
195195

196196
class ImmutableRelation < ActiveRecordError
197197
end
198+
199+
class TransactionIsolationError < ActiveRecordError
200+
end
198201
end

0 commit comments

Comments
 (0)