Add #cache_key to ActiveRecord::Relation.
This commit is contained in:
parent
6ffec3c16c
commit
476e3f552f
|
@ -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
|
||||
relations.
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ module ActiveRecord
|
|||
autoload :Persistence
|
||||
autoload :QueryCache
|
||||
autoload :Querying
|
||||
autoload :CollectionCacheKey
|
||||
autoload :ReadonlyAttributes
|
||||
autoload :RecordInvalid, 'active_record/validations'
|
||||
autoload :Reflection
|
||||
|
|
|
@ -280,6 +280,7 @@ module ActiveRecord #:nodoc:
|
|||
extend Explain
|
||||
extend Enum
|
||||
extend Delegation::DelegateCache
|
||||
extend CollectionCacheKey
|
||||
|
||||
include Core
|
||||
include Persistence
|
||||
|
|
|
@ -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
|
||||
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.
|
||||
#
|
||||
# Comment.where(post_id: 1).scoping do
|
||||
|
|
|
@ -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 New Issue