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
@@ -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
|