mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add collection cache versioning
Cache versioning enables the same cache key to be reused when the object being cached changes by moving the volatile part of the cache key out of the cache key and into a version that is embedded in the cache entry. This is already occurring when the object being cached is an `ActiveRecord::Base`, but when caching an `ActiveRecord::Relation` we are currently still putting the volatile information (max updated at and count) as part of the cache key. This PR moves the volatile part of the relations `cache_key` into the `cache_version` to support recycling cache keys for `ActiveRecord::Relation`s.
This commit is contained in:
parent
758ba117a0
commit
4f2ac80d4c
5 changed files with 93 additions and 15 deletions
|
@ -1,3 +1,14 @@
|
|||
* Add `ActiveRecord::Relation#cache_version` to support recyclable cache keys via
|
||||
the versioned entries in `ActiveSupport::Cache`. This also means that
|
||||
`ActiveRecord::Relation#cache_key` will now return a stable key that does not
|
||||
include the max timestamp or count any more.
|
||||
|
||||
NOTE: This feature is turned off by default, and `cache_key` will still return
|
||||
cache keys with timestamps until you set `ActiveRecord::Base.collection_cache_versioning = true`.
|
||||
That's the setting for all new apps on Rails 6.0+
|
||||
|
||||
*Lachlan Sylvester*
|
||||
|
||||
* Fix dirty tracking for `touch` to track saved changes.
|
||||
|
||||
Fixes #33429.
|
||||
|
|
|
@ -22,6 +22,14 @@ module ActiveRecord
|
|||
#
|
||||
# This is +true+, by default on Rails 5.2 and above.
|
||||
class_attribute :cache_versioning, instance_writer: false, default: false
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Indicates whether to use a stable #cache_key method that is accompanied
|
||||
# by a changing version in the #cache_version method on collections.
|
||||
#
|
||||
# This is +false+, by default until Rails 6.1.
|
||||
class_attribute :collection_cache_versioning, instance_writer: false, default: false
|
||||
end
|
||||
|
||||
# Returns a +String+, which Action Pack uses for constructing a URL to this
|
||||
|
|
|
@ -291,27 +291,23 @@ module ActiveRecord
|
|||
limit_value ? records.many? : size > 1
|
||||
end
|
||||
|
||||
# Returns a cache key that can be used to identify the records fetched by
|
||||
# this query. The cache key is built with a fingerprint of the sql query,
|
||||
# the number of records matched by the query and a timestamp of the last
|
||||
# updated record. When a new record comes to match the query, or any of
|
||||
# the existing records is updated or deleted, the cache key changes.
|
||||
# Returns a stable cache key that can be used to identify this query.
|
||||
# The cache key is built with a fingerprint of the SQL query.
|
||||
#
|
||||
# Product.where("name like ?", "%Cosmic Encounter%").cache_key
|
||||
# # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
|
||||
# Product.where("name like ?", "%Cosmic Encounter%").cache_key
|
||||
# # => "products/query-1850ab3d302391b85b8693e941286659"
|
||||
#
|
||||
# If the collection is loaded, the method will iterate through the records
|
||||
# to generate the timestamp, otherwise it will trigger one SQL query like:
|
||||
# If ActiveRecord::Base.collection_cache_versioning is turned off, as it was
|
||||
# in Rails 6.0 and earlier, the cache key will also include a version.
|
||||
#
|
||||
# SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
|
||||
# ActiveRecord::Base.collection_cache_versioning = false
|
||||
# Product.where("name like ?", "%Cosmic Encounter%").cache_key
|
||||
# # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
|
||||
#
|
||||
# You can also pass a custom timestamp column to fetch the timestamp of the
|
||||
# last updated record.
|
||||
#
|
||||
# Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
|
||||
#
|
||||
# You can customize the strategy to generate the key on a per model basis
|
||||
# overriding ActiveRecord::Base#collection_cache_key.
|
||||
def cache_key(timestamp_column = :updated_at)
|
||||
@cache_keys ||= {}
|
||||
@cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
|
||||
|
@ -321,6 +317,31 @@ module ActiveRecord
|
|||
query_signature = ActiveSupport::Digest.hexdigest(to_sql)
|
||||
key = "#{klass.model_name.cache_key}/query-#{query_signature}"
|
||||
|
||||
if cache_version(timestamp_column)
|
||||
key
|
||||
else
|
||||
"#{key}-#{compute_cache_version(timestamp_column)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a cache version that can be used together with the cache key to form
|
||||
# a recyclable caching scheme. The cache version is built with the number of records
|
||||
# matching the query, and the timestamp of the last updated record. When a new record
|
||||
# comes to match the query, or any of the existing records is updated or deleted,
|
||||
# the cache version changes.
|
||||
#
|
||||
# If the collection is loaded, the method will iterate through the records
|
||||
# to generate the timestamp, otherwise it will trigger one SQL query like:
|
||||
#
|
||||
# SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
|
||||
def cache_version(timestamp_column = :updated_at)
|
||||
if collection_cache_versioning
|
||||
@cache_versions ||= {}
|
||||
@cache_versions[timestamp_column] ||= compute_cache_version(timestamp_column)
|
||||
end
|
||||
end
|
||||
|
||||
def compute_cache_version(timestamp_column) # :nodoc:
|
||||
if loaded? || distinct_value
|
||||
size = records.size
|
||||
if size > 0
|
||||
|
@ -356,9 +377,9 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
if timestamp
|
||||
"#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
|
||||
"#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
|
||||
else
|
||||
"#{key}-#{size}"
|
||||
"#{size}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -171,5 +171,39 @@ module ActiveRecord
|
|||
|
||||
assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
|
||||
end
|
||||
|
||||
test "cache_key should be stable when using collection_cache_versioning" do
|
||||
with_collection_cache_versioning do
|
||||
developers = Developer.where(salary: 100000)
|
||||
|
||||
assert_match(/\Adevelopers\/query-(\h+)\z/, developers.cache_key)
|
||||
|
||||
/\Adevelopers\/query-(\h+)\z/ =~ developers.cache_key
|
||||
|
||||
assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
|
||||
end
|
||||
end
|
||||
|
||||
test "cache_version for relation" do
|
||||
with_collection_cache_versioning do
|
||||
developers = Developer.where(salary: 100000).order(updated_at: :desc)
|
||||
last_developer_timestamp = developers.first.updated_at
|
||||
|
||||
assert_match(/(\d+)-(\d+)\z/, developers.cache_version)
|
||||
|
||||
/(\d+)-(\d+)\z/ =~ developers.cache_version
|
||||
|
||||
assert_equal developers.count.to_s, $1
|
||||
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $2
|
||||
end
|
||||
end
|
||||
|
||||
def with_collection_cache_versioning(value = true)
|
||||
@old_collection_cache_versioning = ActiveRecord::Base.collection_cache_versioning
|
||||
ActiveRecord::Base.collection_cache_versioning = value
|
||||
yield
|
||||
ensure
|
||||
ActiveRecord::Base.collection_cache_versioning = @old_collection_cache_versioning
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -142,6 +142,10 @@ module Rails
|
|||
active_storage.queues.analysis = :active_storage_analysis
|
||||
active_storage.queues.purge = :active_storage_purge
|
||||
end
|
||||
|
||||
if respond_to?(:active_record)
|
||||
active_record.collection_cache_versioning = true
|
||||
end
|
||||
else
|
||||
raise "Unknown version #{target_version.to_s.inspect}"
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue