diff --git a/lib/identity_cache/cached/attribute.rb b/lib/identity_cache/cached/attribute.rb index 9e222b49..3e72b49c 100644 --- a/lib/identity_cache/cached/attribute.rb +++ b/lib/identity_cache/cached/attribute.rb @@ -47,6 +47,20 @@ def expire_for_save(record) end end + def expire_for_values(values_hash) + key_values = key_fields.map { |name| values_hash.fetch(name) } + IdentityCache.cache.delete(cache_key_from_key_values(key_values)) + end + + def expire_for_update(old_values_hash, changes) + expire_for_values(old_values_hash) + + if key_fields.any? { |name| changes.key?(name) } + key_values = key_fields.map { |name| changes.fetch(name) { old_values_hash.fetch(name) } } + IdentityCache.cache.delete(cache_key_from_key_values(key_values)) + end + end + def cache_key(index_key) values_hash = IdentityCache.memcache_hash(unhashed_values_cache_key_string(index_key)) "#{model.rails_cache_key_namespace}#{cache_key_prefix}#{values_hash}" diff --git a/lib/identity_cache/configuration_dsl.rb b/lib/identity_cache/configuration_dsl.rb index bb5e65fc..f7450800 100644 --- a/lib/identity_cache/configuration_dsl.rb +++ b/lib/identity_cache/configuration_dsl.rb @@ -129,6 +129,7 @@ def cache_attribute_by_alias(attribute_or_proc, alias_name:, by:, unique:) cached_attribute = klass.new(self, attribute_or_proc, alias_name, fields, unique) cached_attribute.build cache_indexes.push(cached_attribute) + @cache_indexed_columns = nil end def ensure_base_model diff --git a/lib/identity_cache/parent_model_expiration.rb b/lib/identity_cache/parent_model_expiration.rb index fcd9e0e0..a8e290ee 100644 --- a/lib/identity_cache/parent_model_expiration.rb +++ b/lib/identity_cache/parent_model_expiration.rb @@ -41,6 +41,17 @@ def parent_expiration_entries ParentModelExpiration.install_pending_parent_expiry_hooks(cached_model) _parent_expiration_entries end + + def check_for_unsupported_parent_expiration_entries + return unless parent_expiration_entries.any? + msg = +"Unsupported manual expiration of #{name} record that is embedded in parent associations:\n" + parent_expiration_entries.each do |association_name, cached_associations| + cached_associations.each do |parent_class, _only_on_foreign_key_change| + msg << "- #{association_name}" + end + end + raise msg + end end included do diff --git a/lib/identity_cache/without_primary_index.rb b/lib/identity_cache/without_primary_index.rb index 5b4ed22c..3c0af040 100644 --- a/lib/identity_cache/without_primary_index.rb +++ b/lib/identity_cache/without_primary_index.rb @@ -26,6 +26,55 @@ module ClassMethods def primary_cache_index_enabled false end + + # Get only the columns whose values are needed to manually expire caches + # after updating or deleting rows without triggering after_commit callbacks. + # + # 1. Pass the returned columns into Active Record's `select` or `pluck` query + # method on the scope that will be used to modify the database in order to + # query original for these rows that will be modified. + # 2. Update or delete the rows + # 3. Use {expire_cache_for_update} or {expire_cache_for_delete} to expires the + # caches, passing in the values from the query in step 1 as the indexed_values. + # + # @return [Array] the array of column names + def cache_indexed_columns + @cache_indexed_columns ||= begin + check_for_unsupported_parent_expiration_entries + columns = Set.new + columns << primary_key.to_sym if primary_cache_index_enabled + cache_indexes.each do |cached_attribute| + columns.merge(cached_attribute.key_fields) + end + columns.to_a.freeze + end + end + + def expire_cache_for_update(old_indexed_values, changes) + if primary_cache_index_enabled + id = old_indexed_values.fetch(primary_key.to_sym) + expire_primary_key_cache_index(id) + end + cache_indexes.each do |cached_attribute| + cached_attribute.expire_for_update(old_indexed_values, changes) + end + check_for_unsupported_parent_expiration_entries + end + + private def expire_cache_for_insert_or_delete(indexed_values) + if primary_cache_index_enabled + id = indexed_values.fetch(primary_key.to_sym) + expire_primary_key_cache_index(id) + end + cache_indexes.each do |cached_attribute| + cached_attribute.expire_for_values(indexed_values) + end + check_for_unsupported_parent_expiration_entries + end + + alias_method :expire_cache_for_insert, :expire_cache_for_insert_or_delete + + alias_method :expire_cache_for_delete, :expire_cache_for_insert_or_delete end end end