identity_cache 0.5.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/probots.yml +2 -0
- data/.github/workflows/ci.yml +26 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +24 -9
- data/CHANGELOG.md +21 -0
- data/Gemfile +5 -1
- data/README.md +28 -26
- data/Rakefile +14 -5
- data/dev.yml +9 -16
- data/gemfiles/Gemfile.latest-release +6 -0
- data/gemfiles/Gemfile.rails-edge +6 -0
- data/gemfiles/Gemfile.rails52 +6 -0
- data/identity_cache.gemspec +26 -10
- data/lib/identity_cache.rb +49 -46
- data/lib/identity_cache/belongs_to_caching.rb +12 -40
- data/lib/identity_cache/cache_fetcher.rb +6 -5
- data/lib/identity_cache/cache_hash.rb +2 -2
- data/lib/identity_cache/cache_invalidation.rb +4 -11
- data/lib/identity_cache/cache_key_generation.rb +17 -65
- data/lib/identity_cache/cache_key_loader.rb +128 -0
- data/lib/identity_cache/cached.rb +7 -0
- data/lib/identity_cache/cached/association.rb +87 -0
- data/lib/identity_cache/cached/attribute.rb +123 -0
- data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
- data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
- data/lib/identity_cache/cached/belongs_to.rb +93 -0
- data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
- data/lib/identity_cache/cached/prefetcher.rb +51 -0
- data/lib/identity_cache/cached/primary_index.rb +97 -0
- data/lib/identity_cache/cached/recursive/association.rb +68 -0
- data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
- data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
- data/lib/identity_cache/cached/reference/association.rb +16 -0
- data/lib/identity_cache/cached/reference/has_many.rb +105 -0
- data/lib/identity_cache/cached/reference/has_one.rb +100 -0
- data/lib/identity_cache/configuration_dsl.rb +53 -215
- data/lib/identity_cache/encoder.rb +95 -0
- data/lib/identity_cache/expiry_hook.rb +36 -0
- data/lib/identity_cache/fallback_fetcher.rb +2 -1
- data/lib/identity_cache/load_strategy/eager.rb +28 -0
- data/lib/identity_cache/load_strategy/lazy.rb +71 -0
- data/lib/identity_cache/load_strategy/load_request.rb +20 -0
- data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +127 -58
- data/lib/identity_cache/parent_model_expiration.rb +45 -11
- data/lib/identity_cache/query_api.rb +128 -394
- data/lib/identity_cache/railtie.rb +8 -0
- data/lib/identity_cache/record_not_found.rb +6 -0
- data/lib/identity_cache/should_use_cache.rb +1 -0
- data/lib/identity_cache/version.rb +3 -2
- data/lib/identity_cache/with_primary_index.rb +136 -0
- data/lib/identity_cache/without_primary_index.rb +24 -3
- data/performance/cache_runner.rb +28 -34
- data/performance/cpu.rb +3 -2
- data/performance/externals.rb +4 -3
- data/performance/profile.rb +6 -5
- data/railgun.yml +16 -0
- metadata +44 -73
- data/Gemfile.rails42 +0 -6
- data/Gemfile.rails50 +0 -6
- data/test/attribute_cache_test.rb +0 -110
- data/test/cache_fetch_includes_test.rb +0 -46
- data/test/cache_hash_test.rb +0 -14
- data/test/cache_invalidation_test.rb +0 -139
- data/test/deeply_nested_associated_record_test.rb +0 -19
- data/test/denormalized_has_many_test.rb +0 -214
- data/test/denormalized_has_one_test.rb +0 -160
- data/test/fetch_multi_test.rb +0 -308
- data/test/fetch_test.rb +0 -258
- data/test/fixtures/serialized_record.mysql2 +0 -0
- data/test/fixtures/serialized_record.postgresql +0 -0
- data/test/helpers/active_record_objects.rb +0 -106
- data/test/helpers/database_connection.rb +0 -72
- data/test/helpers/serialization_format.rb +0 -51
- data/test/helpers/update_serialization_format.rb +0 -27
- data/test/identity_cache_test.rb +0 -29
- data/test/index_cache_test.rb +0 -161
- data/test/memoized_attributes_test.rb +0 -59
- data/test/memoized_cache_proxy_test.rb +0 -107
- data/test/normalized_belongs_to_test.rb +0 -107
- data/test/normalized_has_many_test.rb +0 -231
- data/test/normalized_has_one_test.rb +0 -9
- data/test/prefetch_associations_test.rb +0 -379
- data/test/readonly_test.rb +0 -109
- data/test/recursive_denormalized_has_many_test.rb +0 -131
- data/test/save_test.rb +0 -82
- data/test/schema_change_test.rb +0 -112
- data/test/serialization_format_change_test.rb +0 -16
- data/test/test_helper.rb +0 -140
@@ -1,76 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module IdentityCache
|
2
3
|
module ConfigurationDSL
|
3
4
|
extend ActiveSupport::Concern
|
4
5
|
|
5
6
|
included do |base|
|
6
|
-
base.class_attribute
|
7
|
-
base.class_attribute
|
8
|
-
base.class_attribute
|
9
|
-
base.class_attribute :primary_cache_index_enabled
|
7
|
+
base.class_attribute(:cache_indexes)
|
8
|
+
base.class_attribute(:cached_has_manys)
|
9
|
+
base.class_attribute(:cached_has_ones)
|
10
10
|
|
11
11
|
base.cached_has_manys = {}
|
12
12
|
base.cached_has_ones = {}
|
13
13
|
base.cache_indexes = []
|
14
|
-
base.primary_cache_index_enabled = true
|
15
|
-
|
16
|
-
base.after_commit :expire_parent_caches
|
17
14
|
end
|
18
15
|
|
19
16
|
module ClassMethods
|
20
|
-
# Declares a new index in the cache for the class where IdentityCache was
|
21
|
-
# included.
|
22
|
-
#
|
23
|
-
# IdentityCache will add a fetch_by_field1_and_field2_and_...field for every
|
24
|
-
# index.
|
25
|
-
#
|
26
|
-
# == Example:
|
27
|
-
#
|
28
|
-
# class Product
|
29
|
-
# include IdentityCache
|
30
|
-
# cache_index :name, :vendor
|
31
|
-
# end
|
32
|
-
#
|
33
|
-
# Will add Product.fetch_by_name_and_vendor
|
34
|
-
#
|
35
|
-
# == Parameters
|
36
|
-
#
|
37
|
-
# +fields+ Array of symbols or strings representing the fields in the index
|
38
|
-
#
|
39
|
-
# == Options
|
40
|
-
# * unique: if the index would only have unique values
|
41
|
-
#
|
42
|
-
def cache_index(*fields)
|
43
|
-
raise NotImplementedError, "Cache indexes need an enabled primary index" unless primary_cache_index_enabled
|
44
|
-
options = fields.extract_options!
|
45
|
-
unique = options[:unique] || false
|
46
|
-
cache_attribute_by_alias('primary_key', 'id', by: fields, unique: unique)
|
47
|
-
|
48
|
-
field_list = fields.join("_and_")
|
49
|
-
arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
|
50
|
-
|
51
|
-
if unique
|
52
|
-
self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
|
53
|
-
def fetch_by_#{field_list}(#{arg_list}, options={})
|
54
|
-
id = fetch_id_by_#{field_list}(#{arg_list})
|
55
|
-
id && fetch_by_id(id, options)
|
56
|
-
end
|
57
|
-
|
58
|
-
# exception throwing variant
|
59
|
-
def fetch_by_#{field_list}!(#{arg_list}, options={})
|
60
|
-
fetch_by_#{field_list}(#{arg_list}, options) or raise ActiveRecord::RecordNotFound
|
61
|
-
end
|
62
|
-
CODE
|
63
|
-
else
|
64
|
-
self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
|
65
|
-
def fetch_by_#{field_list}(#{arg_list}, options={})
|
66
|
-
ids = fetch_id_by_#{field_list}(#{arg_list})
|
67
|
-
ids.empty? ? ids : fetch_multi(ids, options)
|
68
|
-
end
|
69
|
-
CODE
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
17
|
# Will cache an association to the class including IdentityCache.
|
75
18
|
# The embed option, if set, will make IdentityCache keep the association
|
76
19
|
# values in the same cache entry as the parent.
|
@@ -84,10 +27,8 @@ module IdentityCache
|
|
84
27
|
# include IdentityCache
|
85
28
|
# has_many :options
|
86
29
|
# has_many :orders
|
87
|
-
# has_many :buyers
|
88
30
|
# cache_has_many :options, embed: :ids
|
89
31
|
# cache_has_many :orders
|
90
|
-
# cache_has_many :buyers, inverse_name: 'line_item'
|
91
32
|
# end
|
92
33
|
#
|
93
34
|
# == Parameters
|
@@ -95,27 +36,28 @@ module IdentityCache
|
|
95
36
|
#
|
96
37
|
# == Options
|
97
38
|
#
|
98
|
-
# * embed: If
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
def cache_has_many(association,
|
39
|
+
# * embed: If `true`, IdentityCache will embed the associated records
|
40
|
+
# in the cache entries for this model, as well as all the embedded
|
41
|
+
# associations for the associated record recursively.
|
42
|
+
# If `:ids` (the default), it will only embed the ids for the associated
|
43
|
+
# records.
|
44
|
+
def cache_has_many(association, embed: :ids)
|
104
45
|
ensure_base_model
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
ensure_cacheable_association(association, options)
|
109
|
-
self.cached_has_manys[association] = options
|
110
|
-
|
111
|
-
case options[:embed]
|
112
|
-
when true
|
113
|
-
build_recursive_association_cache(association, options)
|
46
|
+
check_association_for_caching(association)
|
47
|
+
reflection = reflect_on_association(association)
|
48
|
+
association_class = case embed
|
114
49
|
when :ids
|
115
|
-
|
50
|
+
Cached::Reference::HasMany
|
51
|
+
when true
|
52
|
+
Cached::Recursive::HasMany
|
116
53
|
else
|
117
54
|
raise NotImplementedError
|
118
55
|
end
|
56
|
+
|
57
|
+
cached_has_manys[association] = association_class.new(
|
58
|
+
association,
|
59
|
+
reflection: reflection,
|
60
|
+
).tap(&:build)
|
119
61
|
end
|
120
62
|
|
121
63
|
# Will cache an association to the class including IdentityCache.
|
@@ -125,7 +67,7 @@ module IdentityCache
|
|
125
67
|
# == Example:
|
126
68
|
# class Product
|
127
69
|
# cache_has_one :store, embed: true
|
128
|
-
# cache_has_one :vendor
|
70
|
+
# cache_has_one :vendor, embed: :id
|
129
71
|
# end
|
130
72
|
#
|
131
73
|
# == Parameters
|
@@ -133,24 +75,27 @@ module IdentityCache
|
|
133
75
|
#
|
134
76
|
# == Options
|
135
77
|
#
|
136
|
-
# * embed:
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
|
141
|
-
# parent object's class)
|
142
|
-
def cache_has_one(association, options = {})
|
78
|
+
# * embed: If `true`, IdentityCache will embed the associated record
|
79
|
+
# in the cache entries for this model, as well as all the embedded
|
80
|
+
# associations for the associated record recursively.
|
81
|
+
# If `:id`, it will only embed the id for the associated record.
|
82
|
+
def cache_has_one(association, embed:)
|
143
83
|
ensure_base_model
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
84
|
+
check_association_for_caching(association)
|
85
|
+
reflection = reflect_on_association(association)
|
86
|
+
association_class = case embed
|
87
|
+
when :id
|
88
|
+
Cached::Reference::HasOne
|
89
|
+
when true
|
90
|
+
Cached::Recursive::HasOne
|
151
91
|
else
|
152
92
|
raise NotImplementedError
|
153
93
|
end
|
94
|
+
|
95
|
+
cached_has_ones[association] = association_class.new(
|
96
|
+
association,
|
97
|
+
reflection: reflection,
|
98
|
+
).tap(&:build)
|
154
99
|
end
|
155
100
|
|
156
101
|
# Will cache a single attribute on its own blob, it will add a
|
@@ -170,145 +115,38 @@ module IdentityCache
|
|
170
115
|
#
|
171
116
|
# * by: Other attribute or attributes in the model to keep values indexed. Default is :id
|
172
117
|
# * unique: if the index would only have unique values. Default is true
|
173
|
-
def cache_attribute(attribute,
|
174
|
-
cache_attribute_by_alias(attribute
|
175
|
-
end
|
176
|
-
|
177
|
-
def disable_primary_cache_index
|
178
|
-
ActiveSupport::Deprecation.warn("disable_primary_cache_index is deprecated, use `include IdentityCache::WithoutPrimaryIndex` instead")
|
179
|
-
ensure_base_model
|
180
|
-
self.primary_cache_index_enabled = false
|
118
|
+
def cache_attribute(attribute, by: :id, unique: true)
|
119
|
+
cache_attribute_by_alias(attribute, alias_name: attribute, by: by, unique: unique)
|
181
120
|
end
|
182
121
|
|
183
122
|
private
|
184
123
|
|
185
|
-
def cache_attribute_by_alias(
|
124
|
+
def cache_attribute_by_alias(attribute_or_proc, alias_name:, by:, unique:)
|
186
125
|
ensure_base_model
|
187
|
-
|
188
|
-
alias_name = alias_name.to_sym
|
189
|
-
unique = options[:unique].nil? ? true : !!options[:unique]
|
190
|
-
fields = Array(options[:by])
|
191
|
-
|
192
|
-
self.cache_indexes.push [alias_name, fields, unique]
|
193
|
-
|
194
|
-
field_list = fields.join("_and_")
|
195
|
-
arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
|
196
|
-
|
197
|
-
self.instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
|
198
|
-
def fetch_#{alias_name}_by_#{field_list}(#{arg_list})
|
199
|
-
attribute_dynamic_fetcher(#{attribute}, #{fields.inspect}, [#{arg_list}], #{unique})
|
200
|
-
end
|
201
|
-
CODE
|
202
|
-
end
|
203
|
-
|
204
|
-
def build_recursive_association_cache(association, options) #:nodoc:
|
205
|
-
options[:association_reflection] = reflect_on_association(association)
|
206
|
-
options[:cached_accessor_name] = "fetch_#{association}"
|
207
|
-
options[:records_variable_name] = "cached_#{association}"
|
208
|
-
|
209
|
-
self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
|
210
|
-
def #{options[:cached_accessor_name]}
|
211
|
-
fetch_recursively_cached_association('#{options[:records_variable_name]}', :#{association})
|
212
|
-
end
|
213
|
-
CODE
|
214
|
-
|
215
|
-
options[:only_on_foreign_key_change] = false
|
216
|
-
add_parent_expiry_hook(options)
|
217
|
-
end
|
218
|
-
|
219
|
-
def build_id_embedded_has_many_cache(association, options) #:nodoc:
|
220
|
-
singular_association = association.to_s.singularize
|
221
|
-
options[:association_reflection] = reflect_on_association(association)
|
222
|
-
options[:cached_accessor_name] = "fetch_#{association}"
|
223
|
-
options[:ids_name] = "#{singular_association}_ids"
|
224
|
-
options[:cached_ids_name] = "fetch_#{options[:ids_name]}"
|
225
|
-
options[:ids_variable_name] = "cached_#{options[:ids_name]}"
|
226
|
-
options[:records_variable_name] = "cached_#{association}"
|
227
|
-
options[:prepopulate_method_name] = "prepopulate_fetched_#{association}"
|
228
|
-
|
229
|
-
self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
|
230
|
-
attr_reader :#{options[:ids_variable_name]}
|
231
|
-
|
232
|
-
def #{options[:cached_ids_name]}
|
233
|
-
@#{options[:ids_variable_name]} ||= #{options[:ids_name]}
|
234
|
-
end
|
235
|
-
|
236
|
-
def #{options[:cached_accessor_name]}
|
237
|
-
association_klass = association(:#{association}).klass
|
238
|
-
if association_klass.should_use_cache? && !#{association}.loaded?
|
239
|
-
@#{options[:records_variable_name]} ||= #{options[:association_reflection].klass}.fetch_multi(#{options[:cached_ids_name]})
|
240
|
-
else
|
241
|
-
#{association}.to_a
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
def #{options[:prepopulate_method_name]}(records)
|
246
|
-
@#{options[:records_variable_name]} = records
|
247
|
-
end
|
248
|
-
CODE
|
126
|
+
fields = Array(by)
|
249
127
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
def attribute_dynamic_fetcher(attribute, fields, values, unique_index) #:nodoc:
|
255
|
-
raise_if_scoped
|
256
|
-
|
257
|
-
if should_use_cache?
|
258
|
-
cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique_index)
|
259
|
-
IdentityCache.fetch(cache_key) do
|
260
|
-
dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
|
261
|
-
end
|
262
|
-
else
|
263
|
-
dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
|
268
|
-
query = reorder(nil).where(Hash[fields.zip(values)])
|
269
|
-
query = query.limit(1) if unique_index
|
270
|
-
results = query.pluck(attribute)
|
271
|
-
unique_index ? results.first : results
|
272
|
-
end
|
273
|
-
|
274
|
-
def add_parent_expiry_hook(options)
|
275
|
-
child_class = options[:association_reflection].klass
|
276
|
-
unless child_class < IdentityCache
|
277
|
-
message = "associated class #{child_class} will need to include IdentityCache or " \
|
278
|
-
"IdentityCache::WithoutPrimaryIndex for embedded associations"
|
279
|
-
ActiveSupport::Deprecation.warn(message, caller(3))
|
280
|
-
child_class.send(:include, IdentityCache::WithoutPrimaryIndex)
|
281
|
-
end
|
282
|
-
child_class.parent_expiration_entries[options[:inverse_name]] << [self, options[:only_on_foreign_key_change]]
|
283
|
-
end
|
284
|
-
|
285
|
-
def deprecate_embed_option(options, old_value, new_value)
|
286
|
-
if options[:embed] == old_value
|
287
|
-
options[:embed] = new_value
|
288
|
-
ActiveSupport::Deprecation.warn("`embed: #{old_value.inspect}` was renamed to `embed: #{new_value.inspect}` for clarity", caller(2))
|
289
|
-
end
|
128
|
+
klass = fields.one? ? Cached::AttributeByOne : Cached::AttributeByMulti
|
129
|
+
cached_attribute = klass.new(self, attribute_or_proc, alias_name, fields, unique)
|
130
|
+
cached_attribute.build
|
131
|
+
cache_indexes.push(cached_attribute)
|
290
132
|
end
|
291
133
|
|
292
134
|
def ensure_base_model
|
293
135
|
if self != cached_model
|
294
|
-
raise DerivedModelError,
|
136
|
+
raise DerivedModelError, <<~MSG.squish
|
137
|
+
IdentityCache class methods must be called on the same
|
138
|
+
model that includes IdentityCache
|
139
|
+
MSG
|
295
140
|
end
|
296
141
|
end
|
297
142
|
|
298
|
-
def
|
299
|
-
unless association_reflection =
|
143
|
+
def check_association_for_caching(association)
|
144
|
+
unless (association_reflection = reflect_on_association(association))
|
300
145
|
raise AssociationError, "Association named '#{association}' was not found on #{self.class}"
|
301
146
|
end
|
302
147
|
if association_reflection.options[:through]
|
303
148
|
raise UnsupportedAssociationError, "caching through associations isn't supported"
|
304
149
|
end
|
305
|
-
options[:inverse_name] ||= association_reflection.inverse_of.name if association_reflection.inverse_of
|
306
|
-
options[:inverse_name] ||= self.name.underscore.to_sym
|
307
|
-
child_class = association_reflection.klass
|
308
|
-
raise InverseAssociationError unless child_class.reflect_on_association(options[:inverse_name])
|
309
|
-
unless options[:embed] == true || child_class.include?(IdentityCache)
|
310
|
-
raise UnsupportedAssociationError, "associated class #{child_class} must include IdentityCache to be cached without full embedding"
|
311
|
-
end
|
312
150
|
end
|
313
151
|
end
|
314
152
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Encoder
|
4
|
+
DEHYDRATE_EVENT = "dehydration.identity_cache"
|
5
|
+
HYDRATE_EVENT = "hydration.identity_cache"
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def encode(record)
|
9
|
+
return unless record
|
10
|
+
|
11
|
+
ActiveSupport::Notifications.instrument(DEHYDRATE_EVENT, class: record.class.name) do
|
12
|
+
coder_from_record(record, record.class)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def decode(coder, klass)
|
17
|
+
return unless coder
|
18
|
+
|
19
|
+
ActiveSupport::Notifications.instrument(HYDRATE_EVENT, class: klass.name) do
|
20
|
+
record_from_coder(coder, klass)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def coder_from_record(record, klass)
|
27
|
+
return unless record
|
28
|
+
|
29
|
+
coder = {}
|
30
|
+
coder[:attributes] = record.attributes_before_type_cast.dup
|
31
|
+
|
32
|
+
recursively_embedded_associations = klass.send(:recursively_embedded_associations)
|
33
|
+
id_embedded_has_manys = klass.cached_has_manys.select { |_, association| association.embedded_by_reference? }
|
34
|
+
id_embedded_has_ones = klass.cached_has_ones.select { |_, association| association.embedded_by_reference? }
|
35
|
+
|
36
|
+
if recursively_embedded_associations.present?
|
37
|
+
coder[:associations] = recursively_embedded_associations.each_with_object({}) do |(name, association), hash|
|
38
|
+
hash[name] = IdentityCache.map_cached_nil_for(embedded_coder(record, name, association))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if id_embedded_has_manys.present?
|
43
|
+
coder[:association_ids] = id_embedded_has_manys.each_with_object({}) do |(name, association), hash|
|
44
|
+
hash[name] = record.instance_variable_get(association.ids_variable_name)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
if id_embedded_has_ones.present?
|
49
|
+
coder[:association_id] = id_embedded_has_ones.each_with_object({}) do |(name, association), hash|
|
50
|
+
hash[name] = record.instance_variable_get(association.id_variable_name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
coder
|
55
|
+
end
|
56
|
+
|
57
|
+
def embedded_coder(record, _association, cached_association)
|
58
|
+
embedded_record_or_records = record.public_send(cached_association.cached_accessor_name)
|
59
|
+
|
60
|
+
if embedded_record_or_records.respond_to?(:to_ary)
|
61
|
+
embedded_record_or_records.map do |embedded_record|
|
62
|
+
coder_from_record(embedded_record, embedded_record.class)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
coder_from_record(embedded_record_or_records, embedded_record_or_records.class)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def record_from_coder(coder, klass) #:nodoc:
|
70
|
+
record = klass.instantiate(coder[:attributes].dup)
|
71
|
+
|
72
|
+
if coder.key?(:associations)
|
73
|
+
coder[:associations].each do |name, value|
|
74
|
+
record.instance_variable_set(klass.cached_association(name).dehydrated_variable_name, value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
if coder.key?(:association_ids)
|
78
|
+
coder[:association_ids].each do |name, ids|
|
79
|
+
record.instance_variable_set(klass.cached_has_manys.fetch(name).ids_variable_name, ids)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
if coder.key?(:association_id)
|
83
|
+
coder[:association_id].each do |name, id|
|
84
|
+
record.instance_variable_set(klass.cached_has_ones.fetch(name).id_variable_name, id)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
record.readonly! if IdentityCache.fetch_read_only_records
|
89
|
+
record
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private_constant :Encoder
|
95
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
class ExpiryHook
|
4
|
+
def initialize(cached_association)
|
5
|
+
@cached_association = cached_association
|
6
|
+
end
|
7
|
+
|
8
|
+
def install
|
9
|
+
cached_association.validate
|
10
|
+
entry = [parent_class, only_on_foreign_key_change?]
|
11
|
+
child_class.parent_expiration_entries[inverse_name] << entry
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :cached_association
|
17
|
+
|
18
|
+
def only_on_foreign_key_change?
|
19
|
+
cached_association.embedded_by_reference?
|
20
|
+
end
|
21
|
+
|
22
|
+
def inverse_name
|
23
|
+
cached_association.inverse_name
|
24
|
+
end
|
25
|
+
|
26
|
+
def parent_class
|
27
|
+
cached_association.reflection.active_record
|
28
|
+
end
|
29
|
+
|
30
|
+
def child_class
|
31
|
+
cached_association.reflection.klass
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private_constant :ExpiryHook
|
36
|
+
end
|