ActiveRecord has_many: by duplicating mass cache counters

It seems that the ActiveRecord counter_cache function may cause the counting cache to be doubled. The scenario in which I observe this behavior is when I have two models that have has_many :through communication with each other through a union model (i.e. Teacher has a lot of Student through Classroom ). When using the created has_many :through methods to directly link the Teacher and the Student (without manually creating a union record), the counter is increased up to 2 times. Example: teacher.students << Student.create(name: "Bobby Joe") calls teacher.students_count to increase by 2.

Please help me find a solution that mitigates or eliminates this problem by allowing me to continue to use the built-in caching meters and a mass ratio of the appointment through has_many :through .

I spent many hours finding a solution and saved the problem from a small test application, which is the simplest example of failure that I could create. Any additional data needed to help me solve this problem should be lower.

Example circuit and models:

 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 

Here is a short rails console session showing the steps taken and the fact that the rails perform two upgrades to teachers to increase 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">] 

I put the whole test application on github if someone wants to take a closer look ( https://github.com/carlzulauf/test_app ). I also created a unit test that demonstrates the problem and fails ( https://github.com/carlzulauf/test_app/blob/master/test/unit/classroom_test.rb )

+6
source share
1 answer

So far, my research has told me that this is probably a mistake. Here are some github issues already filed for this issue:

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

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

There seems to be an undocumented automatic cache counter called by has_many: through relationships. Therefore, if Teacher.has_many :students, :through => :classrooms , then teacher.students << student assignment collections are already looking for and increasing teacher.students_count if this column exists.

If you add Classroom.belongs_to :teacher, :counter_cache => :students_count , then when you create the class model, an additional callback will be called, and the column will be enlarged twice.

Effective work: rename the cache counter columns to another. Student#teacherz_count and Teacher#studentz_count were effective, letting me pass my test case .

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

I have not yet been able to find where the problem is in the ActiveRecord code base, so I won’t take my own answer for some time if someone knows why has_many :through works this way and where the violation code lives.

Update

I believe I found a line of code violation. Commenting out this line solves the problem:

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

I cannot seem that the cutting rails are working and working, so I cannot imagine the traction for this error. If anyone else, please do.

Finding the line of violation allowed me to create a more efficient monkey patch in my test application that solves the problem without renaming any columns.

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

+12
source

Source: https://habr.com/ru/post/909510/


All Articles