quorak
(Quorak)
October 2, 2020, 11:47am
1
Hey folks,
I have a query of a has_many relation, that I want to cache. The cache_key should be invalid based on max(updated_at) but also based on max(belongs_to.updated_at).
Some code for illustration:
class Article < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :article, touch: true
end
The following will work because I touch Article on every Comment update
Article.includes(:comments).all.cache_version
But
Comment.includes(:article).all.cache_version
will not get a new cache_version when article updated
I would like to propose, to
support multiple timestamp fields in ActiveRecord::Relation.cache_version
support table names in ActiveRecord::Relation.cache_version
Comment.includes(:article).cache_version('articles.updated_at','comments.updated_at')
What do you think?
quorak
(Quorak)
November 26, 2020, 2:50pm
2
I have created an extension for comput_cache_version
that accepts array’s of timestamp columns like:
@slots.cache_key(%w[slots.updated_at advices.updated_at])
to generate a query like
SELECT COUNT(*) AS "size", GREATEST(MAX(slots.updated_at),MAX(advices.updated_at)) AS timestamp FROM [...]
sourcecode is
module RailsExtensions
module ActiveRecord
module Relation
module CacheKeyForRelationalTables
def compute_cache_version(timestamp_column)
# :nodoc:
if loaded? || distinct_value
size = records.size
timestamp = max_by(×tamp_column)._read_attribute(timestamp_column) if size.positive?
else
collection = eager_loading? ? apply_join_dependency : self
column = if timestamp_column.is_a?(Array)
column_placeholder = Array.new(timestamp_column.length, 'MAX(%s)').join(',')
select_values = "COUNT(*) AS #{connection.quote_column_name('size')}, " \
"GREATEST(#{column_placeholder}) AS timestamp"
timestamp_column.map do |c|
connection.visitor.compile(Arel.sql(c))
end
elsif timestamp_column.is_a?(String) && timestamp_column.include?('.')
connection.visitor.compile(Arel.sql(timestamp_column))
else
select_values = "COUNT(*) AS #{connection.quote_column_name('size')}, " \
'MAX(%s) AS timestamp'
connection.visitor.compile(arel_attribute(timestamp_column))
end
if collection.has_limit_or_offset?
query = collection.select("#{column} AS collection_cache_key_timestamp")
subquery_alias = 'subquery_for_cache_key'
subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
arel = query.build_subquery(subquery_alias, select_values % subquery_column)
else
query = collection.unscope(:order)
query.select_values = [select_values % column]
arel = query.arel
end
result = connection.select_one(arel, nil)
if result
column_type = klass.type_for_attribute(timestamp_column)
timestamp = column_type.deserialize(result['timestamp'])
size = result['size']
else
timestamp = nil
size = 0
end
end
if timestamp
"#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
else
size.to_s
end
end
end
end
end
end
ActiveRecord::Relation.prepend RailsExtensions::ActiveRecord::Relation::CacheKeyForRelationalTables
Any thoughts or suggestions?
best