From 476e3f552f59d208cb284f509760b44ad780c17a Mon Sep 17 00:00:00 2001
From: "Alberto F. Capel" <afcapel@gmail.com>
Date: Tue, 14 Jul 2015 23:47:16 +0100
Subject: Add #cache_key to ActiveRecord::Relation.

---
 activerecord/lib/active_record.rb                  |  1 +
 activerecord/lib/active_record/base.rb             |  1 +
 .../lib/active_record/collection_cache_key.rb      | 29 ++++++++++++++++++++++
 activerecord/lib/active_record/relation.rb         | 26 +++++++++++++++++++
 4 files changed, 57 insertions(+)
 create mode 100644 activerecord/lib/active_record/collection_cache_key.rb

(limited to 'activerecord/lib')

diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index f5cf92db64..264f869c68 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -53,6 +53,7 @@ module ActiveRecord
   autoload :Persistence
   autoload :QueryCache
   autoload :Querying
+  autoload :CollectionCacheKey
   autoload :ReadonlyAttributes
   autoload :RecordInvalid, 'active_record/validations'
   autoload :Reflection
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index c918e88590..55a7e053bc 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -280,6 +280,7 @@ module ActiveRecord #:nodoc:
     extend Explain
     extend Enum
     extend Delegation::DelegateCache
+    extend CollectionCacheKey
 
     include Core
     include Persistence
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
new file mode 100644
index 0000000000..72b50c1d28
--- /dev/null
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -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(&timestamp_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
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index e4df122af6..3ed04dee3b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -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
-- 
cgit v1.2.3