未验证 提交 1da9a7e4 编写于 作者: K Kasper Timm Hansen

Merge pull request #34378 'collection-cache-versioning'

Signed-off-by: NKasper Timm Hansen <kaspth@gmail.com>
* 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 Integration
#
# 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 @@ def many?
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 @@ def compute_cache_key(timestamp_column = :updated_at) # :nodoc:
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 @@ def compute_cache_key(timestamp_column = :updated_at) # :nodoc:
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 @@ class CollectionCacheKeyTest < ActiveRecord::TestCase
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 @@ def load_defaults(target_version)
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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册