ActiveRecord has_many: благодаря дублированию кэшей счетчика при массовом назначении - PullRequest
6 голосов
/ 28 февраля 2012

Кажется, что функция counter_cache ActiveRecord может привести к увеличению кеша счетчика в два раза. Сценарий, в котором я наблюдаю это поведение, заключается в том, что у меня есть две модели, которые has_many :through связаны друг с другом через модель соединения (то есть: Teacher имеет много от Student до Classroom). При использовании сгенерированных has_many :through методов для непосредственного связывания Учителя и Ученика (без создания записи соединения вручную) счет увеличивается в 2 раза. Пример: teacher.students << Student.create(name: "Bobby Joe") вызывает увеличение teacher.students_count на 2.

Пожалуйста, помогите мне найти решение, которое смягчает или устраняет эту проблему, в то же время позволяя мне продолжать использовать встроенное счетное кэширование И массовое назначение через отношение has_many :through.

Я потратил много часов на поиски решения и выделил проблему в небольшое тестовое приложение, которое является самым простым неудачным примером, который я мог создать. Надеюсь, что любые дополнительные детали, необходимые для решения этой проблемы, должны быть ниже.

Пример схемы и моделей:

create_table :teachers do |t|
  t.string  :name
  t.integer :students_count, default: 0
  t.timestamps
end

class Teacher < ActiveRecord::Base
  has_many :classrooms
  has_many :students, :through => :classrooms
end

create_table :students do |t|
  t.string  :name
  t.integer :teachers_count, default: 0
  t.timestamps
end

class Student < ActiveRecord::Base
  has_many :classrooms
  has_many :teachers, :through => :classrooms
end

create_table :classrooms do |t|
  t.references :teacher
  t.references :student
  t.timestamps
end

class Classroom < ActiveRecord::Base
  belongs_to :student, :counter_cache => :teachers_count
  belongs_to :teacher, :counter_cache => :students_count
end

Вот короткий сеанс консоли rails, показывающий предпринятые шаги и тот факт, что rails выполняет два обновления до teachers для увеличения students_count:

1.9.2-p290 :001 > t = Teacher.create(name: "Miss Nice")
  SQL (9.7ms)  INSERT INTO "teachers" ("created_at", "name", "students_count", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Tue, 28 Feb 2012 03:31:53 UTC +00:00], ["name", "Miss Nice"], ["students_count", 0], ["updated_at", Tue, 28 Feb 2012 03:31:53 UTC +00:00]]
 => #<Teacher id: 1, name: "Miss Nice", students_count: 0, created_at: "2012-02-28 03:31:53", updated_at: "2012-02-28 03:31:53"> 
1.9.2-p290 :002 > t.students << Student.new(name: "Mary Ann")
  SQL (0.3ms)  INSERT INTO "students" ("created_at", "name", "teachers_count", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Tue, 28 Feb 2012 03:32:12 UTC +00:00], ["name", "Mary Ann"], ["teachers_count", 0], ["updated_at", Tue, 28 Feb 2012 03:32:12 UTC +00:00]]
  SQL (0.3ms)  INSERT INTO "classrooms" ("created_at", "student_id", "teacher_id", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Tue, 28 Feb 2012 03:32:12 UTC +00:00], ["student_id", 1], ["teacher_id", 1], ["updated_at", Tue, 28 Feb 2012 03:32:12 UTC +00:00]]
  SQL (0.2ms)  UPDATE "students" SET "teachers_count" = COALESCE("teachers_count", 0) + 1 WHERE "students"."id" = 1
  Teacher Load (0.1ms)  SELECT "teachers".* FROM "teachers" WHERE "teachers"."id" = 1 LIMIT 1
  SQL (0.1ms)  UPDATE "teachers" SET "students_count" = COALESCE("students_count", 0) + 1 WHERE "teachers"."id" = 1
  SQL (0.0ms)  UPDATE "teachers" SET "students_count" = COALESCE("students_count", 0) + 1 WHERE "teachers"."id" = 1
  Student Load (0.2ms)  SELECT "students".* FROM "students" INNER JOIN "classrooms" ON "students"."id" = "classrooms"."student_id" WHERE "classrooms"."teacher_id" = 1
 => [#<Student id: 1, name: "Mary Ann", teachers_count: 1, created_at: "2012-02-28 03:32:12", updated_at: "2012-02-28 03:32:12">] 

Я поместил все тестовое приложение на github, если кто-то хотел бы посмотреть поближе (https://github.com/carlzulauf/test_app). Я также создал модульный тест, который демонстрирует проблему и не проходит (https://github.com/carlzulauf/test_app/blob/master/test/unit/classroom_test.rb)

1 Ответ

12 голосов
/ 28 февраля 2012

Пока что мои исследования показали, что это, вероятно, ошибка. Вот некоторые проблемы github, уже поданные для этой проблемы:

https://github.com/rails/rails/issues/3903

https://github.com/rails/rails/issues/3085

По-видимому, существует недокументированный кеш автоматического счетчика, вызванный отношениями has_many: through. Поэтому, если Teacher.has_many :students, :through => :classrooms, то teacher.students << student подборки подборок уже ищут и увеличивают teacher.students_count, если этот столбец существует.

Если вы добавите Classroom.belongs_to :teacher, :counter_cache => :students_count, то при создании модели класса будет вызван дополнительный обратный вызов, и столбец будет увеличен в два раза .

Эффективное обходное решение: переименование столбцов кэша счетчика в другое. Student#teacherz_count и Teacher#studentz_count позволили моему контрольному кейсу пройти.

https://github.com/carlzulauf/test_app/commit/707a33f948d5d55a8aa942e825841fdd8a7e7705

Я еще не смог найти, где проблема кроется в базе кода ActiveRecord, поэтому я не буду принимать свой собственный ответ некоторое время, если кто-то знает, почему has_many :through работает таким образом и где код, вызывающий ошибку живет.

Обновление

Мне кажется, я нашел неправильную строку кода. Комментирование этой строки решает проблему:

https://github.com/rails/rails/blob/889e8bee82ea4f75adb6de5badad512d2c615b7f/activerecord/lib/active_record/associations/has_many_through_association.rb#L53

Кажется, я не могу запустить краевые рельсы, поэтому я пока не могу подать заявку на эту ошибку. Если кто-то другой может, пожалуйста, сделайте.

Обнаружение строки обидчика позволило мне создать более эффективное исправление обезьяны в моем тестовом приложении, которое решает проблему без переименования каких-либо столбцов.

https://github.com/carlzulauf/test_app/commit/3c421b035bd032b91ff60e3d74b957651c37c7fa

...