mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add #cache_key to ActiveRecord::Relation.
This commit is contained in:
parent
6ffec3c16c
commit
476e3f552f
6 changed files with 137 additions and 0 deletions
|
@ -1,3 +1,13 @@
|
||||||
|
* Add `cache_key` to ActiveRecord::Relation.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
@users = User.where("name like ?", "%Alberto%")
|
||||||
|
@users.cache_key
|
||||||
|
=> "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000"
|
||||||
|
|
||||||
|
*Alberto Fernández-Capel*
|
||||||
|
|
||||||
* Fix a bug where counter_cache doesn't always work with polymorphic
|
* Fix a bug where counter_cache doesn't always work with polymorphic
|
||||||
relations.
|
relations.
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ module ActiveRecord
|
||||||
autoload :Persistence
|
autoload :Persistence
|
||||||
autoload :QueryCache
|
autoload :QueryCache
|
||||||
autoload :Querying
|
autoload :Querying
|
||||||
|
autoload :CollectionCacheKey
|
||||||
autoload :ReadonlyAttributes
|
autoload :ReadonlyAttributes
|
||||||
autoload :RecordInvalid, 'active_record/validations'
|
autoload :RecordInvalid, 'active_record/validations'
|
||||||
autoload :Reflection
|
autoload :Reflection
|
||||||
|
|
|
@ -280,6 +280,7 @@ module ActiveRecord #:nodoc:
|
||||||
extend Explain
|
extend Explain
|
||||||
extend Enum
|
extend Enum
|
||||||
extend Delegation::DelegateCache
|
extend Delegation::DelegateCache
|
||||||
|
extend CollectionCacheKey
|
||||||
|
|
||||||
include Core
|
include Core
|
||||||
include Persistence
|
include Persistence
|
||||||
|
|
29
activerecord/lib/active_record/collection_cache_key.rb
Normal file
29
activerecord/lib/active_record/collection_cache_key.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module CollectionCacheKey
|
||||||
|
|
||||||
|
def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
|
||||||
|
query_signature = Digest::MD5.hexdigest(collection.to_sql)
|
||||||
|
key = "#{collection.model_name.cache_key}/query-#{query_signature}"
|
||||||
|
|
||||||
|
if collection.loaded?
|
||||||
|
size = collection.size
|
||||||
|
timestamp = collection.max_by(×tamp_column).public_send(timestamp_column)
|
||||||
|
else
|
||||||
|
column_type = type_for_attribute(timestamp_column.to_s)
|
||||||
|
column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}"
|
||||||
|
|
||||||
|
query = collection.select("COUNT(*) AS size", "MAX(#{column}) AS timestamp")
|
||||||
|
result = connection.select_one(query)
|
||||||
|
|
||||||
|
size = result["size"]
|
||||||
|
timestamp = column_type.deserialize(result["timestamp"])
|
||||||
|
end
|
||||||
|
|
||||||
|
if timestamp
|
||||||
|
"#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
|
||||||
|
else
|
||||||
|
"#{key}-#{size}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -298,6 +298,32 @@ module ActiveRecord
|
||||||
limit_value ? to_a.many? : size > 1
|
limit_value ? to_a.many? : size > 1
|
||||||
end
|
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.
|
||||||
|
#
|
||||||
|
# Product.where("name like ?", "%Cosmic Encounter%").cache_key
|
||||||
|
# => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
|
||||||
|
#
|
||||||
|
# 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%')
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
|
end
|
||||||
|
|
||||||
# Scope all queries to the current scope.
|
# Scope all queries to the current scope.
|
||||||
#
|
#
|
||||||
# Comment.where(post_id: 1).scoping do
|
# Comment.where(post_id: 1).scoping do
|
||||||
|
|
70
activerecord/test/cases/collection_cache_key_test.rb
Normal file
70
activerecord/test/cases/collection_cache_key_test.rb
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
require "cases/helper"
|
||||||
|
require "models/computer"
|
||||||
|
require "models/developer"
|
||||||
|
require "models/project"
|
||||||
|
require "models/topic"
|
||||||
|
require "models/post"
|
||||||
|
require "models/comment"
|
||||||
|
|
||||||
|
module ActiveRecord
|
||||||
|
class CollectionCacheKeyTest < ActiveRecord::TestCase
|
||||||
|
fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts
|
||||||
|
|
||||||
|
test "collection_cache_key on model" do
|
||||||
|
assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cache_key for relation" do
|
||||||
|
developers = Developer.where(name: "David")
|
||||||
|
last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at
|
||||||
|
|
||||||
|
assert_match /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key
|
||||||
|
|
||||||
|
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key
|
||||||
|
|
||||||
|
assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
|
||||||
|
assert_equal developers.count.to_s, $2
|
||||||
|
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it triggers at most one query" do
|
||||||
|
developers = Developer.where(name: "David")
|
||||||
|
|
||||||
|
assert_queries(1) { developers.cache_key }
|
||||||
|
assert_queries(0) { developers.cache_key }
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it doesn't trigger any query if the relation is already loaded" do
|
||||||
|
developers = Developer.where(name: "David").load
|
||||||
|
assert_queries(0) { developers.cache_key }
|
||||||
|
end
|
||||||
|
|
||||||
|
test "relation cache_key changes when the sql query changes" do
|
||||||
|
developers = Developer.where(name: "David")
|
||||||
|
other_relation = Developer.where(name: "David").where("1 = 1")
|
||||||
|
|
||||||
|
assert_not_equal developers.cache_key, other_relation.cache_key
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cache_key for empty relation" do
|
||||||
|
developers = Developer.where(name: "Non Existent Developer")
|
||||||
|
assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cache_key with custom timestamp column" do
|
||||||
|
topics = Topic.where("title like ?", "%Topic%")
|
||||||
|
last_topic_timestamp = topics(:fifth).written_on.utc.to_s(:nsec)
|
||||||
|
assert_match(last_topic_timestamp, topics.cache_key(:written_on))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cache_key with unknown timestamp column" do
|
||||||
|
topics = Topic.where("title like ?", "%Topic%")
|
||||||
|
assert_raises(ActiveRecord::StatementInvalid) { topics.cache_key(:published_at) }
|
||||||
|
end
|
||||||
|
|
||||||
|
test "collection proxy provides a cache_key" do
|
||||||
|
developers = projects(:active_record).developers
|
||||||
|
assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue