identity_cache 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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