mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Make #increment! and #decrement! methods concurency safe
This commit is contained in:
parent
210f33c477
commit
85a1c02508
5 changed files with 41 additions and 40 deletions
|
@ -1,3 +1,7 @@
|
|||
* Make AR::Base #increment! and decrement! concurrency safe
|
||||
|
||||
*Bogdan Gusiev*
|
||||
|
||||
* Don't require a database connection to load a class which uses acceptance
|
||||
validations.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module ActiveRecord
|
|||
def replace(record)
|
||||
if record
|
||||
raise_on_type_mismatch!(record)
|
||||
update_counters(record)
|
||||
update_counters_on_replace(record)
|
||||
replace_keys(record)
|
||||
set_inverse_instance(record)
|
||||
@updated = true
|
||||
|
@ -32,45 +32,37 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def decrement_counters # :nodoc:
|
||||
with_cache_name { |name| decrement_counter name }
|
||||
update_counters(-1)
|
||||
end
|
||||
|
||||
def increment_counters # :nodoc:
|
||||
with_cache_name { |name| increment_counter name }
|
||||
update_counters(1)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_counters(by)
|
||||
if require_counter_update? && foreign_key_present?
|
||||
if target && !stale_target?
|
||||
target.increment!(reflection.counter_cache_column, by)
|
||||
else
|
||||
klass.update_counters(target_id, reflection.counter_cache_column => by)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_target?
|
||||
!loaded? && foreign_key_present? && klass
|
||||
end
|
||||
|
||||
def with_cache_name
|
||||
counter_cache_name = reflection.counter_cache_column
|
||||
return unless counter_cache_name && owner.persisted?
|
||||
yield counter_cache_name
|
||||
def require_counter_update?
|
||||
reflection.counter_cache_column && owner.persisted?
|
||||
end
|
||||
|
||||
def update_counters(record)
|
||||
with_cache_name do |name|
|
||||
return unless different_target? record
|
||||
record.class.increment_counter(name, record.id)
|
||||
decrement_counter name
|
||||
end
|
||||
end
|
||||
|
||||
def decrement_counter(counter_cache_name)
|
||||
if foreign_key_present?
|
||||
klass.decrement_counter(counter_cache_name, target_id)
|
||||
end
|
||||
end
|
||||
|
||||
def increment_counter(counter_cache_name)
|
||||
if foreign_key_present?
|
||||
klass.increment_counter(counter_cache_name, target_id)
|
||||
if target && !stale_target?
|
||||
target.increment(counter_cache_name)
|
||||
end
|
||||
def update_counters_on_replace(record)
|
||||
if require_counter_update? && different_target?(record)
|
||||
record.increment!(reflection.counter_cache_column)
|
||||
decrement_counters
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -88,21 +88,15 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def update_counter(difference, reflection = reflection())
|
||||
update_counter_in_database(difference, reflection)
|
||||
update_counter_in_memory(difference, reflection)
|
||||
end
|
||||
|
||||
def update_counter_in_database(difference, reflection = reflection())
|
||||
if reflection.has_cached_counter?
|
||||
owner.class.update_counters(owner.id, reflection.counter_cache_column => difference)
|
||||
owner.increment!(reflection.counter_cache_column, difference)
|
||||
end
|
||||
end
|
||||
|
||||
def update_counter_in_memory(difference, reflection = reflection())
|
||||
if reflection.counter_must_be_updated_by_has_many?
|
||||
counter = reflection.counter_cache_column
|
||||
owner[counter] ||= 0
|
||||
owner[counter] += difference
|
||||
owner.increment(counter, difference)
|
||||
owner.send(:clear_attribute_change, counter) # eww
|
||||
end
|
||||
end
|
||||
|
|
|
@ -332,16 +332,18 @@ module ActiveRecord
|
|||
# Saving is not subjected to validation checks. Returns +true+ if the
|
||||
# record could be saved.
|
||||
def increment!(attribute, by = 1)
|
||||
increment(attribute, by).update_attribute(attribute, self[attribute])
|
||||
increment(attribute, by)
|
||||
change = public_send(attribute) - (attribute_was(attribute.to_s) || 0)
|
||||
self.class.update_counters(id, attribute => change)
|
||||
clear_attribute_change(attribute) # eww
|
||||
self
|
||||
end
|
||||
|
||||
# Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1).
|
||||
# The decrement is performed directly on the underlying attribute, no setter is invoked.
|
||||
# Only makes sense for number-based attributes. Returns +self+.
|
||||
def decrement(attribute, by = 1)
|
||||
self[attribute] ||= 0
|
||||
self[attribute] -= by
|
||||
self
|
||||
increment(attribute, -by)
|
||||
end
|
||||
|
||||
# Wrapper around +decrement+ that saves the record. This method differs from
|
||||
|
@ -349,7 +351,7 @@ module ActiveRecord
|
|||
# Saving is not subjected to validation checks. Returns +true+ if the
|
||||
# record could be saved.
|
||||
def decrement!(attribute, by = 1)
|
||||
decrement(attribute, by).update_attribute(attribute, self[attribute])
|
||||
increment!(attribute, -by)
|
||||
end
|
||||
|
||||
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
|
||||
|
|
|
@ -120,6 +120,15 @@ class PersistenceTest < ActiveRecord::TestCase
|
|||
assert_equal 59, accounts(:signals37, :reload).credit_limit
|
||||
end
|
||||
|
||||
def test_increment_updates_counter_in_db_using_offset
|
||||
a1 = accounts(:signals37)
|
||||
initial_credit = a1.credit_limit
|
||||
a2 = Account.find(accounts(:signals37).id)
|
||||
a1.increment!(:credit_limit)
|
||||
a2.increment!(:credit_limit)
|
||||
assert_equal initial_credit + 2, a1.reload.credit_limit
|
||||
end
|
||||
|
||||
def test_destroy_all
|
||||
conditions = "author_name = 'Mary'"
|
||||
topics_by_mary = Topic.all.merge!(:where => conditions, :order => 'id').to_a
|
||||
|
|
Loading…
Reference in a new issue