Fix bug where reset_counters resets the wrong counter cache.

If a model belongs_to two associations with the same class, then reset_counters
will reset the wrong counter cache.

Finding the right reflection should use the foreign_key instead, which should
be unique.
This commit is contained in:
David Peter 2012-01-16 18:06:14 -08:00
parent 21afd9b96d
commit ee013a503d
7 changed files with 45 additions and 3 deletions

View File

@ -25,9 +25,10 @@ module ActiveRecord
self.name self.name
end end
foreign_key = has_many_association.foreign_key.to_s
child_class = has_many_association.klass child_class = has_many_association.klass
belongs_to = child_class.reflect_on_all_associations(:belongs_to) belongs_to = child_class.reflect_on_all_associations(:belongs_to)
reflection = belongs_to.find { |e| e.class_name == expected_name } reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key }
counter_name = reflection.counter_cache_column counter_name = reflection.counter_cache_column
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({

View File

@ -6,9 +6,11 @@ require 'models/engine'
require 'models/reply' require 'models/reply'
require 'models/category' require 'models/category'
require 'models/categorization' require 'models/categorization'
require 'models/dog'
require 'models/dog_lover'
class CounterCacheTest < ActiveRecord::TestCase class CounterCacheTest < ActiveRecord::TestCase
fixtures :topics, :categories, :categorizations, :cars fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers
class ::SpecialTopic < ::Topic class ::SpecialTopic < ::Topic
has_many :special_replies, :foreign_key => 'parent_id' has_many :special_replies, :foreign_key => 'parent_id'
@ -61,7 +63,7 @@ class CounterCacheTest < ActiveRecord::TestCase
end end
end end
test "reset counter should with belongs_to which has class_name" do test "reset counter with belongs_to which has class_name" do
car = cars(:honda) car = cars(:honda)
assert_nothing_raised do assert_nothing_raised do
Car.reset_counters(car.id, :engines) Car.reset_counters(car.id, :engines)
@ -71,6 +73,20 @@ class CounterCacheTest < ActiveRecord::TestCase
end end
end end
test "reset the right counter if two have the same class_name" do
david = dog_lovers(:david)
DogLover.increment_counter(:bred_dogs_count, david.id)
DogLover.increment_counter(:trained_dogs_count, david.id)
assert_difference 'david.reload.bred_dogs_count', -1 do
DogLover.reset_counters(david.id, :bred_dogs)
end
assert_difference 'david.reload.trained_dogs_count', -1 do
DogLover.reset_counters(david.id, :trained_dogs)
end
end
test "update counter with initial null value" do test "update counter with initial null value" do
category = categories(:general) category = categories(:general)
assert_equal 2, category.categorizations.count assert_equal 2, category.categorizations.count

View File

@ -0,0 +1,4 @@
david:
id: 1
bred_dogs_count: 0
trained_dogs_count: 1

3
activerecord/test/fixtures/dogs.yml vendored Normal file
View File

@ -0,0 +1,3 @@
sophie:
id: 1
trainer_id: 1

View File

@ -0,0 +1,4 @@
class Dog < ActiveRecord::Base
belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count
belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count
end

View File

@ -0,0 +1,4 @@
class DogLover < ActiveRecord::Base
has_many :trained_dogs, :class_name => "Dog", :foreign_key => :trainer_id
has_many :bred_dogs, :class_name => "Dog", :foreign_key => :breeder_id
end

View File

@ -213,6 +213,16 @@ ActiveRecord::Schema.define do
t.integer :access_level, :default => 1 t.integer :access_level, :default => 1
end end
create_table :dog_lovers, :force => true do |t|
t.integer :trained_dogs_count, :default => 0
t.integer :bred_dogs_count, :default => 0
end
create_table :dogs, :force => true do |t|
t.integer :trainer_id
t.integer :breeder_id
end
create_table :edges, :force => true, :id => false do |t| create_table :edges, :force => true, :id => false do |t|
t.column :source_id, :integer, :null => false t.column :source_id, :integer, :null => false
t.column :sink_id, :integer, :null => false t.column :sink_id, :integer, :null => false