identity_cache 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +3 -2
  4. data/CHANGELOG +13 -1
  5. data/Gemfile +2 -2
  6. data/{Gemfile.rails4 → Gemfile.rails41} +2 -2
  7. data/Gemfile32 +7 -0
  8. data/README.md +3 -4
  9. data/identity_cache.gemspec +3 -2
  10. data/lib/identity_cache/belongs_to_caching.rb +1 -1
  11. data/lib/identity_cache/cache_hash.rb +2 -2
  12. data/lib/identity_cache/cache_invalidation.rb +26 -0
  13. data/lib/identity_cache/cache_key_generation.rb +3 -2
  14. data/lib/identity_cache/configuration_dsl.rb +38 -32
  15. data/lib/identity_cache/query_api.rb +52 -52
  16. data/lib/identity_cache/version.rb +1 -1
  17. data/lib/identity_cache.rb +20 -8
  18. data/performance/cache_runner.rb +7 -4
  19. data/performance/profile.rb +13 -3
  20. data/test/attribute_cache_test.rb +13 -4
  21. data/test/cache_fetch_includes_test.rb +1 -44
  22. data/test/cache_invalidation_test.rb +52 -0
  23. data/test/denormalized_has_many_test.rb +4 -4
  24. data/test/fetch_multi_test.rb +54 -0
  25. data/test/fetch_multi_with_batched_associations_test.rb +13 -35
  26. data/test/fetch_test.rb +20 -6
  27. data/test/fixtures/serialized_record +0 -0
  28. data/test/helpers/active_record_objects.rb +8 -2
  29. data/test/helpers/database_connection.rb +8 -5
  30. data/test/helpers/serialization_format.rb +9 -5
  31. data/test/helpers/update_serialization_format.rb +1 -0
  32. data/test/index_cache_test.rb +21 -1
  33. data/test/memoized_cache_proxy_test.rb +9 -17
  34. data/test/normalized_belongs_to_test.rb +2 -0
  35. data/test/normalized_has_many_test.rb +4 -4
  36. data/test/recursive_denormalized_has_many_test.rb +1 -1
  37. data/test/schema_change_test.rb +2 -2
  38. data/test/test_helper.rb +7 -3
  39. metadata +26 -17
  40. data/test/helpers/cache.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4486319de757bbba037be1e7687cb18a2fd72dc8
4
- data.tar.gz: 8869e35fceeca476227e9b8181a573cfe38f1d96
3
+ metadata.gz: 477c35f8ea40ea8d71d6937d63ab1842a7321871
4
+ data.tar.gz: 2729aa2c8c4c06dcf42b8be9f167b88aa79cc81b
5
5
  SHA512:
6
- metadata.gz: 1a7927f13588a480924f558473f536a94790a165820c4ff606e69456a5374394ae66c1b6eb6e40d39bd76490b0537a1b3bc164855f0174b122cbd1109376580e
7
- data.tar.gz: 90f5bec88b0edd0e6c915804533eebb2d5f020dcf99ecc03b262eeaec9313d64f6c30aa9420384d8652014642198083d0bf17a8bd507872705c7838e2df827e8
6
+ metadata.gz: efaef756ca74582dfb7b8669933720f482d95da1493a2f2173e88c8f6cdc79861225858ebced572386e499140f9eb41ecf20a519d4db7c8363e7693141816d81
7
+ data.tar.gz: e11dff95ae3b78d4d57fd305eb961c4bd21e40e34a7fd08df8cf529369be841a0a75ededa64a47943e7328d10c045cb698744b070d4da2d5301ea0cdb652f61d
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.1.0
1
+ 2.1.1
data/.travis.yml CHANGED
@@ -2,11 +2,12 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
- - 2.1.0
5
+ - 2.1.1
6
6
  - jruby
7
7
  gemfile:
8
+ - Gemfile32
8
9
  - Gemfile
9
- - Gemfile.rails4
10
+ - Gemfile.rails41
10
11
  services:
11
12
  - memcache
12
13
  - mysql
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ Unreleased
2
+ * Consistently use ActiveRecord / Arel APIs to build SQL queries
3
+ [related #148]
4
+
5
+ * Stop expiring cache on after_touch callback. [backwards incompatible]
6
+ * fetch_multi accepts an array of keys as argument
7
+ * Fix: SystemStackError when fetching more records than the max stack size
8
+ * Fix: Bug in fetch_multi in a transaction where results weren't compacted.
9
+ * Fix: Avoid unused preload on fetch_multi with :includes option for cache miss
10
+ * Change :embed option value from false to :ids for cache_has_many for clarity
11
+ * Fix: reload will invalidate the local instance cache
12
+
1
13
  0.0.7
2
14
  * Fix: Not implemented error for cache_has_one with embed: false
3
15
  * Fix: cache key to change when adding a cache_has_many association with :embed => false
@@ -7,7 +19,7 @@
7
19
  0.0.6
8
20
  * Fix: bug where previously nil-cached attribute caches weren't expired on record creation
9
21
  * Fix: cache key to not change when adding a non-embedded association.
10
- * Perf: Rails 4 Only create CollectionProxy when using it
22
+ * Perf: Rails 4 Only create CollectionProxy when using it
11
23
 
12
24
  0.0.5
13
25
 
data/Gemfile CHANGED
@@ -3,5 +3,5 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in identity_cache.gemspec
4
4
  gemspec
5
5
 
6
- gem 'activerecord', '~> 3.2.16'
7
- gem 'activesupport', '~> 3.2.16'
6
+ gem 'activerecord', '~> 4.0.4'
7
+ gem 'activesupport', '~> 4.0.4'
@@ -3,5 +3,5 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in identity_cache.gemspec
4
4
  gemspec
5
5
 
6
- gem 'activerecord', '~> 4.0.1'
7
- gem 'activesupport', '~> 4.0.1'
6
+ gem 'activerecord', '~> 4.1.0'
7
+ gem 'activesupport', '~> 4.1.0'
data/Gemfile32 ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in identity_cache.gemspec
4
+ gemspec
5
+
6
+ gem 'activerecord', '~> 3.2.16'
7
+ gem 'activesupport', '~> 3.2.16'
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # IdentityCache
2
- [![Build Status](https://api.travis-ci.org/Shopify/identity_cache.png?branch=master)](http://travis-ci.org/Shopify/identity_cache)
2
+ [![Build Status](https://travis-ci.org/Shopify/identity_cache.svg?branch=master)](https://travis-ci.org/Shopify/identity_cache)
3
3
 
4
4
  Opt in read through ActiveRecord caching used in production and extracted from Shopify. IdentityCache lets you specify how you want to cache your model objects, at the model level, and adds a number of convenience methods for accessing those objects through the cache. Memcached is used as the backend cache store, and the database is only hit when a copy of the object cannot be found in Memcached.
5
5
 
@@ -17,7 +17,6 @@ gem 'cityhash' # optional, for faster hashing (C-Ruby only)
17
17
  And then execute:
18
18
 
19
19
  $ bundle
20
-
21
20
 
22
21
 
23
22
  Add the following to your environment/production.rb:
@@ -167,7 +166,7 @@ Example:
167
166
  #### cache_has_many
168
167
 
169
168
  Options:
170
- _[:embed]_ Specifies that the association should be included with the parent when caching. This means the associated objects will be loaded already when the parent is loaded from the cache and will not need to be fetched on their own.
169
+ _[:embed]_ When true, specifies that the association should be included with the parent when caching. This means the associated objects will be loaded already when the parent is loaded from the cache and will not need to be fetched on their own. When :ids, only the id of the associated records will be included with the parent when caching.
171
170
 
172
171
  _[:inverse_name]_ Specifies the name of parent object used by the association. This is useful for polymorphic associations when the association is often named something different between the parent and child objects.
173
172
 
@@ -177,7 +176,7 @@ Example:
177
176
  #### cache_has_one
178
177
 
179
178
  Options:
180
- _[:embed]_ Specifies that the association should be included with the parent when caching. This means the associated objects will be loaded already when the parent is loaded from the cache and will not need to be fetched on their own.
179
+ _[:embed]_ When true, specifies that the association should be included with the parent when caching. This means the associated objects will be loaded already when the parent is loaded from the cache and will not need to be fetched on their own. No other values are currently implemented.
181
180
 
182
181
  _[:inverse_name]_ Specifies the name of parent object used by the association. This is useful for polymorphic associations when the association is often named something different between the parent and child objects.
183
182
 
@@ -15,12 +15,13 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = IdentityCache::VERSION
17
17
 
18
- gem.add_dependency('ar_transaction_changes', '= 0.0.3')
19
- gem.add_dependency('activerecord', '>= 3.2', '< 4.1')
18
+ gem.add_dependency('ar_transaction_changes', '~> 1.0')
19
+ gem.add_dependency('activerecord', '>= 3.2')
20
20
 
21
21
  gem.add_development_dependency('memcached_store', '~> 0.11.2')
22
22
  gem.add_development_dependency('rake')
23
23
  gem.add_development_dependency('mocha', '0.14.0')
24
+ gem.add_development_dependency('spy')
24
25
 
25
26
  if RUBY_PLATFORM == 'java'
26
27
  gem.add_development_dependency 'jruby-openssl'
@@ -24,7 +24,7 @@ module IdentityCache
24
24
  end
25
25
 
26
26
  def build_normalized_belongs_to_cache(association, options)
27
- self.class_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
27
+ self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
28
28
  def #{options[:cached_accessor_name]}
29
29
  if IdentityCache.should_cache? && #{options[:foreign_key]}.present? && !association(:#{association}).loaded?
30
30
  self.#{association} = #{options[:association_class]}.fetch_by_id(#{options[:foreign_key]})
@@ -21,12 +21,12 @@ module IdentityCache
21
21
  module CacheHash
22
22
 
23
23
  if defined?(CityHash)
24
-
24
+
25
25
  def memcache_hash(key) #:nodoc:
26
26
  CityHash.hash64(key)
27
27
  end
28
28
  else
29
-
29
+
30
30
  def memcache_hash(key) #:nodoc:
31
31
  a = Digest::MD5.digest(key).unpack('LL')
32
32
  (a[0] << 32) | a[1]
@@ -0,0 +1,26 @@
1
+ module IdentityCache
2
+ module CacheInvalidation
3
+
4
+ CACHE_KEY_NAMES = [:ids_variable_name, :records_variable_name]
5
+
6
+ def reload(*)
7
+ clear_cached_associations
8
+ super
9
+ end
10
+
11
+ private
12
+
13
+ def clear_cached_associations
14
+ self.class.send(:all_cached_associations).each do |_, data|
15
+ CACHE_KEY_NAMES.each do |key|
16
+ if data[key]
17
+ instance_variable_name = "@#{data[key]}"
18
+ if instance_variable_defined?(instance_variable_name)
19
+ remove_instance_variable(instance_variable_name)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -11,9 +11,10 @@ module IdentityCache
11
11
  schema_string = schema_to_string(klass.columns)
12
12
  if klass.include?(IdentityCache)
13
13
  klass.send(:all_cached_associations).sort.each do |name, options|
14
- if options[:embed]
14
+ case options[:embed]
15
+ when true
15
16
  schema_string << ",#{name}:(#{denormalized_schema_hash(options[:association_class])})"
16
- elsif options[:cached_ids_name]
17
+ when :ids
17
18
  schema_string << ",#{name}:ids"
18
19
  end
19
20
  end
@@ -88,34 +88,35 @@ module IdentityCache
88
88
  #
89
89
  # == Options
90
90
  #
91
- # * embed: If set will cause IdentityCache to keep the values for this
92
- # association in the same cache entry as the parent, instead of its own.
91
+ # * embed: If set to true, will cause IdentityCache to keep the
92
+ # values for this association in the same cache entry as the parent,
93
+ # instead of its own.
93
94
  # * inverse_name: The name of the parent in the association if the name is
94
95
  # not the lowercase pluralization of the parent object's class
95
96
  def cache_has_many(association, options = {})
96
- options[:embed] ||= false
97
+ options[:embed] = :ids unless options.has_key?(:embed)
98
+ deprecate_embed_option(options, false, :ids)
97
99
  options[:inverse_name] ||= self.name.underscore.to_sym
98
100
  raise InverseAssociationError unless self.reflect_on_association(association)
99
101
  self.cached_has_manys[association] = options
100
102
 
101
- if options[:embed]
102
- build_denormalized_association_cache(association, options)
103
+ case options[:embed]
104
+ when true
105
+ build_recursive_association_cache(association, options)
106
+ when :ids
107
+ build_id_embedded_has_many_cache(association, options)
103
108
  else
104
- build_normalized_has_many_cache(association, options)
109
+ raise NotImplementedError
105
110
  end
106
111
  end
107
112
 
108
113
  # Will cache an association to the class including IdentityCache.
109
- # The embed option if set will make IdentityCache keep the association
110
- # values in the same cache entry as the parent.
111
- #
112
- # Embedded associations are more effective in offloading database work,
113
- # however they will increase the size of the cache entries and make the
114
- # whole entry expire with the change of any of the embedded members
114
+ # IdentityCache will keep the association values in the same cache entry
115
+ # as the parent.
115
116
  #
116
117
  # == Example:
117
118
  # class Product
118
- # cached_has_one :store, :embed => false
119
+ # cached_has_one :store, :embed => true
119
120
  # cached_has_one :vendor
120
121
  # end
121
122
  #
@@ -124,8 +125,9 @@ module IdentityCache
124
125
  #
125
126
  # == Options
126
127
  #
127
- # * embed: If set will cause IdentityCache to keep the values for this
128
- # association in the same cache entry as the parent, instead of its own.
128
+ # * embed: Only true is supported, which is also the default, so
129
+ # IdentityCache will keep the values for this association in the same
130
+ # cache entry as the parent, instead of its own.
129
131
  # * inverse_name: The name of the parent in the association ( only
130
132
  # necessary if the name is not the lowercase pluralization of the
131
133
  # parent object's class)
@@ -135,8 +137,8 @@ module IdentityCache
135
137
  raise InverseAssociationError unless self.reflect_on_association(association)
136
138
  self.cached_has_ones[association] = options
137
139
 
138
- if options[:embed]
139
- build_denormalized_association_cache(association, options)
140
+ if options[:embed] == true
141
+ build_recursive_association_cache(association, options)
140
142
  else
141
143
  raise NotImplementedError
142
144
  end
@@ -181,9 +183,8 @@ module IdentityCache
181
183
  private
182
184
 
183
185
  def identity_cache_single_value_dynamic_fetcher(fields, values) # :nodoc:
184
- sql_on_miss = "SELECT #{quoted_primary_key} FROM #{quoted_table_name} WHERE #{identity_cache_sql_conditions(fields, values)} LIMIT 1"
185
186
  cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
186
- id = IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
187
+ id = IdentityCache.fetch(cache_key) { identity_cache_conditions(fields, values).limit(1).pluck(primary_key).first }
187
188
  unless id.nil?
188
189
  record = fetch_by_id(id)
189
190
  IdentityCache.cache.delete(cache_key) unless record
@@ -193,28 +194,27 @@ module IdentityCache
193
194
  end
194
195
 
195
196
  def identity_cache_multiple_value_dynamic_fetcher(fields, values) # :nodoc
196
- sql_on_miss = "SELECT #{quoted_primary_key} FROM #{quoted_table_name} WHERE #{identity_cache_sql_conditions(fields, values)}"
197
197
  cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
198
- ids = IdentityCache.fetch(cache_key) { connection.select_values(sql_on_miss) }
198
+ ids = IdentityCache.fetch(cache_key) { identity_cache_conditions(fields, values).pluck(primary_key) }
199
199
 
200
- ids.empty? ? [] : fetch_multi(*ids)
200
+ ids.empty? ? [] : fetch_multi(ids)
201
201
  end
202
202
 
203
- def build_denormalized_association_cache(association, options) #:nodoc:
203
+ def build_recursive_association_cache(association, options) #:nodoc:
204
204
  options[:association_class] ||= reflect_on_association(association).klass
205
205
  options[:cached_accessor_name] ||= "fetch_#{association}"
206
- options[:records_variable_name] ||= "cached_#{association}"
206
+ options[:records_variable_name] ||= "cached_#{association}"
207
207
  options[:population_method_name] ||= "populate_#{association}_cache"
208
208
 
209
209
 
210
210
  unless instance_methods.include?(options[:cached_accessor_name].to_sym)
211
211
  self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
212
212
  def #{options[:cached_accessor_name]}
213
- fetch_denormalized_cached_association('#{options[:records_variable_name]}', :#{association})
213
+ fetch_recursively_cached_association('#{options[:records_variable_name]}', :#{association})
214
214
  end
215
215
 
216
216
  def #{options[:population_method_name]}
217
- populate_denormalized_cached_association('#{options[:records_variable_name]}', :#{association})
217
+ populate_recursively_cached_association('#{options[:records_variable_name]}', :#{association})
218
218
  end
219
219
  CODE
220
220
 
@@ -222,7 +222,7 @@ module IdentityCache
222
222
  end
223
223
  end
224
224
 
225
- def build_normalized_has_many_cache(association, options) #:nodoc:
225
+ def build_id_embedded_has_many_cache(association, options) #:nodoc:
226
226
  singular_association = association.to_s.singularize
227
227
  options[:association_class] ||= reflect_on_association(association).klass
228
228
  options[:cached_accessor_name] ||= "fetch_#{association}"
@@ -249,7 +249,7 @@ module IdentityCache
249
249
  def #{options[:cached_accessor_name]}
250
250
  if IdentityCache.should_cache? || #{association}.loaded?
251
251
  #{options[:population_method_name]} unless @#{options[:ids_variable_name]} || @#{options[:records_variable_name]}
252
- @#{options[:records_variable_name]} ||= #{options[:association_class]}.fetch_multi(*@#{options[:ids_variable_name]})
252
+ @#{options[:records_variable_name]} ||= #{options[:association_class]}.fetch_multi(@#{options[:ids_variable_name]})
253
253
  else
254
254
  #{association}
255
255
  end
@@ -265,8 +265,7 @@ module IdentityCache
265
265
 
266
266
  def attribute_dynamic_fetcher(attribute, fields, values) #:nodoc:
267
267
  cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
268
- sql_on_miss = "SELECT #{connection.quote_column_name(attribute)} FROM #{quoted_table_name} WHERE #{identity_cache_sql_conditions(fields, values)} LIMIT 1"
269
- IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
268
+ IdentityCache.fetch(cache_key) { identity_cache_conditions(fields, values).limit(1).pluck(attribute).first }
270
269
  end
271
270
 
272
271
  def add_parent_expiry_hook(options)
@@ -289,8 +288,15 @@ module IdentityCache
289
288
  CODE
290
289
  end
291
290
 
292
- def identity_cache_sql_conditions(fields, values)
293
- fields.each_with_index.collect { |f, i| "#{connection.quote_column_name(f)} = #{quote_value(values[i],nil)}" }.join(" AND ")
291
+ def identity_cache_conditions(fields, values)
292
+ reorder(nil).where(Hash[fields.zip(values)])
293
+ end
294
+
295
+ def deprecate_embed_option(options, old_value, new_value)
296
+ if options[:embed] == old_value
297
+ options[:embed] = new_value
298
+ ActiveSupport::Deprecation.warn("`embed: #{old_value.inspect}` was renamed to `embed: #{new_value.inspect}` for clarity", caller(2))
299
+ end
294
300
  end
295
301
  end
296
302
  end
@@ -4,7 +4,9 @@ module IdentityCache
4
4
 
5
5
  included do |base|
6
6
  base.after_commit :expire_cache
7
- base.after_touch :expire_cache
7
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("4.0.4")
8
+ base.after_touch :expire_cache
9
+ end
8
10
  end
9
11
 
10
12
  module ClassMethods
@@ -31,7 +33,7 @@ module IdentityCache
31
33
  end
32
34
 
33
35
  else
34
- self.where(id: id).first
36
+ self.reorder(nil).where(primary_key => id).first
35
37
  end
36
38
  end
37
39
 
@@ -47,28 +49,30 @@ module IdentityCache
47
49
  def fetch_multi(*ids)
48
50
  raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
49
51
  options = ids.extract_options!
50
- if IdentityCache.should_cache?
51
-
52
+ ids.flatten!(1)
53
+ records = if IdentityCache.should_cache?
52
54
  require_if_necessary do
53
55
  cache_keys = ids.map {|id| rails_cache_key(id) }
54
56
  key_to_id_map = Hash[ cache_keys.zip(ids) ]
57
+ key_to_record_map = {}
55
58
 
56
- coders_by_key = IdentityCache.fetch_multi(*cache_keys) do |unresolved_keys|
59
+ coders_by_key = IdentityCache.fetch_multi(cache_keys) do |unresolved_keys|
57
60
  ids = unresolved_keys.map {|key| key_to_id_map[key] }
58
- records = find_batch(ids, options)
59
- records.compact.each{ |record| record.send(:populate_association_caches) }
61
+ records = find_batch(ids)
62
+ found_records = records.compact
63
+ found_records.each{ |record| record.send(:populate_association_caches) }
64
+ key_to_record_map = found_records.index_by{ |record| rails_cache_key(record.id) }
60
65
  records.map {|record| coder_from_record(record) }
61
66
  end
62
67
 
63
- records = cache_keys.map {|key| record_from_coder(coders_by_key[key]) }.compact
64
- prefetch_associations(options[:includes], records) if options[:includes]
65
-
66
- records
68
+ cache_keys.map{ |key| key_to_record_map[key] || record_from_coder(coders_by_key[key]) }
67
69
  end
68
-
69
70
  else
70
- find_batch(ids, options)
71
+ find_batch(ids)
71
72
  end
73
+ records.compact!
74
+ prefetch_associations(options[:includes], records) if options[:includes]
75
+ records
72
76
  end
73
77
 
74
78
  private
@@ -114,7 +118,7 @@ module IdentityCache
114
118
  else
115
119
  record_from_coder(coder_or_array)
116
120
  end
117
- variable_name = record.class.send(:all_embedded_associations)[association_name][:records_variable_name]
121
+ variable_name = record.class.send(:recursively_embedded_associations)[association_name][:records_variable_name]
118
122
  record.instance_variable_set(:"@#{variable_name}", IdentityCache.map_cached_nil_for(value))
119
123
  end
120
124
 
@@ -139,14 +143,17 @@ module IdentityCache
139
143
  end
140
144
 
141
145
  def add_cached_associations_to_coder(record, coder)
142
- if record.class.respond_to?(:all_embedded_associations, true) && record.class.send(:all_embedded_associations).present?
143
- coder[:associations] = record.class.send(:all_embedded_associations).each_with_object({}) do |(name, options), hash|
144
- hash[name] = IdentityCache.map_cached_nil_for(get_embedded_association(record, name, options))
146
+ klass = record.class
147
+ if klass.include?(IdentityCache)
148
+ if (recursively_embedded_associations = klass.send(:recursively_embedded_associations)).present?
149
+ coder[:associations] = recursively_embedded_associations.each_with_object({}) do |(name, options), hash|
150
+ hash[name] = IdentityCache.map_cached_nil_for(get_embedded_association(record, name, options))
151
+ end
145
152
  end
146
- end
147
- if record.class.respond_to?(:cached_has_manys) && record.class.cached_has_manys.present?
148
- coder[:normalized_has_many] = record.class.cached_has_manys.each_with_object({}) do |(name, options), hash|
149
- hash[name] = record.instance_variable_get(:"@#{options[:ids_variable_name]}") unless options[:embed]
153
+ if (cached_has_manys = klass.cached_has_manys).present?
154
+ coder[:normalized_has_many] = cached_has_manys.each_with_object({}) do |(name, options), hash|
155
+ hash[name] = record.instance_variable_get(:"@#{options[:ids_variable_name]}") unless options[:embed] == true
156
+ end
150
157
  end
151
158
  end
152
159
  end
@@ -170,14 +177,14 @@ module IdentityCache
170
177
  end
171
178
 
172
179
  def resolve_cache_miss(id)
173
- object = self.includes(cache_fetch_includes).where(id: id).try(:first)
180
+ object = self.includes(cache_fetch_includes).reorder(nil).where(primary_key => id).try(:first)
174
181
  object.send(:populate_association_caches) if object
175
182
  object
176
183
  end
177
184
 
178
- def all_embedded_associations
185
+ def recursively_embedded_associations
179
186
  all_cached_associations.select do |cached_association, options|
180
- options[:embed].present?
187
+ options[:embed] == true
181
188
  end
182
189
  end
183
190
 
@@ -185,23 +192,19 @@ module IdentityCache
185
192
  (cached_has_manys || {}).merge(cached_has_ones || {}).merge(cached_belongs_tos || {})
186
193
  end
187
194
 
188
- def all_cached_associations_needing_population
195
+ def embedded_associations
189
196
  all_cached_associations.select do |cached_association, options|
190
- options[:population_method_name].present? # non-embedded belongs_to associations don't need population
197
+ options[:embed]
191
198
  end
192
199
  end
193
200
 
194
- def cache_fetch_includes(additions = {})
195
- additions = hashify_includes_structure(additions)
196
- embedded_associations = all_cached_associations.select { |name, options| options[:embed] }
197
-
198
- associations_for_identity_cache = embedded_associations.map do |child_association, options|
201
+ def cache_fetch_includes
202
+ associations_for_identity_cache = recursively_embedded_associations.map do |child_association, options|
199
203
  child_class = reflect_on_association(child_association).try(:klass)
200
204
 
201
- child_includes = additions.delete(child_association)
202
-
205
+ child_includes = nil
203
206
  if child_class.respond_to?(:cache_fetch_includes, true)
204
- child_includes = child_class.send(:cache_fetch_includes, child_includes)
207
+ child_includes = child_class.send(:cache_fetch_includes)
205
208
  end
206
209
 
207
210
  if child_includes.blank?
@@ -211,19 +214,15 @@ module IdentityCache
211
214
  end
212
215
  end
213
216
 
214
- associations_for_identity_cache.push(additions) if additions.keys.size > 0
215
217
  associations_for_identity_cache.compact
216
218
  end
217
219
 
218
- def find_batch(ids, options = {})
219
- @id_column ||= columns.detect {|c| c.name == "id"}
220
+ def find_batch(ids)
221
+ @id_column ||= columns.detect {|c| c.name == primary_key}
220
222
  ids = ids.map{ |id| @id_column.type_cast(id) }
221
- records = where('id IN (?)', ids).includes(cache_fetch_includes(options[:includes])).to_a
223
+ records = where(primary_key => ids).includes(cache_fetch_includes).to_a
222
224
  records_by_id = records.index_by(&:id)
223
- records = ids.map{ |id| records_by_id[id] }
224
- mismatching_ids = records.compact.map(&:id) - ids
225
- IdentityCache.logger.error "[IDC id mismatch] fetch_batch_requested=#{ids.inspect} fetch_batch_got=#{mismatchig_ids.inspect} mismatching ids " unless mismatching_ids.empty?
226
- records
225
+ ids.map{ |id| records_by_id[id] }
227
226
  end
228
227
 
229
228
  def prefetch_associations(associations, records)
@@ -233,7 +232,7 @@ module IdentityCache
233
232
  case
234
233
  when details = cached_has_manys[association]
235
234
 
236
- if details[:embed]
235
+ if details[:embed] == true
237
236
  child_records = records.map(&details[:cached_accessor_name].to_sym).flatten
238
237
  else
239
238
  ids_to_parent_record = records.each_with_object({}) do |record, hash|
@@ -258,14 +257,14 @@ module IdentityCache
258
257
  next_level_records = child_records
259
258
 
260
259
  when details = cached_belongs_tos[association]
261
- if details[:embed]
260
+ if details[:embed] == true
262
261
  raise ArgumentError.new("Embedded belongs_to associations do not support prefetching yet.")
263
262
  else
264
263
  ids_to_child_record = records.each_with_object({}) do |child_record, hash|
265
264
  parent_id = child_record.send(details[:foreign_key])
266
265
  hash[parent_id] = child_record if parent_id.present?
267
266
  end
268
- parent_records = details[:association_class].fetch_multi(*ids_to_child_record.keys)
267
+ parent_records = details[:association_class].fetch_multi(ids_to_child_record.keys)
269
268
  parent_records.each do |parent_record|
270
269
  child_record = ids_to_child_record[parent_record.id]
271
270
  child_record.send(details[:prepopulate_method_name], parent_record)
@@ -275,7 +274,7 @@ module IdentityCache
275
274
  next_level_records = parent_records
276
275
 
277
276
  when details = cached_has_ones[association]
278
- if details[:embed]
277
+ if details[:embed] == true
279
278
  parent_records = records.map(&details[:cached_accessor_name].to_sym)
280
279
  else
281
280
  raise ArgumentError.new("Non-embedded has_one associations do not support prefetching yet.")
@@ -317,9 +316,9 @@ module IdentityCache
317
316
  private
318
317
 
319
318
  def populate_association_caches # :nodoc:
320
- self.class.send(:all_cached_associations_needing_population).each do |cached_association, options|
319
+ self.class.send(:embedded_associations).each do |cached_association, options|
321
320
  send(options[:population_method_name])
322
- reflection = options[:embed] && self.class.reflect_on_association(cached_association)
321
+ reflection = options[:embed] == true && self.class.reflect_on_association(cached_association)
323
322
  if reflection && reflection.klass.respond_to?(:cached_has_manys)
324
323
  child_objects = Array.wrap(send(options[:cached_accessor_name]))
325
324
  child_objects.each{ |child| child.send(:populate_association_caches) }
@@ -327,10 +326,10 @@ module IdentityCache
327
326
  end
328
327
  end
329
328
 
330
- def fetch_denormalized_cached_association(ivar_name, association_name) # :nodoc:
329
+ def fetch_recursively_cached_association(ivar_name, association_name) # :nodoc:
331
330
  ivar_full_name = :"@#{ivar_name}"
332
331
  if IdentityCache.should_cache?
333
- populate_denormalized_cached_association(ivar_name, association_name)
332
+ populate_recursively_cached_association(ivar_name, association_name)
334
333
  assoc = IdentityCache.unmap_cached_nil_for(instance_variable_get(ivar_full_name))
335
334
  assoc.is_a?(ActiveRecord::Associations::CollectionAssociation) ? assoc.reader : assoc
336
335
  else
@@ -338,7 +337,7 @@ module IdentityCache
338
337
  end
339
338
  end
340
339
 
341
- def populate_denormalized_cached_association(ivar_name, association_name) # :nodoc:
340
+ def populate_recursively_cached_association(ivar_name, association_name) # :nodoc:
342
341
  ivar_full_name = :"@#{ivar_name}"
343
342
 
344
343
  value = instance_variable_get(ivar_full_name)
@@ -405,7 +404,8 @@ module IdentityCache
405
404
  end
406
405
 
407
406
  def was_new_record? # :nodoc:
408
- !destroyed? && transaction_changed_attributes.has_key?('id') && transaction_changed_attributes['id'].nil?
407
+ pk = self.class.primary_key
408
+ !destroyed? && transaction_changed_attributes.has_key?(pk) && transaction_changed_attributes[pk].nil?
409
409
  end
410
410
  end
411
411
  end
@@ -1,4 +1,4 @@
1
1
  module IdentityCache
2
- VERSION = "0.0.7"
2
+ VERSION = "0.1.0"
3
3
  CACHE_VERSION = 3
4
4
  end