From 7afbc89c37e56531c9ef4e34369e329aab1b21de Mon Sep 17 00:00:00 2001 From: Olek Janiszewski Date: Wed, 18 Jan 2012 23:03:55 +0100 Subject: [PATCH] Add ActiveRecord::Base#with_lock Add a `with_lock` method to ActiveRecord objects, which starts a transaction, locks the object (pessimistically) and yields to the block. The method takes one (optional) parameter and passes it to `lock!`. Before: class Order < ActiveRecord::Base def cancel! transaction do lock! # ... cancelling logic end end end After: class Order < ActiveRecord::Base def cancel! with_lock do # ... cancelling logic end end end --- activerecord/CHANGELOG.md | 29 ++++++++++++++++++- .../lib/active_record/locking/pessimistic.rb | 22 ++++++++++++++ activerecord/test/cases/locking_test.rb | 20 +++++++++++++ .../source/active_record_querying.textile | 11 +++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index f7de341fbe..a458f9e6e4 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -72,6 +72,33 @@ ## Rails 3.2.0 (unreleased) ## +* Added a `with_lock` method to ActiveRecord objects, which starts + a transaction, locks the object (pessimistically) and yields to the block. + The method takes one (optional) parameter and passes it to `lock!`. + + Before: + + class Order < ActiveRecord::Base + def cancel! + transaction do + lock! + # ... cancelling logic + end + end + end + + After: + + class Order < ActiveRecord::Base + def cancel! + with_lock do + # ... cancelling logic + end + end + end + + *Olek Janiszewski* + * 'on' and 'ON' boolean columns values are type casted to true *Santiago Pastorino* @@ -82,7 +109,7 @@ Example: rake db:migrate SCOPE=blog - *Piotr Sarnacki* + *Piotr Sarnacki* * Migrations copied from engines are now scoped with engine's name, for example 01_create_posts.blog.rb. *Piotr Sarnacki* diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 66994e4797..58af92f0b1 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -38,6 +38,18 @@ module Locking # account2.save! # end # + # You can start a transaction and acquire the lock in one go by calling + # with_lock with a block. The block is called from within + # a transaction, the object is already locked. Example: + # + # account = Account.first + # account.with_lock do + # # This block is called within a transaction, + # # account is already locked. + # account.balance -= 100 + # account.save! + # end + # # Database-specific information on row locking: # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE @@ -50,6 +62,16 @@ def lock!(lock = true) reload(:lock => lock) if persisted? self end + + # Wraps the passed block in a transaction, locking the object + # before yielding. You pass can the SQL locking clause + # as argument (see lock!). + def with_lock(lock = true) + transaction do + lock!(lock) + yield + end + end end end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 0c458d5318..807274ca67 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -388,6 +388,26 @@ def test_sane_lock_method end end + def test_with_lock_commits_transaction + person = Person.find 1 + person.with_lock do + person.first_name = 'fooman' + person.save! + end + assert_equal 'fooman', person.reload.first_name + end + + def test_with_lock_rolls_back_transaction + person = Person.find 1 + old = person.first_name + person.with_lock do + person.first_name = 'fooman' + person.save! + raise 'oops' + end rescue nil + assert_equal old, person.reload.first_name + end + if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) def test_no_locks_no_wait first, second = duel { Person.find 1 } diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index beada85ce3..5970a45839 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -692,6 +692,17 @@ Item.transaction do end +If you already have an instance of your model, you can start a transaction and acquire the lock in one go using the following code: + + +item = Item.first +item.with_lock do + # This block is called within a transaction, + # item is already locked. + item.increment!(:views) +end + + h3. Joining Tables Active Record provides a finder method called +joins+ for specifying +JOIN+ clauses on the resulting SQL. There are multiple ways to use the +joins+ method.