identity_cache 0.5.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Cached
|
4
|
+
class Association # :nodoc:
|
5
|
+
include EmbeddedFetching
|
6
|
+
|
7
|
+
def initialize(name, reflection:)
|
8
|
+
@name = name
|
9
|
+
@reflection = reflection
|
10
|
+
@cached_accessor_name = :"fetch_#{name}"
|
11
|
+
@records_variable_name = :"@cached_#{name}"
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :name, :reflection, :cached_accessor_name, :records_variable_name
|
15
|
+
|
16
|
+
def build
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def read(_record)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(_record, _value)
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear(_record)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch(_records)
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_async(_load_strategy, _records)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def embedded?
|
41
|
+
embedded_by_reference? || embedded_recursively?
|
42
|
+
end
|
43
|
+
|
44
|
+
def embedded_by_reference?
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def embedded_recursively?
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
def inverse_name
|
53
|
+
@inverse_name ||= begin
|
54
|
+
reflection.inverse_of&.name ||
|
55
|
+
reflection.active_record.name.underscore
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate
|
60
|
+
parent_class = reflection.active_record
|
61
|
+
child_class = reflection.klass
|
62
|
+
|
63
|
+
unless child_class < IdentityCache::WithoutPrimaryIndex
|
64
|
+
if embedded_recursively?
|
65
|
+
raise UnsupportedAssociationError, <<~MSG.squish
|
66
|
+
cached association #{parent_class}\##{reflection.name} requires
|
67
|
+
associated class #{child_class} to include IdentityCache
|
68
|
+
or IdentityCache::WithoutPrimaryIndex
|
69
|
+
MSG
|
70
|
+
else
|
71
|
+
raise UnsupportedAssociationError, <<~MSG.squish
|
72
|
+
cached association #{parent_class}\##{reflection.name} requires
|
73
|
+
associated class #{child_class} to include IdentityCache
|
74
|
+
MSG
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
unless child_class.reflect_on_association(inverse_name)
|
79
|
+
raise InverseAssociationError, <<~MSG
|
80
|
+
Inverse name for association #{parent_class}\##{reflection.name} could not be determined.
|
81
|
+
Use the :inverse_of option on the Active Record association to specify the inverse association name.
|
82
|
+
MSG
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module Cached
|
5
|
+
# @abstract
|
6
|
+
class Attribute
|
7
|
+
attr_reader :model, :alias_name, :key_fields, :unique
|
8
|
+
|
9
|
+
def initialize(model, attribute_or_proc, alias_name, key_fields, unique)
|
10
|
+
@model = model
|
11
|
+
if attribute_or_proc.is_a?(Proc)
|
12
|
+
@attribute_proc = attribute_or_proc
|
13
|
+
else
|
14
|
+
@attribute = attribute_or_proc.to_sym
|
15
|
+
end
|
16
|
+
@alias_name = alias_name.to_sym
|
17
|
+
@key_fields = key_fields.map(&:to_sym)
|
18
|
+
@unique = !!unique
|
19
|
+
end
|
20
|
+
|
21
|
+
def attribute
|
22
|
+
@attribute ||= @attribute_proc.call.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
def fetch(db_key)
|
26
|
+
db_key = cast_db_key(db_key)
|
27
|
+
|
28
|
+
if model.should_use_cache?
|
29
|
+
IdentityCache.fetch(cache_key(db_key)) do
|
30
|
+
load_one_from_db(db_key)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
load_one_from_db(db_key)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def expire(record)
|
38
|
+
unless record.send(:was_new_record?)
|
39
|
+
old_key = old_cache_key(record)
|
40
|
+
IdentityCache.cache.delete(old_key)
|
41
|
+
end
|
42
|
+
unless record.destroyed?
|
43
|
+
new_key = new_cache_key(record)
|
44
|
+
if new_key != old_key
|
45
|
+
IdentityCache.cache.delete(new_key)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def cache_key(index_key)
|
51
|
+
values_hash = IdentityCache.memcache_hash(unhashed_values_cache_key_string(index_key))
|
52
|
+
"#{model.rails_cache_key_namespace}#{cache_key_prefix}#{values_hash}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_one_from_db(key)
|
56
|
+
query = model.reorder(nil).where(load_from_db_where_conditions(key))
|
57
|
+
query = query.limit(1) if unique
|
58
|
+
results = query.pluck(attribute)
|
59
|
+
unique ? results.first : results
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# @abstract
|
65
|
+
def cast_db_key(_index_key)
|
66
|
+
raise NotImplementedError
|
67
|
+
end
|
68
|
+
|
69
|
+
# @abstract
|
70
|
+
def unhashed_values_cache_key_string(_index_key)
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
# @abstract
|
75
|
+
def load_from_db_where_conditions(_index_key_or_keys)
|
76
|
+
raise NotImplementedError
|
77
|
+
end
|
78
|
+
|
79
|
+
# @abstract
|
80
|
+
def cache_key_from_key_values(_key_values)
|
81
|
+
raise NotImplementedError
|
82
|
+
end
|
83
|
+
|
84
|
+
def field_types
|
85
|
+
@field_types ||= key_fields.map { |field| model.type_for_attribute(field.to_s) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def cache_key_prefix
|
89
|
+
@cache_key_prefix ||= begin
|
90
|
+
unique_indicator = unique ? '' : 's'
|
91
|
+
"attr#{unique_indicator}" \
|
92
|
+
":#{model.base_class.name}" \
|
93
|
+
":#{attribute}" \
|
94
|
+
":#{key_fields.join('/')}:"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def new_cache_key(record)
|
99
|
+
new_key_values = key_fields.map { |field| record.send(field) }
|
100
|
+
cache_key_from_key_values(new_key_values)
|
101
|
+
end
|
102
|
+
|
103
|
+
def old_cache_key(record)
|
104
|
+
old_key_values = key_fields.map do |field|
|
105
|
+
field_string = field.to_s
|
106
|
+
changes = record.transaction_changed_attributes
|
107
|
+
if record.destroyed? && changes.key?(field_string)
|
108
|
+
changes[field_string]
|
109
|
+
elsif record.persisted? && changes.key?(field_string)
|
110
|
+
changes[field_string]
|
111
|
+
else
|
112
|
+
record.send(field)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
cache_key_from_key_values(old_key_values)
|
116
|
+
end
|
117
|
+
|
118
|
+
def fetch_method_suffix
|
119
|
+
"#{alias_name}_by_#{key_fields.join('_and_')}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module Cached
|
5
|
+
class AttributeByMulti < Attribute
|
6
|
+
def build
|
7
|
+
cached_attribute = self
|
8
|
+
|
9
|
+
model.define_singleton_method(:"fetch_#{fetch_method_suffix}") do |*key_values|
|
10
|
+
raise_if_scoped
|
11
|
+
cached_attribute.fetch(key_values)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Attribute method overrides
|
18
|
+
|
19
|
+
def cast_db_key(key_values)
|
20
|
+
field_types.each_with_index do |type, i|
|
21
|
+
key_values[i] = type.cast(key_values[i])
|
22
|
+
end
|
23
|
+
key_values
|
24
|
+
end
|
25
|
+
|
26
|
+
def unhashed_values_cache_key_string(key_values)
|
27
|
+
key_values.map { |v| v.try!(:to_s).inspect }.join('/')
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_from_db_where_conditions(key_values)
|
31
|
+
Hash[key_fields.zip(key_values)]
|
32
|
+
end
|
33
|
+
|
34
|
+
alias_method :cache_key_from_key_values, :cache_key
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module Cached
|
5
|
+
class AttributeByOne < Attribute
|
6
|
+
attr_reader :key_field
|
7
|
+
|
8
|
+
def initialize(*)
|
9
|
+
super
|
10
|
+
@key_field = key_fields.first
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
cached_attribute = self
|
15
|
+
|
16
|
+
model.define_singleton_method(:"fetch_#{fetch_method_suffix}") do |key|
|
17
|
+
raise_if_scoped
|
18
|
+
cached_attribute.fetch(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
model.define_singleton_method(:"fetch_multi_#{fetch_method_suffix}") do |keys|
|
22
|
+
raise_if_scoped
|
23
|
+
cached_attribute.fetch_multi(keys)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def fetch_multi(keys)
|
28
|
+
keys = keys.map { |key| cast_db_key(key) }
|
29
|
+
|
30
|
+
unless model.should_use_cache?
|
31
|
+
return load_multi_from_db(keys)
|
32
|
+
end
|
33
|
+
|
34
|
+
unordered_hash = CacheKeyLoader.load_multi(self, keys)
|
35
|
+
|
36
|
+
# Calling `values` on the result is expected to return the values in the same order as their
|
37
|
+
# corresponding keys. The fetch_multi_by_#{field_list} generated methods depend on this.
|
38
|
+
ordered_hash = {}
|
39
|
+
keys.each { |key| ordered_hash[key] = unordered_hash.fetch(key) }
|
40
|
+
ordered_hash
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_multi_from_db(keys)
|
44
|
+
rows = model.reorder(nil).where(load_from_db_where_conditions(keys)).pluck(key_field, attribute)
|
45
|
+
result = {}
|
46
|
+
default = unique ? nil : []
|
47
|
+
keys.each do |index_value|
|
48
|
+
result[index_value] = default.try!(:dup)
|
49
|
+
end
|
50
|
+
if unique
|
51
|
+
rows.each do |index_value, attribute_value|
|
52
|
+
result[index_value] = attribute_value
|
53
|
+
end
|
54
|
+
else
|
55
|
+
rows.each do |index_value, attribute_value|
|
56
|
+
result[index_value] << attribute_value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
def cache_encode(db_value)
|
63
|
+
db_value
|
64
|
+
end
|
65
|
+
alias_method :cache_decode, :cache_encode
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Attribute method overrides
|
70
|
+
|
71
|
+
def cast_db_key(key)
|
72
|
+
field_types.first.cast(key)
|
73
|
+
end
|
74
|
+
|
75
|
+
def unhashed_values_cache_key_string(key)
|
76
|
+
key.try!(:to_s).inspect
|
77
|
+
end
|
78
|
+
|
79
|
+
def load_from_db_where_conditions(key_values)
|
80
|
+
{ key_field => key_values }
|
81
|
+
end
|
82
|
+
|
83
|
+
def cache_key_from_key_values(key_values)
|
84
|
+
cache_key(key_values.first)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Cached
|
4
|
+
class BelongsTo < Association # :nodoc:
|
5
|
+
attr_reader :records_variable_name
|
6
|
+
|
7
|
+
def build
|
8
|
+
reflection.active_record.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
9
|
+
def #{cached_accessor_name}
|
10
|
+
association_klass = association(:#{name}).klass
|
11
|
+
if association_klass.should_use_cache? && #{reflection.foreign_key}.present? && !association(:#{name}).loaded?
|
12
|
+
if defined?(#{records_variable_name})
|
13
|
+
#{records_variable_name}
|
14
|
+
else
|
15
|
+
#{records_variable_name} = association_klass.fetch_by_id(#{reflection.foreign_key})
|
16
|
+
end
|
17
|
+
else
|
18
|
+
#{name}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
RUBY
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear(record)
|
25
|
+
if record.instance_variable_defined?(records_variable_name)
|
26
|
+
record.remove_instance_variable(records_variable_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch(records)
|
31
|
+
fetch_async(LoadStrategy::Eager, records) { |associated_records| associated_records }
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch_async(load_strategy, records)
|
35
|
+
if reflection.polymorphic?
|
36
|
+
cache_keys_to_associated_ids = {}
|
37
|
+
|
38
|
+
records.each do |owner_record|
|
39
|
+
associated_id = owner_record.send(reflection.foreign_key)
|
40
|
+
next unless associated_id && !owner_record.instance_variable_defined?(records_variable_name)
|
41
|
+
associated_cache_key = Object.const_get(
|
42
|
+
owner_record.send(reflection.foreign_type)
|
43
|
+
).cached_model.cached_primary_index
|
44
|
+
unless cache_keys_to_associated_ids[associated_cache_key]
|
45
|
+
cache_keys_to_associated_ids[associated_cache_key] = {}
|
46
|
+
end
|
47
|
+
cache_keys_to_associated_ids[associated_cache_key][associated_id] = owner_record
|
48
|
+
end
|
49
|
+
|
50
|
+
load_strategy.load_batch(cache_keys_to_associated_ids) do |associated_records_by_cache_key|
|
51
|
+
batch_records = []
|
52
|
+
associated_records_by_cache_key.each do |cache_key, associated_records|
|
53
|
+
associated_records.keys.each do |id, associated_record|
|
54
|
+
owner_record = cache_keys_to_associated_ids.fetch(cache_key).fetch(id)
|
55
|
+
batch_records << owner_record
|
56
|
+
owner_record.instance_variable_set(records_variable_name, associated_record)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
yield batch_records
|
61
|
+
end
|
62
|
+
else
|
63
|
+
ids_to_owner_record = records.each_with_object({}) do |owner_record, hash|
|
64
|
+
associated_id = owner_record.send(reflection.foreign_key)
|
65
|
+
if associated_id && !owner_record.instance_variable_defined?(records_variable_name)
|
66
|
+
hash[associated_id] = owner_record
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
load_strategy.load_multi(
|
71
|
+
reflection.klass.cached_primary_index,
|
72
|
+
ids_to_owner_record.keys
|
73
|
+
) do |associated_records_by_id|
|
74
|
+
associated_records_by_id.each do |id, associated_record|
|
75
|
+
owner_record = ids_to_owner_record.fetch(id)
|
76
|
+
owner_record.instance_variable_set(records_variable_name, associated_record)
|
77
|
+
end
|
78
|
+
|
79
|
+
yield associated_records_by_id.values.compact
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def embedded_recursively?
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
def embedded_by_reference?
|
89
|
+
false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Cached
|
4
|
+
module EmbeddedFetching
|
5
|
+
private
|
6
|
+
|
7
|
+
def fetch_embedded(records)
|
8
|
+
fetch_embedded_async(LoadStrategy::Eager, records) {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch_embedded_async(load_strategy, records)
|
12
|
+
return yield if embedded_fetched?(records)
|
13
|
+
|
14
|
+
klass = reflection.active_record
|
15
|
+
cached_associations = klass.send(:embedded_associations)
|
16
|
+
|
17
|
+
return yield if cached_associations.empty?
|
18
|
+
|
19
|
+
return yield unless klass.primary_cache_index_enabled
|
20
|
+
|
21
|
+
load_strategy.load_multi(klass.cached_primary_index, records.map(&:id)) do |cached_records_by_id|
|
22
|
+
cached_associations.each_value do |cached_association|
|
23
|
+
records.each do |record|
|
24
|
+
next unless (cached_record = cached_records_by_id[record.id])
|
25
|
+
cached_value = cached_association.read(cached_record)
|
26
|
+
cached_association.write(record, cached_value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def embedded_fetched?(records)
|
35
|
+
# NOTE: Assume all records are the same, so just check the first one.
|
36
|
+
record = records.first
|
37
|
+
record.association(name).loaded? || record.instance_variable_defined?(records_variable_name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|