aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md9
-rw-r--r--activerecord/lib/active_record/integration.rb49
-rw-r--r--activerecord/test/cases/cache_key_test.rb22
-rw-r--r--activerecord/test/cases/integration_test.rb48
4 files changed, 117 insertions, 11 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 4e264f5f2a..beead181f3 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,12 @@
+* Add ActiveRecord::Base#cache_version to support recyclable cache keys via the new versioned entries
+ in ActiveSupport::Cache. This also means that ActiveRecord::Base#cache_key will now return a stable key
+ that does not include a timestamp 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.cache_versioning = true. That's the setting for all new apps on Rails 5.2+
+
+ *DHH*
+
* Respect 'SchemaDumper.ignore_tables' in rake tasks for databases structure dump
*Rusty Geldmacher*, *Guillermo Iguaran*
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 8e71b60b29..ed652e26aa 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -13,6 +13,15 @@ module ActiveRecord
# This is +:usec+, by default.
class_attribute :cache_timestamp_format, instance_writer: false
self.cache_timestamp_format = :usec
+
+ ##
+ # :singleton-method:
+ # Indicates whether to use a stable #cache_key method that is accompanied
+ # by a changing version in the #cache_version method.
+ #
+ # This is +false+, by default until Rails 6.0.
+ class_attribute :cache_versioning, instance_writer: false
+ self.cache_versioning = false
end
# Returns a +String+, which Action Pack uses for constructing a URL to this
@@ -52,25 +61,47 @@ module ActiveRecord
# used to generate the key:
#
# Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ #
+ # If ActiveRecord::Base.cache_versioning is turned on, no version will be included
+ # in the cache key. The version will instead be supplied by #cache_version. This
+ # separation enables recycling of cache keys.
+ #
+ # Product.cache_versioning = true
+ # Product.new.cache_key # => "products/new"
+ # Person.find(5).cache_key # => "people/5" (even if updated_at available)
def cache_key(*timestamp_names)
if new_record?
"#{model_name.cache_key}/new"
else
- timestamp = if timestamp_names.any?
- max_updated_column_timestamp(timestamp_names)
+ if cache_version && timestamp_names.none?
+ "#{model_name.cache_key}/#{id}"
else
- max_updated_column_timestamp
- end
+ timestamp = if timestamp_names.any?
+ max_updated_column_timestamp(timestamp_names)
+ else
+ max_updated_column_timestamp
+ end
- if timestamp
- timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{model_name.cache_key}/#{id}-#{timestamp}"
- else
- "#{model_name.cache_key}/#{id}"
+ if timestamp
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
+ else
+ "#{model_name.cache_key}/#{id}"
+ end
end
end
end
+ # Returns a cache version that can be used together with the cache key to form
+ # a recyclable caching scheme. By default, the #updated_at column is used for the
+ # cache_version, but this method can be overwritten to return something else.
+ #
+ # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
+ # +false+ (which it is by default until Rails 6.0).
+ def cache_version
+ try(:updated_at).try(:to_i) if cache_versioning
+ end
+
module ClassMethods
# Defines your model's +to_param+ method to generate "pretty" URLs
# using +method_name+, which can be any attribute or method that
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
index 2c6a38ec35..f74cb18244 100644
--- a/activerecord/test/cases/cache_key_test.rb
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -4,15 +4,23 @@ module ActiveRecord
class CacheKeyTest < ActiveRecord::TestCase
self.use_transactional_tests = false
- class CacheMe < ActiveRecord::Base; end
+ class CacheMe < ActiveRecord::Base
+ self.cache_versioning = false
+ end
+
+ class CacheMeWithVersion < ActiveRecord::Base
+ self.cache_versioning = true
+ end
setup do
@connection = ActiveRecord::Base.connection
- @connection.create_table(:cache_mes) { |t| t.timestamps }
+ @connection.create_table(:cache_mes, force: true) { |t| t.timestamps }
+ @connection.create_table(:cache_me_with_versions, force: true) { |t| t.timestamps }
end
teardown do
@connection.drop_table :cache_mes, if_exists: true
+ @connection.drop_table :cache_me_with_versions, if_exists: true
end
test "cache_key format is not too precise" do
@@ -21,5 +29,15 @@ module ActiveRecord
assert_equal key, record.reload.cache_key
end
+
+ test "cache_key has no version when versioning is on" do
+ record = CacheMeWithVersion.create
+ assert_equal "active_record/cache_key_test/cache_me_with_versions/#{record.id}", record.cache_key
+ end
+
+ test "cache_version is only there when versioning is on" do
+ assert CacheMeWithVersion.create.cache_version.present?
+ assert_not CacheMe.create.cache_version.present?
+ end
end
end
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index 0678bb714f..7ffa86c42f 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -177,4 +177,52 @@ class IntegrationTest < ActiveRecord::TestCase
owner.happy_at = nil
assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at)
end
+
+ def test_cache_key_is_stable_with_versioning_on
+ Developer.cache_versioning = true
+
+ developer = Developer.first
+ first_key = developer.cache_key
+
+ developer.touch
+ second_key = developer.cache_key
+
+ assert_equal first_key, second_key
+ ensure
+ Developer.cache_versioning = false
+ end
+
+ def test_cache_version_changes_with_versioning_on
+ Developer.cache_versioning = true
+
+ developer = Developer.first
+ first_version = developer.cache_version
+
+ travel 10.seconds do
+ developer.touch
+ end
+
+ second_version = developer.cache_version
+
+ assert_not_equal first_version, second_version
+ ensure
+ Developer.cache_versioning = false
+ end
+
+ def test_cache_key_retains_version_when_custom_timestamp_is_used
+ Developer.cache_versioning = true
+
+ developer = Developer.first
+ first_key = developer.cache_key(:updated_at)
+
+ travel 10.seconds do
+ developer.touch
+ end
+
+ second_key = developer.cache_key(:updated_at)
+
+ assert_not_equal first_key, second_key
+ ensure
+ Developer.cache_versioning = false
+ end
end