identity_cache 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3aaaad2b3f7ef8cd9c4aaeb338756bfcccfcbd4c
4
- data.tar.gz: 06ee79bbcc777f7903139d9eab772ddcd50ad1af
3
+ metadata.gz: 953d802de951edde24a15f5d1c073bebdaebe477
4
+ data.tar.gz: 8a329ec724a1b9a9dba8435a47de9df84c7e8715
5
5
  SHA512:
6
- metadata.gz: 0678a544ee6f53d428517c5a44409a1f93a9e690002f6c8d3efe1ee731f50aed2eef3c68b7bba5b66f4500b0fd5bdef9fc09e990a1e9a5ece1d0fac8e496ada6
7
- data.tar.gz: c1313137f940b3e7c22523b9bf331416f4824288d4e45617931042641e73aabef0bc1d659384faf6be904d503164f80748c783fb5c3d216c72b1af0a6d40b2ba
6
+ metadata.gz: 89fe603c343ad1fcd8c92d1bd12a7375a52b82a12c4c016256e1940ddbd0a1d66d1248b033640b661b8d30f21020cc29b5b5600a9a36591b5e03cac9238d0ca9
7
+ data.tar.gz: 252d8f5066cc625fb799d490c3a4e5736283e82adf8817179c35697dbbb76ac07d28feeda32ad6e1d84db2440f820277578f3b9ea253997d64f3b1735e5fa74c
data/.travis.yml CHANGED
@@ -5,9 +5,15 @@ rvm:
5
5
  - 2.2.3
6
6
 
7
7
  gemfile:
8
- - Gemfile.rails40
9
- - Gemfile.rails41
10
8
  - Gemfile.rails42
9
+ - Gemfile.rails50
10
+
11
+ matrix:
12
+ exclude:
13
+ - rvm: 2.1
14
+ gemfile: Gemfile.rails50
15
+ - gemfile: Gemfile.rails50
16
+ env: DB=postgresql
11
17
 
12
18
  env:
13
19
  - DB=mysql2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # IdentityCache changelog
2
2
 
3
+ #### 0.3.2
4
+
5
+ - Deprecate returning non read-only records when cache is used. Set IdentityCache.fetch_readonly_records to true to avoid this. (#282)
6
+ - Use loaded association first when fetching a cache_has_many id embedded association (#280)
7
+ - Deprecate setting the inverse active record association on cache hits. Set IdentityCache.never_set_inverse_association to true to avoid this. (#279)
8
+ - Fetch association returns relation or array depending on the configuration. It was only returning a relation for cache_has_many fetch association methods. (#276)
9
+ - Stop sharing the same attributes hash between the fetched record and the memoized cache, which could interfere with dirty tracking (#267)
10
+
3
11
  #### 0.3.1
4
12
 
5
13
  - Fix cache_index for non-id primary key
data/Gemfile.rails50 ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'activerecord', '~> 5.0.0'
5
+ gem 'activesupport', '~> 5.0.0'
6
+ gem 'mysql2', '>= 0.3.18', '< 0.5'
data/README.md CHANGED
@@ -101,6 +101,16 @@ end
101
101
  @product.fetch_images
102
102
  ```
103
103
 
104
+ To read multiple records in batch use `fetch_multi`.
105
+
106
+ ``` ruby
107
+ class Product < ActiveRecord::Base
108
+ include IdentityCache
109
+ end
110
+
111
+ @product.fetch_multi([1, 2])
112
+ ```
113
+
104
114
  ### Embedding Associations
105
115
 
106
116
  IdentityCache can easily embed objects into the parents' cache entry. This means loading the parent object will also load the association and add it to the cache along with the parent. Subsequent cache requests will load the parent along with the association in one fetch. This can again mean some duplication in the cache if you want to be able to cache objects on their own as well, so it should be done with care. This works with both `cache_has_many` and `cache_has_one` methods.
data/dev.yml ADDED
@@ -0,0 +1,54 @@
1
+ name: identity-cache
2
+
3
+ up:
4
+ - homebrew:
5
+ - postgresql
6
+ - ruby:
7
+ version: 2.2.3p172-shopify
8
+ package: shopify/shopify/shopify-ruby
9
+ - railgun
10
+ - bundler
11
+
12
+ env:
13
+ RAILGUN_HOST: identity-cache.railgun
14
+ MYSQL_HOST: identity-cache.railgun
15
+ POSTGRES_HOST: identity-cache.railgun
16
+ MEMCACHED_HOST: identity-cache.railgun
17
+
18
+ commands:
19
+ test:
20
+ syntax:
21
+ optional:
22
+ argument: file
23
+ optional: args...
24
+ desc: 'Run tests'
25
+ run: |
26
+ if [[ $# -eq 0 ]]; then
27
+ bundle exec rake test
28
+ else
29
+ bundle exec ruby -I test "$@"
30
+ fi
31
+
32
+ benchmark-cpu:
33
+ desc: 'Run the identity cache CPU benchmark'
34
+ run: bundle exec rake benchmark:cpu
35
+
36
+ profile:
37
+ desc: 'Profile IDC code'
38
+ run: bundle exec rake profile:run
39
+
40
+ update-serialization-format:
41
+ desc: 'Update serialization format test fixture'
42
+ run: bundle exec rake update_serialization_format
43
+
44
+ railgun:
45
+ image: dev:railgun-common-services-0.2.x
46
+ ip_address: 192.168.64.98
47
+ memory: 1G
48
+ cores: 1
49
+ disk: 1G
50
+ services:
51
+ mysql: 3306
52
+ postgresql: 5432
53
+ memcached: 11211
54
+
@@ -2,8 +2,8 @@
2
2
  require File.expand_path('../lib/identity_cache/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ["Camilo Lopez", "Tom Burns", "Harry Brundage", "Dylan Smith", "Tobias Lutke", "Arthur Neves", "Francis Bogsanyi"]
6
- gem.email = ["harry.brundage@shopify.com"]
5
+ gem.authors = ["Camilo Lopez", "Tom Burns", "Harry Brundage", "Dylan Thacker-Smith", "Tobias Lutke", "Arthur Neves", "Francis Bogsanyi"]
6
+ gem.email = ["gems@shopify.com"]
7
7
  gem.description = %q{Opt in read through ActiveRecord caching.}
8
8
  gem.summary = %q{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.}
9
9
  gem.homepage = "https://github.com/Shopify/identity_cache"
@@ -16,9 +16,9 @@ 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', '>= 4.0.4')
20
- gem.add_development_dependency('memcached', '~> 1.8.0')
19
+ gem.add_dependency('activerecord', '>= 4.2.0')
21
20
 
21
+ gem.add_development_dependency('memcached', '~> 1.8.0')
22
22
  gem.add_development_dependency('memcached_store', '~> 0.12.6')
23
23
  gem.add_development_dependency('rake')
24
24
  gem.add_development_dependency('mocha', '0.14.0')
@@ -41,7 +41,11 @@ module IdentityCache
41
41
  @#{options[:records_variable_name]} = association(:#{association}).klass.fetch_by_id(#{foreign_key})
42
42
  end
43
43
  else
44
- #{association}
44
+ if IdentityCache.fetch_read_only_records && IdentityCache.should_use_cache?
45
+ load_and_readonlyify(:#{association})
46
+ else
47
+ #{association}
48
+ end
45
49
  end
46
50
  end
47
51
 
@@ -11,6 +11,7 @@ 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
+ klass.send(:check_association_scope, name)
14
15
  case options[:embed]
15
16
  when true
16
17
  schema_string << ",#{name}:(#{denormalized_schema_hash(options[:association_reflection].klass)})"
@@ -205,17 +205,14 @@ module IdentityCache
205
205
  options[:cached_accessor_name] = "fetch_#{association}"
206
206
  options[:records_variable_name] = "cached_#{association}"
207
207
 
208
+ self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
209
+ def #{options[:cached_accessor_name]}
210
+ fetch_recursively_cached_association('#{options[:records_variable_name]}', :#{association})
211
+ end
212
+ CODE
208
213
 
209
- unless instance_methods.include?(options[:cached_accessor_name].to_sym)
210
- self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
211
- def #{options[:cached_accessor_name]}
212
- fetch_recursively_cached_association('#{options[:records_variable_name]}', :#{association})
213
- end
214
- CODE
215
-
216
- options[:only_on_foreign_key_change] = false
217
- add_parent_expiry_hook(options)
218
- end
214
+ options[:only_on_foreign_key_change] = false
215
+ add_parent_expiry_hook(options)
219
216
  end
220
217
 
221
218
  def build_id_embedded_has_many_cache(association, options) #:nodoc:
@@ -236,10 +233,14 @@ module IdentityCache
236
233
  end
237
234
 
238
235
  def #{options[:cached_accessor_name]}
239
- if IdentityCache.should_use_cache? || #{association}.loaded?
236
+ if IdentityCache.should_use_cache? && !#{association}.loaded?
240
237
  @#{options[:records_variable_name]} ||= #{options[:association_reflection].klass}.fetch_multi(#{options[:cached_ids_name]})
241
238
  else
242
- #{association}
239
+ if IdentityCache.fetch_returns_relation
240
+ #{association}
241
+ else
242
+ #{association}.to_a
243
+ end
243
244
  end
244
245
  end
245
246
 
@@ -46,7 +46,7 @@ module IdentityCache
46
46
  klass = parent_association.klass
47
47
  end
48
48
  old_parent = klass.find(transaction_changed_attributes[foreign_key])
49
- rescue ActiveRecord::RecordNotFound => e
49
+ rescue ActiveRecord::RecordNotFound
50
50
  # suppress errors finding the old parent if its been destroyed since it will have expired itself in that case
51
51
  end
52
52
  end
@@ -26,7 +26,9 @@ module IdentityCache
26
26
  object = nil
27
27
  coder = IdentityCache.fetch(rails_cache_key(id)){ coder_from_record(object = resolve_cache_miss(id)) }
28
28
  object ||= record_from_coder(coder)
29
- IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]} " if object && object.id != id.to_i
29
+ if object && object.id.to_s != id.to_s
30
+ IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]}"
31
+ end
30
32
  object
31
33
  end
32
34
  else
@@ -74,6 +76,38 @@ module IdentityCache
74
76
  records
75
77
  end
76
78
 
79
+ def prefetch_associations(associations, records)
80
+ records = records.to_a
81
+ return if records.empty?
82
+ unless IdentityCache.should_use_cache?
83
+ ActiveRecord::Associations::Preloader.new.preload(records, associations)
84
+ return
85
+ end
86
+
87
+ case associations
88
+ when nil
89
+ # do nothing
90
+ when Symbol
91
+ prefetch_one_association(associations, records)
92
+ when Array
93
+ associations.each do |association|
94
+ prefetch_associations(association, records)
95
+ end
96
+ when Hash
97
+ associations.each do |association, sub_associations|
98
+ next_level_records = prefetch_one_association(association, records)
99
+
100
+ associated_class = reflect_on_association(association).klass
101
+ if associated_class.respond_to?(:prefetch_associations)
102
+ associated_class.prefetch_associations(sub_associations, next_level_records)
103
+ end
104
+ end
105
+ else
106
+ raise TypeError, "Invalid associations class #{associations.class}"
107
+ end
108
+ nil
109
+ end
110
+
77
111
  private
78
112
 
79
113
  def raise_if_scoped
@@ -82,6 +116,14 @@ module IdentityCache
82
116
  end
83
117
  end
84
118
 
119
+ def check_association_scope(association_name)
120
+ association_reflection = reflect_on_association(association_name)
121
+ scope = association_reflection.scope
122
+ if scope && !association_reflection.klass.all.instance_exec(&scope).joins_values.empty?
123
+ raise UnsupportedAssociationError, "caching association #{self}.#{association_name} scoped with a join isn't supported"
124
+ end
125
+ end
126
+
85
127
  def record_from_coder(coder) #:nodoc:
86
128
  if coder
87
129
  klass = coder[:class]
@@ -89,28 +131,47 @@ module IdentityCache
89
131
 
90
132
  coder[:associations].each {|name, value| set_embedded_association(record, name, value) } if coder.has_key?(:associations)
91
133
  coder[:association_ids].each {|name, ids| record.instance_variable_set(:"@#{record.class.cached_has_manys[name][:ids_variable_name]}", ids) } if coder.has_key?(:association_ids)
134
+ record.readonly! if IdentityCache.fetch_read_only_records
92
135
  record
93
136
  end
94
137
  end
95
138
 
139
+ def set_inverse_of_cached_has_many(record, association_reflection, child_records)
140
+ associated_class = association_reflection.klass
141
+ return unless associated_class < IdentityCache
142
+
143
+ inverse_name = record.class.cached_has_manys.fetch(association_reflection.name).fetch(:inverse_name)
144
+ inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
145
+ return unless inverse_cached_association
146
+
147
+ prepopulate_method_name = inverse_cached_association.fetch(:prepopulate_method_name)
148
+ child_records.each { |child_record| child_record.send(prepopulate_method_name, record) }
149
+ end
150
+
96
151
  def set_embedded_association(record, association_name, coder_or_array) #:nodoc:
97
152
  value = if IdentityCache.unmap_cached_nil_for(coder_or_array).nil?
98
153
  nil
99
154
  elsif (reflection = record.class.reflect_on_association(association_name)).collection?
100
155
  association = reflection.association_class.new(record, reflection)
101
156
  association.target = coder_or_array.map {|e| record_from_coder(e) }
102
- association.target.each {|e| association.set_inverse_instance(e) }
157
+
158
+ set_inverse_of_cached_has_many(record, reflection, association.target)
159
+
160
+ unless IdentityCache.never_set_inverse_association
161
+ association.target.each {|e| association.set_inverse_instance(e) }
162
+ end
163
+
103
164
  association
104
165
  else
105
166
  record_from_coder(coder_or_array)
106
167
  end
107
168
  variable_name = record.class.send(:recursively_embedded_associations)[association_name][:records_variable_name]
108
- record.instance_variable_set(:"@#{variable_name}", IdentityCache.map_cached_nil_for(value))
169
+ record.instance_variable_set(:"@#{variable_name}", value)
109
170
  end
110
171
 
111
172
  def get_embedded_association(record, association, options) #:nodoc:
112
173
  embedded_variable = record.public_send(options.fetch(:cached_accessor_name))
113
- if record.class.reflect_on_association(association).collection?
174
+ if embedded_variable.respond_to?(:to_ary)
114
175
  embedded_variable.map {|e| coder_from_record(e) }
115
176
  else
116
177
  coder_from_record(embedded_variable)
@@ -120,7 +181,7 @@ module IdentityCache
120
181
  def coder_from_record(record) #:nodoc:
121
182
  unless record.nil?
122
183
  coder = {
123
- attributes: record.attributes_before_type_cast,
184
+ attributes: record.attributes_before_type_cast.dup,
124
185
  class: record.class,
125
186
  }
126
187
  add_cached_associations_to_coder(record, coder)
@@ -164,7 +225,10 @@ module IdentityCache
164
225
 
165
226
  def resolve_cache_miss(id)
166
227
  record = self.includes(cache_fetch_includes).reorder(nil).where(primary_key => id).first
167
- preload_id_embedded_associations([record]) if record
228
+ if record
229
+ preload_id_embedded_associations([record])
230
+ record.readonly! if IdentityCache.fetch_read_only_records && IdentityCache.should_use_cache?
231
+ end
168
232
  record
169
233
  end
170
234
 
@@ -209,7 +273,7 @@ module IdentityCache
209
273
  end
210
274
 
211
275
  def all_cached_associations
212
- (cached_has_manys || {}).merge(cached_has_ones || {}).merge(cached_belongs_tos || {})
276
+ cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
213
277
  end
214
278
 
215
279
  def embedded_associations
@@ -241,120 +305,138 @@ module IdentityCache
241
305
  @id_column ||= columns.detect {|c| c.name == primary_key}
242
306
  ids = ids.map{ |id| connection.type_cast(id, @id_column) }
243
307
  records = where(primary_key => ids).includes(cache_fetch_includes).to_a
308
+ records.each(&:readonly!) if IdentityCache.fetch_read_only_records && IdentityCache.should_use_cache?
244
309
  preload_id_embedded_associations(records)
245
310
  records_by_id = records.index_by(&:id)
246
311
  ids.map{ |id| records_by_id[id] }
247
312
  end
248
313
 
249
- def prefetch_associations(associations, records)
250
- associations = hashify_includes_structure(associations)
314
+ def fetch_embedded_associations(records)
315
+ associations = embedded_associations
316
+ return if associations.empty?
251
317
 
252
- associations.each do |association, sub_associations|
253
- case
254
- when details = cached_has_manys[association]
318
+ return unless primary_cache_index_enabled
255
319
 
256
- if details[:embed] == true
257
- child_records = records.map(&details[:cached_accessor_name].to_sym).flatten
320
+ cached_records_by_id = fetch_multi(records.map(&:id)).index_by(&:id)
321
+
322
+ associations.each_value do |options|
323
+ records.each do |record|
324
+ next unless cached_record = cached_records_by_id[record.id]
325
+ if options[:embed] == :ids
326
+ cached_association = cached_record.public_send(options.fetch(:cached_ids_name))
327
+ record.instance_variable_set(:"@#{options.fetch(:ids_variable_name)}", cached_association)
258
328
  else
259
- ids_to_parent_record = records.each_with_object({}) do |record, hash|
260
- child_ids = record.send(details[:cached_ids_name])
261
- child_ids.each do |child_id|
262
- hash[child_id] = record
263
- end
264
- end
329
+ cached_association = cached_record.public_send(options.fetch(:cached_accessor_name))
330
+ record.instance_variable_set(:"@#{options.fetch(:records_variable_name)}", cached_association)
331
+ end
332
+ end
333
+ end
334
+ end
265
335
 
266
- parent_record_to_child_records = Hash.new { |h, k| h[k] = [] }
267
- child_records = details[:association_reflection].klass.fetch_multi(*ids_to_parent_record.keys)
268
- child_records.each do |child_record|
269
- parent_record = ids_to_parent_record[child_record.id]
270
- parent_record_to_child_records[parent_record] << child_record
271
- end
336
+ def prefetch_embedded_association(records, association, details)
337
+ # Make the same assumption as ActiveRecord::Associations::Preloader, which is
338
+ # that all the records have the same associations loaded, so we can just check
339
+ # the first record to see if an association is loaded.
340
+ first_record = records.first
341
+ return if first_record.association(association).loaded?
342
+ iv_name_key = details[:embed] == true ? :records_variable_name : :ids_variable_name
343
+ return if first_record.instance_variable_defined?(:"@#{details[iv_name_key]}")
344
+ fetch_embedded_associations(records)
345
+ end
272
346
 
273
- parent_record_to_child_records.each do |parent_record, child_records|
274
- parent_record.send(details[:prepopulate_method_name], child_records)
347
+ def prefetch_one_association(association, records)
348
+ case
349
+ when details = cached_has_manys[association]
350
+ prefetch_embedded_association(records, association, details)
351
+ if details[:embed] == true
352
+ child_records = records.flat_map(&details[:cached_accessor_name].to_sym)
353
+ else
354
+ ids_to_parent_record = records.each_with_object({}) do |record, hash|
355
+ child_ids = record.send(details[:cached_ids_name])
356
+ child_ids.each do |child_id|
357
+ hash[child_id] = record
275
358
  end
276
359
  end
277
360
 
278
- next_level_records = child_records
279
-
280
- when details = cached_belongs_tos[association]
281
- if details[:embed] == true
282
- raise ArgumentError.new("Embedded belongs_to associations do not support prefetching yet.")
283
- else
284
- reflection = details[:association_reflection]
285
- if reflection.polymorphic?
286
- raise ArgumentError.new("Polymorphic belongs_to associations do not support prefetching yet.")
287
- end
361
+ parent_record_to_child_records = Hash.new { |h, k| h[k] = [] }
362
+ child_records = details[:association_reflection].klass.fetch_multi(*ids_to_parent_record.keys)
363
+ child_records.each do |child_record|
364
+ parent_record = ids_to_parent_record[child_record.id]
365
+ parent_record_to_child_records[parent_record] << child_record
366
+ end
288
367
 
289
- ids_to_child_record = records.each_with_object({}) do |child_record, hash|
290
- parent_id = child_record.send(reflection.foreign_key)
291
- hash[parent_id] = child_record if parent_id.present?
292
- end
293
- parent_records = reflection.klass.fetch_multi(ids_to_child_record.keys)
294
- parent_records.each do |parent_record|
295
- child_record = ids_to_child_record[parent_record.id]
296
- child_record.send(details[:prepopulate_method_name], parent_record)
297
- end
368
+ parent_record_to_child_records.each do |parent, children|
369
+ parent.send(details[:prepopulate_method_name], children)
298
370
  end
371
+ end
299
372
 
300
- next_level_records = parent_records
373
+ next_level_records = child_records
301
374
 
302
- when details = cached_has_ones[association]
303
- if details[:embed] == true
304
- parent_records = records.map(&details[:cached_accessor_name].to_sym)
305
- else
306
- raise ArgumentError.new("Non-embedded has_one associations do not support prefetching yet.")
375
+ when details = cached_belongs_tos[association]
376
+ if details[:embed] == true
377
+ raise ArgumentError.new("Embedded belongs_to associations do not support prefetching yet.")
378
+ else
379
+ reflection = details[:association_reflection]
380
+ if reflection.polymorphic?
381
+ raise ArgumentError.new("Polymorphic belongs_to associations do not support prefetching yet.")
307
382
  end
308
383
 
309
- next_level_records = parent_records
384
+ cached_iv_name = :"@#{details.fetch(:records_variable_name)}"
385
+ ids_to_child_record = records.each_with_object({}) do |child_record, hash|
386
+ parent_id = child_record.send(reflection.foreign_key)
387
+ if parent_id && !child_record.instance_variable_defined?(cached_iv_name)
388
+ hash[parent_id] = child_record
389
+ end
390
+ end
391
+ parent_records = reflection.klass.fetch_multi(ids_to_child_record.keys)
392
+ parent_records.each do |parent_record|
393
+ child_record = ids_to_child_record[parent_record.id]
394
+ child_record.send(details[:prepopulate_method_name], parent_record)
395
+ end
396
+ end
310
397
 
398
+ next_level_records = parent_records
399
+
400
+ when details = cached_has_ones[association]
401
+ if details[:embed] == true
402
+ prefetch_embedded_association(records, association, details)
403
+ parent_records = records.map(&details[:cached_accessor_name].to_sym)
311
404
  else
312
- raise ArgumentError.new("Unknown cached association #{association} listed for prefetching")
405
+ raise ArgumentError.new("Non-embedded has_one associations do not support prefetching yet.")
313
406
  end
314
407
 
315
- if details && details[:association_reflection].klass.respond_to?(:prefetch_associations, true)
316
- details[:association_reflection].klass.send(:prefetch_associations, sub_associations, next_level_records)
317
- end
318
- end
319
- end
408
+ next_level_records = parent_records
320
409
 
321
- def hashify_includes_structure(structure)
322
- case structure
323
- when nil
324
- {}
325
- when Symbol
326
- {structure => []}
327
- when Hash
328
- structure.clone
329
- when Array
330
- structure.each_with_object({}) do |member, hash|
331
- case member
332
- when Hash
333
- hash.merge!(member)
334
- when Symbol
335
- hash[member] = []
336
- end
337
- end
410
+ else
411
+ raise ArgumentError.new("Unknown cached association #{association} listed for prefetching")
338
412
  end
413
+ next_level_records
339
414
  end
340
415
  end
341
416
 
342
417
  private
343
418
 
344
419
  def fetch_recursively_cached_association(ivar_name, association_name) # :nodoc:
345
- if IdentityCache.should_use_cache?
420
+ assoc = if IdentityCache.should_use_cache?
346
421
  ivar_full_name = :"@#{ivar_name}"
347
422
 
348
- unless ivar_value = instance_variable_get(ivar_full_name)
349
- ivar_value = IdentityCache.map_cached_nil_for(send(association_name))
350
- instance_variable_set(ivar_full_name, ivar_value)
423
+ assoc = if instance_variable_defined?(ivar_full_name)
424
+ instance_variable_get(ivar_full_name)
425
+ else
426
+ cached_assoc = if IdentityCache.fetch_read_only_records
427
+ load_and_readonlyify(association_name)
428
+ else
429
+ send(association_name)
430
+ end
431
+ instance_variable_set(ivar_full_name, cached_assoc)
351
432
  end
352
433
 
353
- assoc = IdentityCache.unmap_cached_nil_for(ivar_value)
354
434
  assoc.is_a?(ActiveRecord::Associations::CollectionAssociation) ? assoc.reader : assoc
355
435
  else
356
436
  send(association_name.to_sym)
357
437
  end
438
+ assoc = assoc.to_ary if assoc.respond_to?(:to_ary) && !IdentityCache.fetch_returns_relation
439
+ assoc
358
440
  end
359
441
 
360
442
  def expire_primary_index # :nodoc:
@@ -400,5 +482,15 @@ module IdentityCache
400
482
  pk = self.class.primary_key
401
483
  !destroyed? && transaction_changed_attributes.has_key?(pk) && transaction_changed_attributes[pk].nil?
402
484
  end
485
+
486
+ def load_and_readonlyify(association_name)
487
+ record_or_records = send(association_name)
488
+
489
+ if self.class.reflect_on_association(association_name).collection?
490
+ record_or_records.map { |p| p.dup.tap(&:readonly!) }
491
+ else
492
+ record_or_records.dup.tap(&:readonly!) if record_or_records
493
+ end
494
+ end
403
495
  end
404
496
  end
@@ -1,4 +1,4 @@
1
1
  module IdentityCache
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.2"
3
3
  CACHE_VERSION = 6
4
4
  end