counter cache

This commit is contained in:
Dylan Knutson
2025-07-21 06:03:48 +00:00
parent 3a8d71e2f7
commit e234b48e7f
4 changed files with 167 additions and 42 deletions

View File

@@ -235,10 +235,20 @@ module HasAuxTable
if self.method_defined?(column_name.to_sym)
raise "invariant: method #{column_name} already defined"
end
config.define_aux_attribute_delegate(column_name)
config.define_aux_attribute_delegate(:"#{column_name}?")
config.define_aux_attribute_delegate(:"#{column_name}=")
[
"",
"_in_database",
"?",
"=",
"_changed?",
"_change",
%w[clear_ _change]
].each do |mod|
prefix, suffix = mod.is_a?(Array) ? mod : ["", mod]
config.define_aux_attribute_delegate(
:"#{prefix}#{column_name}#{suffix}"
)
end
end
result

View File

@@ -190,7 +190,10 @@ module HasAuxTable
main_class.define_method(method_name) do |*args, **kwargs, &block|
T.bind(self, ActiveRecord::Base)
aux_model = config.aux_model_for(self)
ret =
T.unsafe(aux_model).public_send(method_name, *args, **kwargs, &block)
puts "#{self.class.name}##{method_name} -> #{config.aux_association_name} -> (#{args.inspect} => #{ret})"
ret
end
end

View File

@@ -879,25 +879,79 @@ RSpec.describe HasAuxTable do
end
describe "counter cache" do
before do
@reader = Reader.create!(name: "John Doe", reading_speed: 100)
@book =
def verify_counter_cache(model, assoc_name, expected_count)
expect(model.send("#{assoc_name}_count")).to eq(expected_count)
expect(model.send(assoc_name).count).to eq(expected_count || 0)
model.reload
expect(model.send("#{assoc_name}_count")).to eq(expected_count)
expect(model.send(assoc_name).count).to eq(expected_count || 0)
end
let(:reader) { Reader.create!(name: "John Doe", reading_speed: 100) }
let(:book) do
Book.create!(title: "The Great Gatsby", author: "F. Scott Fitzgerald")
end
it "updates counter caches that are on the aux model" do
@reader.read_books << @book
expect(@reader.read_book_joins_count).to eq(1)
expect(@reader.read_book_joins.count).to eq(1)
end
it "updates counter caches that are on the main table of an aux model" do
verify_counter_cache(reader, :read_book_joins, nil)
reader.read_books << book
verify_counter_cache(reader, :read_book_joins, 1)
end
it "updates counter caches on a non-aux model" do
@reader.read_books << @book
expect(@book.read_book_joins_count).to eq(1)
expect(@book.read_book_joins.count).to eq(1)
verify_counter_cache(book, :read_book_joins, nil)
reader.read_books << book
verify_counter_cache(book, :read_book_joins, 1)
end
it "has_one is a base class, belongs_to is a subclass, created via subclass" do
a = ModelA.create!(a_field1: "a_0")
verify_counter_cache(a, :model_bs, nil)
5.times do |i|
ModelB.create!(model_a: a, b_field1: "b_#{i}")
verify_counter_cache(a, :model_bs, i + 1)
end
end
it "has_one is a base class, belongs_to is a subclass, created via association" do
a = ModelA.create!(a_field1: "a_0")
verify_counter_cache(a, :model_bs, nil)
5.times do |i|
a.model_bs.create!(b_field1: "b_#{i}")
verify_counter_cache(a, :model_bs, i + 1)
end
end
it "has_one is a subclass, belongs_to is a subclass, created via subclass" do
a = ModelA2.create!(a_field1: "a2_0", a2_field1: "a2_0")
verify_counter_cache(a, :model_b2s, nil)
5.times do |i|
a.model_b2s.create!(b_field1: "b_#{i}")
verify_counter_cache(a, :model_b2s, i + 1)
end
end
it "has_one is a subclass, belongs_to is a subclass, created via association" do
a = ModelA2.create!(a_field1: "a2_0", a2_field1: "a2_0")
verify_counter_cache(a, :model_b2s, nil)
5.times do |i|
a.model_b2s.create!(b_field1: "b_#{i}")
verify_counter_cache(a, :model_b2s, i + 1)
end
end
it "has_one is subclass, belongs_to is a vanilla class" do
a = ModelA1.create!(a_field1: "a1_0", a1_field1: "a1_0")
verify_counter_cache(a, :model_cs, nil)
5.times do |i|
a.model_cs.create!(c_field1: "c_#{i}")
verify_counter_cache(a, :model_cs, i + 1)
end
end
end

View File

@@ -32,8 +32,6 @@ ActiveRecord::Schema.define do
create_base_table :people do |t|
t.string :name
t.integer :friends_count
t.integer :lovers_count
t.timestamps
t.create_aux :driver do |t|
@@ -286,29 +284,89 @@ class User < ActiveRecord::Base
has_many :posts, inverse_of: :user, class_name: "Post"
end
# class TwitterUser < User
# aux_table :twitter
# validates :twitter_handle, presence: true
# has_many :posts, inverse_of: :user, class_name: "TwitterPost"
# end
ActiveRecord::Schema.define do
create_base_table :model_as do |t|
t.string :a_field1
t.integer :model_bs_count
# class RedditUser < User
# aux_table :reddit
# validates :reddit_handle, presence: true
# has_many :posts, inverse_of: :user, class_name: "RedditPost"
# end
t.create_aux :a1 do |t|
t.integer :a1_field1
t.integer :model_cs_count
end
# class Post < ActiveRecord::Base
# include HasAuxTable
# belongs_to :user, inverse_of: :posts
# end
t.create_aux :a2 do |t|
t.string :a2_field1
t.integer :model_b2s_count
end
end
# class TwitterPost < Post
# aux_table :twitter
# belongs_to :user, inverse_of: :posts, class_name: "TwitterUser"
# end
create_base_table :model_bs do |t|
t.string :b_field1
t.references :model_a, foreign_key: { to_table: :model_as }
# class RedditPost < Post
# aux_table :reddit
# belongs_to :user, inverse_of: :posts, class_name: "RedditUser"
# end
t.create_aux :b1 do |t|
t.integer :b1_field1
end
t.create_aux :b2 do |t|
t.references :model_a2,
foreign_key: {
to_table: :model_as_a2_aux,
primary_key: :base_table_id
}
end
end
create_table :model_cs do |t|
t.string :c_field1
t.references :model_a1,
foreign_key: {
to_table: :model_as_a1_aux,
primary_key: :base_table_id
}
end
# A* has_many B
# A1 has_many C
# B belongs_to A
# C belongs_to A1
end
class ModelA < ActiveRecord::Base
include HasAuxTable
has_many :model_bs, inverse_of: :model_a
validates :a_field1, presence: true, uniqueness: true
end
class ModelA1 < ModelA
aux_table :a1
has_many :model_cs, inverse_of: :model_a1
validates :a1_field1, presence: true, uniqueness: true
end
class ModelA2 < ModelA
aux_table :a2
has_many :model_b2s, inverse_of: :model_a2
validates :a2_field1, presence: true, uniqueness: true
end
class ModelB < ActiveRecord::Base
include HasAuxTable
belongs_to :model_a, inverse_of: :model_bs, counter_cache: true
validates :b_field1, presence: true, uniqueness: true
end
class ModelB1 < ModelB
aux_table :b1
validates :b1_field1, presence: true, uniqueness: true
end
class ModelB2 < ModelB
aux_table :b2
belongs_to :model_a2, inverse_of: :model_b2s, counter_cache: true
end
class ModelC < ActiveRecord::Base
belongs_to :model_a1, inverse_of: :model_cs, counter_cache: true
validates :c_field1, presence: true, uniqueness: true
end