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 +4 -4
- data/.travis.yml +8 -2
- data/CHANGELOG.md +8 -0
- data/Gemfile.rails50 +6 -0
- data/README.md +10 -0
- data/dev.yml +54 -0
- data/identity_cache.gemspec +4 -4
- data/lib/identity_cache/belongs_to_caching.rb +5 -1
- data/lib/identity_cache/cache_key_generation.rb +1 -0
- data/lib/identity_cache/configuration_dsl.rb +13 -12
- data/lib/identity_cache/parent_model_expiration.rb +1 -1
- data/lib/identity_cache/query_api.rb +174 -82
- data/lib/identity_cache/version.rb +1 -1
- data/lib/identity_cache.rb +45 -1
- data/performance/cache_runner.rb +1 -1
- data/test/cache_hash_test.rb +0 -2
- data/test/cache_invalidation_test.rb +5 -4
- data/test/denormalized_has_many_test.rb +101 -7
- data/test/denormalized_has_one_test.rb +36 -0
- data/test/fetch_multi_test.rb +42 -0
- data/test/fetch_test.rb +32 -0
- data/test/helpers/database_connection.rb +4 -3
- data/test/index_cache_test.rb +2 -2
- data/test/memoized_attributes_test.rb +49 -0
- data/test/normalized_belongs_to_test.rb +24 -0
- data/test/normalized_has_many_test.rb +29 -5
- data/test/{prefetch_normalized_associations_test.rb → prefetch_associations_test.rb} +118 -9
- data/test/recursive_denormalized_has_many_test.rb +18 -2
- data/test/schema_change_test.rb +3 -2
- data/test/test_helper.rb +4 -2
- metadata +13 -12
- data/.ruby-version +0 -1
- data/Gemfile.rails40 +0 -6
- data/Gemfile.rails41 +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 953d802de951edde24a15f5d1c073bebdaebe477
|
4
|
+
data.tar.gz: 8a329ec724a1b9a9dba8435a47de9df84c7e8715
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
+
|
data/identity_cache.gemspec
CHANGED
@@ -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 = ["
|
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
|
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
|
-
|
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
|
-
|
210
|
-
|
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?
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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}",
|
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
|
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
|
-
|
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
|
-
|
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
|
250
|
-
associations =
|
314
|
+
def fetch_embedded_associations(records)
|
315
|
+
associations = embedded_associations
|
316
|
+
return if associations.empty?
|
251
317
|
|
252
|
-
|
253
|
-
case
|
254
|
-
when details = cached_has_manys[association]
|
318
|
+
return unless primary_cache_index_enabled
|
255
319
|
|
256
|
-
|
257
|
-
|
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
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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
|
-
|
274
|
-
|
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
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
-
|
290
|
-
|
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
|
-
|
373
|
+
next_level_records = child_records
|
301
374
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
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("
|
405
|
+
raise ArgumentError.new("Non-embedded has_one associations do not support prefetching yet.")
|
313
406
|
end
|
314
407
|
|
315
|
-
|
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
|
-
|
322
|
-
|
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
|
-
|
349
|
-
|
350
|
-
|
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
|