Skip to content

Commit db74fcd

Browse files
author
Brian Durand
committed
Use database lock to ensure data integrity on counter caches.
1 parent 12a0383 commit db74fcd

5 files changed

Lines changed: 37 additions & 1 deletion

File tree

activerecord/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,4 +910,8 @@
910910

911911
*Aaron Patterson*
912912

913+
* Use database lock to ensure data integrity on counter caches.
914+
915+
*Brian Durand*
916+
913917
Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes.

activerecord/lib/active_record/associations/builder/belongs_to.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ def belongs_to_counter_cache_after_create_for_#{name}
3131
def belongs_to_counter_cache_before_destroy_for_#{name}
3232
unless marked_for_destruction?
3333
record = #{name}
34-
record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil?
34+
if record && self.class.obtain_lock(id)
35+
record.class.decrement_counter(:#{cache_column}, record.id)
36+
end
3537
end
3638
end
3739
CODE

activerecord/lib/active_record/locking/pessimistic.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ module Locking
5454
# MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
5555
# PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
5656
module Pessimistic
57+
extend ActiveSupport::Concern
58+
59+
module ClassMethods
60+
# Obtain a row level lock on the row with the specified primary key value (if row locking is supported).
61+
# Returns +true+ if the row exists or +false+ if the row does not exist.
62+
def obtain_lock(id, lock = true)
63+
sql = where(primary_key => id).select(primary_key).lock(lock).to_sql
64+
!!connection.select_one(sql)
65+
end
66+
end
67+
5768
# Obtain a row lock on this record. Reloads the record to obtain the requested
5869
# lock. Pass an SQL locking clause to append the end of the SELECT statement
5970
# or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns

activerecord/test/cases/associations/belongs_to_associations_test.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,20 @@ def test_custom_counter_cache
393393
assert_equal 17, reply.replies.size
394394
end
395395

396+
def test_counter_cache_with_destroy
397+
topic = Topic.create!(:title => "Zoom-zoom-zoom")
398+
topic.replies.create!(:title => "re: zoom", :content => "speedy quick!")
399+
400+
assert_equal 1, topic.reload[:replies_count]
401+
402+
reply_1 = Reply.find(topic.replies.first.id)
403+
reply_2 = Reply.find(topic.replies.first.id)
404+
reply_1.destroy
405+
reply_2.destroy
406+
407+
assert_equal 0, topic.reload[:replies_count]
408+
end
409+
396410
def test_association_assignment_sticks
397411
post = Post.first
398412

activerecord/test/cases/locking_test.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,11 @@ def test_with_lock_rolls_back_transaction
408408
assert_equal old, person.reload.first_name
409409
end
410410

411+
def test_obtain_lock
412+
assert Person.obtain_lock(1)
413+
assert !Person.obtain_lock(0)
414+
end
415+
411416
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
412417
def test_no_locks_no_wait
413418
first, second = duel { Person.find 1 }

0 commit comments

Comments
 (0)