identity_cache 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -4
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.rails40 +1 -0
  6. data/Gemfile.rails41 +1 -0
  7. data/Gemfile.rails42 +1 -0
  8. data/README.md +7 -0
  9. data/identity_cache.gemspec +1 -1
  10. data/lib/identity_cache.rb +5 -2
  11. data/lib/identity_cache/belongs_to_caching.rb +13 -5
  12. data/lib/identity_cache/cache_key_generation.rb +12 -19
  13. data/lib/identity_cache/configuration_dsl.rb +83 -84
  14. data/lib/identity_cache/parent_model_expiration.rb +4 -3
  15. data/lib/identity_cache/query_api.rb +93 -91
  16. data/lib/identity_cache/version.rb +2 -2
  17. data/test/attribute_cache_test.rb +42 -63
  18. data/test/deeply_nested_associated_record_test.rb +1 -0
  19. data/test/denormalized_has_many_test.rb +18 -0
  20. data/test/denormalized_has_one_test.rb +15 -5
  21. data/test/fetch_multi_test.rb +25 -3
  22. data/test/fetch_test.rb +20 -7
  23. data/test/fixtures/serialized_record.mysql2 +0 -0
  24. data/test/fixtures/serialized_record.postgresql +0 -0
  25. data/test/helpers/active_record_objects.rb +10 -0
  26. data/test/helpers/database_connection.rb +6 -1
  27. data/test/helpers/serialization_format.rb +1 -1
  28. data/test/index_cache_test.rb +50 -25
  29. data/test/normalized_belongs_to_test.rb +21 -6
  30. data/test/normalized_has_many_test.rb +44 -0
  31. data/test/{fetch_multi_with_batched_associations_test.rb → prefetch_normalized_associations_test.rb} +41 -3
  32. data/test/save_test.rb +14 -14
  33. data/test/schema_change_test.rb +2 -0
  34. data/test/test_helper.rb +4 -4
  35. metadata +11 -10
  36. data/Gemfile.rails32 +0 -5
  37. data/test/fixtures/serialized_record +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8b3399599b1784b0df560313cc902e20e7cd86fd
4
- data.tar.gz: 4a45bfa103d6095837bf2226a1ccd505219001fb
3
+ metadata.gz: e9c2e392201ed1b9c143a6ebff82f7c7af602a89
4
+ data.tar.gz: 445f3ce67f61a386db9bd99939d169e03562052b
5
5
  SHA512:
6
- metadata.gz: 50b692df952ca33119c152a4aaaa9bf0e0360c2a56005bd0711a707ea516031a12ceb7f8e9e9599b52f0ba5ce1568c33585b7598bdc9a6a1da018b7b42eec0a3
7
- data.tar.gz: e9519a582f37d3e029a43046157139ba37c3a897fda507febe592e1219a2d84968c3c10b68780a4c4397eeec9102cfd3ed4f69f41756818e7532b17ce4aa4d1e
6
+ metadata.gz: d3012a594fb51bcd37c08e322545b4cf3d9e2fe8ce3d14253df79cb423fd673f143e7dea5b8c7b13499660c56db9e266d3bc0fe6e923a6f1c37a918ba5d63fa4
7
+ data.tar.gz: a790523fd6989dde3669602a4ddd045032c44808f7e1f6a06f3e01fbf93e9ac137377e77fd360266a11642fdf7c6d5ef21ec1cb157a1d4ea37e2bde88dc5612c
@@ -2,7 +2,7 @@ language: ruby
2
2
 
3
3
  rvm:
4
4
  - 2.1
5
- - 2.2
5
+ - 2.2.3
6
6
 
7
7
  gemfile:
8
8
  - Gemfile.rails40
@@ -23,6 +23,4 @@ before_script:
23
23
  - mysql -e 'create database identity_cache_test'
24
24
  - psql -c 'create database identity_cache_test;' -U postgres
25
25
 
26
- matrix:
27
- allow_failures:
28
- - gemfile: Gemfile.rails42
26
+ cache: bundler
@@ -1,5 +1,27 @@
1
1
  # IdentityCache changelog
2
2
 
3
+ #### 0.2.6 (unreleased)
4
+ - Add support for includes option on cache_index and fetch_by_id
5
+ - Use ActiveRecord instantiate
6
+ - Add association pre-fetching support for fetch_by_id
7
+ - Remove support for 3.2
8
+ - Fix N+1 from fetching embedded ids on a cache miss
9
+ - Raise when trying to cache a through association. Previously it wouldn't be invalidated properly.
10
+ - Raise if a class method is called on a scope. Previously the scope was ignored.
11
+ - Raise if a class method is called on a subclass of one that included IdentityCache. This never worked properly.
12
+ - Fix cache_belongs_to on polymorphic assocations.
13
+ - Fetching a cache_belongs_to association no longer loads the belongs_to association
14
+
15
+ #### 0.2.5
16
+
17
+ - Fixed support for namespaced model classes
18
+ - Added some deduplication for parent cache expiry
19
+ - Fixed some deprecation warnings in rails 4.2
20
+
21
+ #### 0.2.4
22
+
23
+ - Refactoring, documentation and test changes
24
+
3
25
  #### 0.2.3
4
26
 
5
27
  - PostgreSQL support
data/Gemfile CHANGED
@@ -1,2 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
+
4
+ gem 'mysql2', '~> 0.3.13'
@@ -3,3 +3,4 @@ gemspec
3
3
 
4
4
  gem 'activerecord', '~> 4.0.4'
5
5
  gem 'activesupport', '~> 4.0.4'
6
+ gem 'mysql2', '~> 0.3.13'
@@ -3,3 +3,4 @@ gemspec
3
3
 
4
4
  gem 'activerecord', '~> 4.1.0'
5
5
  gem 'activesupport', '~> 4.1.0'
6
+ gem 'mysql2', '~> 0.3.13'
@@ -3,3 +3,4 @@ gemspec
3
3
 
4
4
  gem 'activerecord', '~> 4.2.0'
5
5
  gem 'activesupport', '~> 4.2.0'
6
+ gem 'mysql2', '~> 0.3.13'
data/README.md CHANGED
@@ -125,7 +125,9 @@ IdentityCache tries to figure out both sides of an association whenever it can s
125
125
 
126
126
  ``` ruby
127
127
  class Metafield < ActiveRecord::Base
128
+ include IdentityCache
128
129
  belongs_to :owner, :polymorphic => true
130
+ cache_belongs_to :owner
129
131
  end
130
132
 
131
133
  class Product < ActiveRecord::Base
@@ -183,6 +185,11 @@ _[:inverse_name]_ Specifies the name of parent object used by the association. T
183
185
  Example:
184
186
  `cache_has_one :configuration, :embed => true`
185
187
 
188
+ #### cache_belongs_to
189
+
190
+ Example:
191
+ `cache_belongs_to :shop`
192
+
186
193
  #### cache_attribute
187
194
 
188
195
  Options:
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.version = IdentityCache::VERSION
17
17
 
18
18
  gem.add_dependency('ar_transaction_changes', '~> 1.0')
19
- gem.add_dependency('activerecord', '>= 3.2')
19
+ gem.add_dependency('activerecord', '>= 4.0.4')
20
20
  gem.add_development_dependency('memcached', '~> 1.8.0')
21
21
 
22
22
  gem.add_development_dependency('memcached_store', '~> 0.12.6')
@@ -27,6 +27,9 @@ module IdentityCache
27
27
  super "Inverse name for association could not be determined. Please use the :inverse_name option to specify the inverse association name for this cache."
28
28
  end
29
29
  end
30
+ class UnsupportedScopeError < StandardError; end
31
+ class UnsupportedAssociationError < StandardError; end
32
+ class DerivedModelError < StandardError; end
30
33
 
31
34
  class << self
32
35
  include IdentityCache::CacheHash
@@ -38,7 +41,7 @@ module IdentityCache
38
41
  self.cache_namespace = "IDC:#{CACHE_VERSION}:".freeze
39
42
 
40
43
  def included(base) #:nodoc:
41
- raise AlreadyIncludedError if base.respond_to? :cache_indexes
44
+ raise AlreadyIncludedError if base.include?(IdentityCache::ConfigurationDSL)
42
45
 
43
46
  base.send(:include, ArTransactionChanges) unless base.include?(ArTransactionChanges)
44
47
  base.send(:include, IdentityCache::BelongsToCaching)
@@ -112,7 +115,7 @@ module IdentityCache
112
115
  return {} if keys.size == 0
113
116
 
114
117
  result = if should_use_cache?
115
- fetch_in_batches(keys) do |missed_keys|
118
+ fetch_in_batches(keys.uniq) do |missed_keys|
116
119
  results = yield missed_keys
117
120
  results.map {|e| map_cached_nil_for e }
118
121
  end
@@ -9,6 +9,7 @@ module IdentityCache
9
9
 
10
10
  module ClassMethods
11
11
  def cache_belongs_to(association, options = {})
12
+ ensure_base_model
12
13
  raise NotImplementedError if options[:embed]
13
14
 
14
15
  unless association_reflection = reflect_on_association(association)
@@ -20,25 +21,32 @@ module IdentityCache
20
21
 
21
22
  options[:embed] = false
22
23
  options[:cached_accessor_name] = "fetch_#{association}"
23
- options[:foreign_key] = association_reflection.foreign_key
24
- options[:association_class] = association_reflection.klass
24
+ options[:records_variable_name] = "cached_#{association}"
25
+ options[:association_reflection] = association_reflection
25
26
  options[:prepopulate_method_name] = "prepopulate_fetched_#{association}"
26
27
 
27
28
  build_normalized_belongs_to_cache(association, options)
28
29
  end
29
30
 
31
+ private
32
+
30
33
  def build_normalized_belongs_to_cache(association, options)
34
+ foreign_key = options[:association_reflection].foreign_key
31
35
  self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
32
36
  def #{options[:cached_accessor_name]}
33
- if IdentityCache.should_use_cache? && #{options[:foreign_key]}.present? && !association(:#{association}).loaded?
34
- self.#{association} = #{options[:association_class]}.fetch_by_id(#{options[:foreign_key]})
37
+ if IdentityCache.should_use_cache? && #{foreign_key}.present? && !association(:#{association}).loaded?
38
+ if instance_variable_defined?(:@#{options[:records_variable_name]})
39
+ @#{options[:records_variable_name]}
40
+ else
41
+ @#{options[:records_variable_name]} = association(:#{association}).klass.fetch_by_id(#{foreign_key})
42
+ end
35
43
  else
36
44
  #{association}
37
45
  end
38
46
  end
39
47
 
40
48
  def #{options[:prepopulate_method_name]}(record)
41
- self.#{association} = record
49
+ @#{options[:records_variable_name]} = record
42
50
  end
43
51
  CODE
44
52
  end
@@ -13,7 +13,7 @@ module IdentityCache
13
13
  klass.send(:all_cached_associations).sort.each do |name, options|
14
14
  case options[:embed]
15
15
  when true
16
- schema_string << ",#{name}:(#{denormalized_schema_hash(options[:association_class])})"
16
+ schema_string << ",#{name}:(#{denormalized_schema_hash(options[:association_reflection].klass)})"
17
17
  when :ids
18
18
  schema_string << ",#{name}:ids"
19
19
  end
@@ -35,12 +35,13 @@ module IdentityCache
35
35
  "#{rails_cache_key_namespace}blob:#{base_class.name}:#{rails_cache_key_prefix}:"
36
36
  end
37
37
 
38
- def rails_cache_index_key_for_fields_and_values(fields, values)
39
- "#{rails_cache_key_namespace}index:#{base_class.name}:#{rails_cache_string_for_fields_and_values(fields, values)}"
40
- end
41
-
42
- def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
43
- "#{rails_cache_key_namespace}attribute:#{base_class.name}:#{attribute}:#{rails_cache_string_for_fields_and_values(fields, values)}"
38
+ def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique)
39
+ unique_indicator = unique ? '' : 's'
40
+ "#{rails_cache_key_namespace}" \
41
+ "attr#{unique_indicator}" \
42
+ ":#{base_class.name}" \
43
+ ":#{attribute}" \
44
+ ":#{rails_cache_string_for_fields_and_values(fields, values)}"
44
45
  end
45
46
 
46
47
  def rails_cache_key_namespace
@@ -58,20 +59,12 @@ module IdentityCache
58
59
  self.class.rails_cache_key(id)
59
60
  end
60
61
 
61
- def secondary_cache_index_key_for_current_values(fields) # :nodoc:
62
- self.class.rails_cache_index_key_for_fields_and_values(fields, current_values_for_fields(fields))
63
- end
64
-
65
- def secondary_cache_index_key_for_previous_values(fields) # :nodoc:
66
- self.class.rails_cache_index_key_for_fields_and_values(fields, old_values_for_fields(fields))
67
- end
68
-
69
- def attribute_cache_key_for_attribute_and_current_values(attribute, fields) # :nodoc:
70
- self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, current_values_for_fields(fields))
62
+ def attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique) # :nodoc:
63
+ self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, current_values_for_fields(fields), unique)
71
64
  end
72
65
 
73
- def attribute_cache_key_for_attribute_and_previous_values(attribute, fields) # :nodoc:
74
- self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields))
66
+ def attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique) # :nodoc:
67
+ self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields), unique)
75
68
  end
76
69
 
77
70
  def current_values_for_fields(fields) # :nodoc:
@@ -3,15 +3,15 @@ module IdentityCache
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do |base|
6
+ base.class_attribute :cached_model
6
7
  base.class_attribute :cache_indexes
7
- base.class_attribute :cache_attributes
8
8
  base.class_attribute :cached_has_manys
9
9
  base.class_attribute :cached_has_ones
10
10
  base.class_attribute :primary_cache_index_enabled
11
11
 
12
+ base.cached_model = base
12
13
  base.cached_has_manys = {}
13
14
  base.cached_has_ones = {}
14
- base.cache_attributes = []
15
15
  base.cache_indexes = []
16
16
  base.primary_cache_index_enabled = true
17
17
  end
@@ -42,26 +42,29 @@ module IdentityCache
42
42
  def cache_index(*fields)
43
43
  raise NotImplementedError, "Cache indexes need an enabled primary index" unless primary_cache_index_enabled
44
44
  options = fields.extract_options!
45
- self.cache_indexes.push fields
45
+ unique = options[:unique] || false
46
+ cache_attribute_by_alias('primary_key', 'id', by: fields, unique: unique)
46
47
 
47
48
  field_list = fields.join("_and_")
48
49
  arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
49
50
 
50
- if options[:unique]
51
+ if unique
51
52
  self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
52
- def fetch_by_#{field_list}(#{arg_list})
53
- identity_cache_single_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}])
53
+ def fetch_by_#{field_list}(#{arg_list}, options={})
54
+ id = fetch_#{primary_key}_by_#{field_list}(#{arg_list})
55
+ id && fetch_by_id(id, options)
54
56
  end
55
57
 
56
58
  # exception throwing variant
57
- def fetch_by_#{field_list}!(#{arg_list})
58
- fetch_by_#{field_list}(#{arg_list}) or raise ActiveRecord::RecordNotFound
59
+ def fetch_by_#{field_list}!(#{arg_list}, options={})
60
+ fetch_by_#{field_list}(#{arg_list}, options) or raise ActiveRecord::RecordNotFound
59
61
  end
60
62
  CODE
61
63
  else
62
64
  self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
63
- def fetch_by_#{field_list}(#{arg_list})
64
- identity_cache_multiple_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}])
65
+ def fetch_by_#{field_list}(#{arg_list}, options={})
66
+ ids = fetch_#{primary_key}_by_#{field_list}(#{arg_list})
67
+ ids.empty? ? ids : fetch_multi(ids, options)
65
68
  end
66
69
  CODE
67
70
  end
@@ -78,9 +81,13 @@ module IdentityCache
78
81
  #
79
82
  # == Example:
80
83
  # class Product
81
- # cached_has_many :options, :embed => false
82
- # cached_has_many :orders
83
- # cached_has_many :buyers, :inverse_name => 'line_item'
84
+ # include IdentityCache
85
+ # has_many :options
86
+ # has_many :orders
87
+ # has_many :buyers
88
+ # cache_has_many :options, embed: :ids
89
+ # cache_has_many :orders
90
+ # cache_has_many :buyers, inverse_name: 'line_item'
84
91
  # end
85
92
  #
86
93
  # == Parameters
@@ -94,13 +101,11 @@ module IdentityCache
94
101
  # * inverse_name: The name of the parent in the association if the name is
95
102
  # not the lowercase pluralization of the parent object's class
96
103
  def cache_has_many(association, options = {})
104
+ ensure_base_model
97
105
  options = options.slice(:embed, :inverse_name)
98
106
  options[:embed] = :ids unless options.has_key?(:embed)
99
107
  deprecate_embed_option(options, false, :ids)
100
- options[:inverse_name] ||= self.name.underscore.to_sym
101
- unless self.reflect_on_association(association)
102
- raise AssociationError, "Association named '#{association}' was not found on #{self.class}"
103
- end
108
+ ensure_cacheable_association(association, options)
104
109
  self.cached_has_manys[association] = options
105
110
 
106
111
  case options[:embed]
@@ -119,8 +124,8 @@ module IdentityCache
119
124
  #
120
125
  # == Example:
121
126
  # class Product
122
- # cached_has_one :store, :embed => true
123
- # cached_has_one :vendor
127
+ # cache_has_one :store, embed: true
128
+ # cache_has_one :vendor
124
129
  # end
125
130
  #
126
131
  # == Parameters
@@ -135,12 +140,10 @@ module IdentityCache
135
140
  # necessary if the name is not the lowercase pluralization of the
136
141
  # parent object's class)
137
142
  def cache_has_one(association, options = {})
143
+ ensure_base_model
138
144
  options = options.slice(:embed, :inverse_name)
139
145
  options[:embed] = true unless options.has_key?(:embed)
140
- options[:inverse_name] ||= self.name.underscore.to_sym
141
- unless self.reflect_on_association(association)
142
- raise AssociationError, "Association named '#{association}' was not found on #{self.class}"
143
- end
146
+ ensure_cacheable_association(association, options)
144
147
  self.cached_has_ones[association] = options
145
148
 
146
149
  if options[:embed] == true
@@ -155,8 +158,9 @@ module IdentityCache
155
158
  #
156
159
  # == Example:
157
160
  # class Product
158
- # cache_attribute :quantity, :by => :name
159
- # cache_attribute :quantity :by => [:name, :vendor]
161
+ # include IdentityCache
162
+ # cache_attribute :quantity, by: :name
163
+ # cache_attribute :quantity, by: [:name, :vendor]
160
164
  # end
161
165
  #
162
166
  # == Parameters
@@ -165,52 +169,41 @@ module IdentityCache
165
169
  # == Options
166
170
  #
167
171
  # * by: Other attribute or attributes in the model to keep values indexed. Default is :id
172
+ # * unique: if the index would only have unique values. Default is true
168
173
  def cache_attribute(attribute, options = {})
169
- options[:by] ||= :id
170
- fields = Array(options[:by])
171
-
172
- self.cache_attributes.push [attribute, fields]
173
-
174
- field_list = fields.join("_and_")
175
- arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
176
-
177
- self.instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
178
- def fetch_#{attribute}_by_#{field_list}(#{arg_list})
179
- attribute_dynamic_fetcher(#{attribute.inspect}, #{fields.inspect}, [#{arg_list}])
180
- end
181
- CODE
174
+ cache_attribute_by_alias(attribute.inspect, attribute, options)
182
175
  end
183
176
 
184
177
  def disable_primary_cache_index
185
- raise NotImplementedError, "Secondary indexes rely on the primary index to function. You must either remove the secondary indexes or don't disable the primary" if self.cache_indexes.size > 0
178
+ ensure_base_model
186
179
  self.primary_cache_index_enabled = false
187
180
  end
188
181
 
189
182
  private
190
183
 
191
- def identity_cache_single_value_dynamic_fetcher(fields, values) # :nodoc:
192
- cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
193
- id = IdentityCache.fetch(cache_key) { identity_cache_conditions(fields, values).limit(1).pluck(primary_key).first }
194
- unless id.nil?
195
- record = fetch_by_id(id)
196
- IdentityCache.cache.delete(cache_key) unless record
197
- end
184
+ def cache_attribute_by_alias(attribute, alias_name, options)
185
+ ensure_base_model
186
+ options[:by] ||= :id
187
+ alias_name = alias_name.to_sym
188
+ unique = options[:unique].nil? ? true : !!options[:unique]
189
+ fields = Array(options[:by])
198
190
 
199
- record
200
- end
191
+ self.cache_indexes.push [alias_name, fields, unique]
201
192
 
202
- def identity_cache_multiple_value_dynamic_fetcher(fields, values) # :nodoc
203
- cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
204
- ids = IdentityCache.fetch(cache_key) { identity_cache_conditions(fields, values).pluck(primary_key) }
193
+ field_list = fields.join("_and_")
194
+ arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
205
195
 
206
- ids.empty? ? [] : fetch_multi(ids)
196
+ self.instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
197
+ def fetch_#{alias_name}_by_#{field_list}(#{arg_list})
198
+ attribute_dynamic_fetcher(#{attribute}, #{fields.inspect}, [#{arg_list}], #{unique})
199
+ end
200
+ CODE
207
201
  end
208
202
 
209
203
  def build_recursive_association_cache(association, options) #:nodoc:
210
- options[:association_class] = reflect_on_association(association).klass
204
+ options[:association_reflection] = reflect_on_association(association)
211
205
  options[:cached_accessor_name] = "fetch_#{association}"
212
206
  options[:records_variable_name] = "cached_#{association}"
213
- options[:population_method_name] = "populate_#{association}_cache"
214
207
 
215
208
 
216
209
  unless instance_methods.include?(options[:cached_accessor_name].to_sym)
@@ -218,10 +211,6 @@ module IdentityCache
218
211
  def #{options[:cached_accessor_name]}
219
212
  fetch_recursively_cached_association('#{options[:records_variable_name]}', :#{association})
220
213
  end
221
-
222
- def #{options[:population_method_name]}
223
- populate_recursively_cached_association('#{options[:records_variable_name]}', :#{association})
224
- end
225
214
  CODE
226
215
 
227
216
  options[:only_on_foreign_key_change] = false
@@ -231,32 +220,24 @@ module IdentityCache
231
220
 
232
221
  def build_id_embedded_has_many_cache(association, options) #:nodoc:
233
222
  singular_association = association.to_s.singularize
234
- options[:association_class] = reflect_on_association(association).klass
223
+ options[:association_reflection] = reflect_on_association(association)
235
224
  options[:cached_accessor_name] = "fetch_#{association}"
236
225
  options[:ids_name] = "#{singular_association}_ids"
237
226
  options[:cached_ids_name] = "fetch_#{options[:ids_name]}"
238
227
  options[:ids_variable_name] = "cached_#{options[:ids_name]}"
239
228
  options[:records_variable_name] = "cached_#{association}"
240
- options[:population_method_name] = "populate_#{association}_cache"
241
229
  options[:prepopulate_method_name] = "prepopulate_fetched_#{association}"
242
230
 
243
231
  self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
244
232
  attr_reader :#{options[:ids_variable_name]}
245
233
 
246
234
  def #{options[:cached_ids_name]}
247
- #{options[:population_method_name]} unless @#{options[:ids_variable_name]}
248
- @#{options[:ids_variable_name]}
249
- end
250
-
251
- def #{options[:population_method_name]}
252
- @#{options[:ids_variable_name]} = #{options[:ids_name]}
253
- association_cache.delete(:#{association})
235
+ @#{options[:ids_variable_name]} ||= #{options[:ids_name]}
254
236
  end
255
237
 
256
238
  def #{options[:cached_accessor_name]}
257
239
  if IdentityCache.should_use_cache? || #{association}.loaded?
258
- #{options[:population_method_name]} unless @#{options[:ids_variable_name]} || @#{options[:records_variable_name]}
259
- @#{options[:records_variable_name]} ||= #{options[:association_class]}.fetch_multi(@#{options[:ids_variable_name]})
240
+ @#{options[:records_variable_name]} ||= #{options[:association_reflection].klass}.fetch_multi(#{options[:cached_ids_name]})
260
241
  else
261
242
  #{association}
262
243
  end
@@ -271,30 +252,26 @@ module IdentityCache
271
252
  add_parent_expiry_hook(options)
272
253
  end
273
254
 
274
- def attribute_dynamic_fetcher(attribute, fields, values) #:nodoc:
275
- cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
276
- IdentityCache.fetch(cache_key) { identity_cache_conditions(fields, values).limit(1).pluck(attribute).first }
255
+ def attribute_dynamic_fetcher(attribute, fields, values, unique_index) #:nodoc:
256
+ raise_if_scoped
257
+ cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique_index)
258
+ IdentityCache.fetch(cache_key) do
259
+ query = reorder(nil).where(Hash[fields.zip(values)])
260
+ query = query.limit(1) if unique_index
261
+ results = query.pluck(attribute)
262
+ unique_index ? results.first : results
263
+ end
277
264
  end
278
265
 
279
266
  def add_parent_expiry_hook(options)
280
- child_class = options[:association_class]
281
- raise InverseAssociationError unless child_class.reflect_on_association(options[:inverse_name])
267
+ child_class = options[:association_reflection].klass
282
268
 
283
269
  child_class.send(:include, ArTransactionChanges) unless child_class.include?(ArTransactionChanges)
284
270
  child_class.send(:include, ParentModelExpiration) unless child_class.include?(ParentModelExpiration)
285
271
 
286
272
  child_class.parent_expiration_entries[options[:inverse_name]] << [self, options[:only_on_foreign_key_change]]
287
273
 
288
- child_class.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
289
- after_commit :expire_parent_caches
290
- if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("4.0.4")
291
- after_touch :expire_parent_caches
292
- end
293
- CODE
294
- end
295
-
296
- def identity_cache_conditions(fields, values)
297
- reorder(nil).where(Hash[fields.zip(values)])
274
+ child_class.after_commit :expire_parent_caches
298
275
  end
299
276
 
300
277
  def deprecate_embed_option(options, old_value, new_value)
@@ -303,6 +280,28 @@ module IdentityCache
303
280
  ActiveSupport::Deprecation.warn("`embed: #{old_value.inspect}` was renamed to `embed: #{new_value.inspect}` for clarity", caller(2))
304
281
  end
305
282
  end
283
+
284
+ def ensure_base_model
285
+ if self != cached_model
286
+ raise DerivedModelError, "IdentityCache class methods must be called on the same model that includes IdentityCache"
287
+ end
288
+ end
289
+
290
+ def ensure_cacheable_association(association, options)
291
+ unless association_reflection = self.reflect_on_association(association)
292
+ raise AssociationError, "Association named '#{association}' was not found on #{self.class}"
293
+ end
294
+ if association_reflection.options[:through]
295
+ raise UnsupportedAssociationError, "caching through associations isn't supported"
296
+ end
297
+ options[:inverse_name] ||= association_reflection.inverse_of.name if association_reflection.inverse_of
298
+ options[:inverse_name] ||= self.name.underscore.to_sym
299
+ child_class = association_reflection.klass
300
+ raise InverseAssociationError unless child_class.reflect_on_association(options[:inverse_name])
301
+ unless options[:embed] == true || child_class.include?(IdentityCache)
302
+ raise UnsupportedAssociationError, "associated class #{child_class} must include IdentityCache to be cached without full embedding"
303
+ end
304
+ end
306
305
  end
307
306
  end
308
307
  end