identity_cache 0.4.1 → 1.1.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.
Files changed (94) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +92 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +5 -0
  6. data/CAVEATS.md +25 -0
  7. data/CHANGELOG.md +73 -19
  8. data/Gemfile +5 -1
  9. data/LICENSE +1 -1
  10. data/README.md +49 -27
  11. data/Rakefile +14 -5
  12. data/dev.yml +12 -16
  13. data/gemfiles/Gemfile.latest-release +8 -0
  14. data/gemfiles/Gemfile.min-supported +7 -0
  15. data/gemfiles/Gemfile.rails-edge +7 -0
  16. data/identity_cache.gemspec +29 -10
  17. data/lib/identity_cache.rb +78 -51
  18. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  19. data/lib/identity_cache/cache_fetcher.rb +6 -5
  20. data/lib/identity_cache/cache_hash.rb +2 -2
  21. data/lib/identity_cache/cache_invalidation.rb +4 -11
  22. data/lib/identity_cache/cache_key_generation.rb +17 -65
  23. data/lib/identity_cache/cache_key_loader.rb +128 -0
  24. data/lib/identity_cache/cached.rb +7 -0
  25. data/lib/identity_cache/cached/association.rb +87 -0
  26. data/lib/identity_cache/cached/attribute.rb +123 -0
  27. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  28. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  29. data/lib/identity_cache/cached/belongs_to.rb +100 -0
  30. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  31. data/lib/identity_cache/cached/prefetcher.rb +61 -0
  32. data/lib/identity_cache/cached/primary_index.rb +96 -0
  33. data/lib/identity_cache/cached/recursive/association.rb +109 -0
  34. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  35. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  36. data/lib/identity_cache/cached/reference/association.rb +16 -0
  37. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  38. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  39. data/lib/identity_cache/configuration_dsl.rb +53 -215
  40. data/lib/identity_cache/encoder.rb +95 -0
  41. data/lib/identity_cache/expiry_hook.rb +36 -0
  42. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  43. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  44. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  45. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  46. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  47. data/lib/identity_cache/mem_cache_store_cas.rb +53 -0
  48. data/lib/identity_cache/memoized_cache_proxy.rb +137 -58
  49. data/lib/identity_cache/parent_model_expiration.rb +46 -11
  50. data/lib/identity_cache/query_api.rb +102 -408
  51. data/lib/identity_cache/railtie.rb +8 -0
  52. data/lib/identity_cache/record_not_found.rb +6 -0
  53. data/lib/identity_cache/should_use_cache.rb +1 -0
  54. data/lib/identity_cache/version.rb +3 -2
  55. data/lib/identity_cache/with_primary_index.rb +136 -0
  56. data/lib/identity_cache/without_primary_index.rb +24 -3
  57. data/performance/cache_runner.rb +25 -73
  58. data/performance/cpu.rb +4 -3
  59. data/performance/externals.rb +4 -3
  60. data/performance/profile.rb +6 -5
  61. data/railgun.yml +16 -0
  62. metadata +60 -73
  63. data/.travis.yml +0 -30
  64. data/Gemfile.rails42 +0 -6
  65. data/Gemfile.rails50 +0 -6
  66. data/test/attribute_cache_test.rb +0 -110
  67. data/test/cache_fetch_includes_test.rb +0 -46
  68. data/test/cache_hash_test.rb +0 -14
  69. data/test/cache_invalidation_test.rb +0 -139
  70. data/test/deeply_nested_associated_record_test.rb +0 -19
  71. data/test/denormalized_has_many_test.rb +0 -211
  72. data/test/denormalized_has_one_test.rb +0 -160
  73. data/test/fetch_multi_test.rb +0 -308
  74. data/test/fetch_test.rb +0 -258
  75. data/test/fixtures/serialized_record.mysql2 +0 -0
  76. data/test/fixtures/serialized_record.postgresql +0 -0
  77. data/test/helpers/active_record_objects.rb +0 -106
  78. data/test/helpers/database_connection.rb +0 -72
  79. data/test/helpers/serialization_format.rb +0 -42
  80. data/test/helpers/update_serialization_format.rb +0 -24
  81. data/test/identity_cache_test.rb +0 -29
  82. data/test/index_cache_test.rb +0 -161
  83. data/test/memoized_attributes_test.rb +0 -49
  84. data/test/memoized_cache_proxy_test.rb +0 -107
  85. data/test/normalized_belongs_to_test.rb +0 -107
  86. data/test/normalized_has_many_test.rb +0 -231
  87. data/test/normalized_has_one_test.rb +0 -9
  88. data/test/prefetch_associations_test.rb +0 -364
  89. data/test/readonly_test.rb +0 -109
  90. data/test/recursive_denormalized_has_many_test.rb +0 -131
  91. data/test/save_test.rb +0 -82
  92. data/test/schema_change_test.rb +0 -112
  93. data/test/serialization_format_change_test.rb +0 -16
  94. data/test/test_helper.rb +0 -140
@@ -1,106 +0,0 @@
1
- module SwitchNamespace
2
-
3
- module ClassMethods
4
- def rails_cache_key_namespace
5
- "#{self.namespace}:#{super}"
6
- end
7
- end
8
-
9
- def self.included(base)
10
- base.extend ClassMethods
11
- base.class_eval do
12
- class_attribute :namespace
13
- self.namespace = 'ns'
14
- end
15
- end
16
- end
17
-
18
- module ActiveRecordObjects
19
-
20
- def setup_models(base = ActiveRecord::Base)
21
- Object.send :const_set, 'DeeplyAssociatedRecord', Class.new(base) {
22
- include IdentityCache
23
- belongs_to :item
24
- belongs_to :associated_record
25
- default_scope { order('name DESC') }
26
- }
27
-
28
- Object.send :const_set, 'AssociatedRecord', Class.new(base) {
29
- include IdentityCache
30
- belongs_to :item, inverse_of: :associated_records
31
- belongs_to :item_two, inverse_of: :associated_records
32
- has_many :deeply_associated_records
33
- default_scope { order('id DESC') }
34
- }
35
-
36
- Object.send :const_set, 'NormalizedAssociatedRecord', Class.new(base) {
37
- include IdentityCache
38
- belongs_to :item
39
- default_scope { order('id DESC') }
40
- }
41
-
42
- Object.send :const_set, 'NotCachedRecord', Class.new(base) {
43
- belongs_to :item, :touch => true
44
- default_scope { order('id DESC') }
45
- }
46
-
47
- Object.send :const_set, 'PolymorphicRecord', Class.new(base) {
48
- belongs_to :owner, :polymorphic => true
49
- }
50
-
51
- Object.send :const_set, 'Deeply', Module.new
52
- Deeply.send :const_set, 'Nested', Module.new
53
- Deeply::Nested.send :const_set, 'AssociatedRecord', Class.new(base) {
54
- include IdentityCache
55
- }
56
-
57
- Object.send :const_set, 'Item', Class.new(base) {
58
- include IdentityCache
59
- belongs_to :item
60
- has_many :associated_records, inverse_of: :item
61
- has_many :deeply_associated_records, inverse_of: :item
62
- has_many :normalized_associated_records
63
- has_many :not_cached_records
64
- has_many :polymorphic_records, :as => 'owner'
65
- has_one :polymorphic_record, :as => 'owner'
66
- has_one :associated, :class_name => 'AssociatedRecord'
67
- }
68
-
69
- Object.send :const_set, 'ItemTwo', Class.new(base) {
70
- include IdentityCache
71
- has_many :associated_records, inverse_of: :item_two, foreign_key: :item_two_id
72
- self.table_name = 'items2'
73
- }
74
-
75
- Object.send :const_set, 'KeyedRecord', Class.new(base) {
76
- include IdentityCache
77
- self.primary_key = "hashed_key"
78
- }
79
-
80
- Object.send :const_set, 'StiRecord', Class.new(base) {
81
- include IdentityCache
82
- has_many :polymorphic_records, :as => 'owner'
83
- }
84
-
85
- Object.send :const_set, 'StiRecordTypeA', Class.new(StiRecord) {
86
- }
87
- end
88
-
89
- def teardown_models
90
- ActiveSupport::DescendantsTracker.clear
91
- ActiveSupport::Dependencies.clear
92
- Object.send :remove_const, 'DeeplyAssociatedRecord'
93
- Object.send :remove_const, 'PolymorphicRecord'
94
- Object.send :remove_const, 'NormalizedAssociatedRecord'
95
- Object.send :remove_const, 'AssociatedRecord'
96
- Object.send :remove_const, 'NotCachedRecord'
97
- Object.send :remove_const, 'Item'
98
- Object.send :remove_const, 'ItemTwo'
99
- Object.send :remove_const, 'KeyedRecord'
100
- Object.send :remove_const, 'StiRecord'
101
- Object.send :remove_const, 'StiRecordTypeA'
102
- Deeply::Nested.send :remove_const, 'AssociatedRecord'
103
- Deeply.send :remove_const, 'Nested'
104
- Object.send :remove_const, 'Deeply'
105
- end
106
- end
@@ -1,72 +0,0 @@
1
- module DatabaseConnection
2
- def self.db_name
3
- ENV.fetch('DB', 'mysql2')
4
- end
5
-
6
- def self.setup
7
- db_config = ENV['DATABASE_URL'] || DEFAULT_CONFIG[db_name]
8
- begin
9
- ActiveRecord::Base.establish_connection(db_config)
10
- ActiveRecord::Base.connection
11
- rescue
12
- raise unless db_config.is_a?(Hash)
13
- ActiveRecord::Base.establish_connection(db_config.merge('database' => nil))
14
- ActiveRecord::Base.connection.create_database(db_config['database'])
15
- ActiveRecord::Base.establish_connection(db_config)
16
- end
17
- end
18
-
19
- def self.drop_tables
20
- TABLES.keys.each do |table|
21
- ActiveRecord::Base.connection.drop_table(table) if table_exists?(table)
22
- end
23
- end
24
-
25
- def self.table_exists?(table)
26
- if ActiveRecord::Base.connection.respond_to?(:data_source_exists?)
27
- ActiveRecord::Base.connection.data_source_exists?(table)
28
- else
29
- ActiveRecord::Base.connection.table_exists?(table)
30
- end
31
- end
32
-
33
- def self.create_tables
34
- TABLES.each do |table, fields|
35
- fields = fields.dup
36
- options = fields.last.is_a?(Hash) ? fields.pop : {}
37
- ActiveRecord::Base.connection.create_table(table, options) do |t|
38
- fields.each do |column_type, *args|
39
- t.send(column_type, *args)
40
- end
41
- end
42
- end
43
- end
44
-
45
- TABLES = {
46
- :polymorphic_records => [[:string, :owner_type], [:integer, :owner_id], [:timestamps, null: true]],
47
- :deeply_associated_records => [[:string, :name], [:integer, :associated_record_id], [:integer, :item_id], [:timestamps, null: true]],
48
- :associated_records => [[:string, :name], [:integer, :item_id], [:integer, :item_two_id]],
49
- :normalized_associated_records => [[:string, :name], [:integer, :item_id], [:timestamps, null: true]],
50
- :not_cached_records => [[:string, :name], [:integer, :item_id], [:timestamps, null: true]],
51
- :items => [[:integer, :item_id], [:string, :title], [:timestamps, null: true]],
52
- :items2 => [[:integer, :item_id], [:string, :title], [:timestamps, null: true]],
53
- :keyed_records => [[:string, :value], :primary_key => "hashed_key"],
54
- :sti_records => [[:string, :type], [:string, :name]],
55
- }
56
-
57
- DEFAULT_CONFIG = {
58
- 'mysql2' => {
59
- 'adapter' => 'mysql2',
60
- 'database' => 'identity_cache_test',
61
- 'host' => ENV['MYSQL_HOST'] || '127.0.0.1',
62
- 'username' => 'root'
63
- },
64
- 'postgresql' => {
65
- 'adapter' => 'postgresql',
66
- 'database' => 'identity_cache_test',
67
- 'host' => ENV['POSTGRES_HOST'] || '127.0.0.1',
68
- 'username' => 'postgres',
69
- 'prepared_statements' => false,
70
- }
71
- }
72
- end
@@ -1,42 +0,0 @@
1
- module SerializationFormat
2
- def serialized_record
3
- AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
4
- AssociatedRecord.cache_belongs_to :item, :embed => false
5
- Item.cache_has_many :associated_records, :embed => true
6
- Item.cache_has_one :associated
7
- time = Time.parse('1970-01-01T00:00:00 UTC')
8
-
9
- record = Item.new(:title => 'foo')
10
- record.associated_records << AssociatedRecord.new(:name => 'bar')
11
- record.associated_records << AssociatedRecord.new(:name => 'baz')
12
- record.associated = AssociatedRecord.new(:name => 'bork')
13
- record.not_cached_records << NotCachedRecord.new(:name => 'NoCache', created_at: time)
14
- record.associated.deeply_associated_records << DeeplyAssociatedRecord.new(:name => "corge", created_at: time)
15
- record.associated.deeply_associated_records << DeeplyAssociatedRecord.new(:name => "qux", created_at: time)
16
- record.created_at = time
17
- record.save
18
- [Item, NotCachedRecord, DeeplyAssociatedRecord].each do |model|
19
- model.update_all(updated_at: time)
20
- end
21
- record.reload
22
- Item.fetch(record.id)
23
- IdentityCache.fetch(record.primary_cache_index_key)
24
- end
25
-
26
- def serialized_record_file
27
- File.expand_path("../../fixtures/serialized_record.#{DatabaseConnection.db_name}", __FILE__)
28
- end
29
-
30
- def serialize(record, anIO = nil)
31
- hash = {
32
- :version => IdentityCache::CACHE_VERSION,
33
- :record => record
34
- }
35
-
36
- if anIO
37
- Marshal.dump(hash, anIO)
38
- else
39
- Marshal.dump(hash)
40
- end
41
- end
42
- end
@@ -1,24 +0,0 @@
1
- $LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
2
-
3
- require 'logger'
4
- require 'active_record'
5
- require 'memcached_store'
6
- require 'active_support/cache/memcached_store'
7
- require_relative 'serialization_format'
8
- require_relative 'database_connection'
9
- require_relative 'active_record_objects'
10
- require 'identity_cache'
11
-
12
- include SerializationFormat
13
- include ActiveRecordObjects
14
-
15
- DatabaseConnection.setup
16
- DatabaseConnection.drop_tables
17
- DatabaseConnection.create_tables
18
- IdentityCache.logger = Logger.new(nil)
19
- IdentityCache.cache_backend = ActiveSupport::Cache::MemcachedStore.new("localhost:11211", :support_cas => true)
20
- setup_models
21
- File.open(serialized_record_file, 'w') {|file| serialize(serialized_record, file) }
22
- puts "Serialized record to #{serialized_record_file}"
23
- IdentityCache.cache.clear
24
- teardown_models
@@ -1,29 +0,0 @@
1
- require "test_helper"
2
-
3
- class IdentityCacheTest < IdentityCache::TestCase
4
-
5
- class BadModelBase < ActiveRecord::Base
6
- include IdentityCache
7
- end
8
-
9
- class BadModel < BadModelBase
10
- end
11
-
12
- def test_identity_cache_raises_if_loaded_twice
13
- assert_raises(IdentityCache::AlreadyIncludedError) do
14
- BadModel.class_eval do
15
- include IdentityCache
16
- end
17
- end
18
- end
19
-
20
- def test_should_use_cache_outside_transaction
21
- assert_equal true, IdentityCache.should_use_cache?
22
- end
23
-
24
- def test_should_use_cache_in_transaction
25
- ActiveRecord::Base.transaction do
26
- assert_equal false, IdentityCache.should_use_cache?
27
- end
28
- end
29
- end
@@ -1,161 +0,0 @@
1
- require "test_helper"
2
-
3
- class IndexCacheTest < IdentityCache::TestCase
4
- NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE
5
-
6
- def setup
7
- super
8
- @record = Item.new
9
- @record.id = 1
10
- @record.title = 'bob'
11
- end
12
-
13
- def test_fetch_with_garbage_input
14
- Item.cache_index :title, :id
15
-
16
- assert_queries(1) do
17
- assert_equal [], Item.fetch_by_title_and_id('garbage', 'garbage')
18
- end
19
- end
20
-
21
- def test_fetch_with_unique_adds_limit_clause
22
- Item.cache_index :title, :id, :unique => true
23
-
24
- Item.connection.expects(:exec_query)
25
- .with(regexp_matches(/ LIMIT 1\Z/i), any_parameters)
26
- .returns(ActiveRecord::Result.new([], []))
27
-
28
- assert_nil Item.fetch_by_title_and_id('title', '2')
29
- end
30
-
31
- def test_unique_index_caches_nil
32
- Item.cache_index :title, :unique => true
33
- assert_nil Item.fetch_by_title('bob')
34
- assert_equal IdentityCache::CACHED_NIL, backend.read(cache_key(unique: true))
35
- end
36
-
37
- def test_unique_index_expired_by_new_record
38
- Item.cache_index :title, :unique => true
39
- IdentityCache.cache.write(cache_key(unique: true), IdentityCache::CACHED_NIL)
40
- @record.save!
41
- assert_equal IdentityCache::DELETED, backend.read(cache_key(unique: true))
42
- end
43
-
44
- def test_unique_index_filled_on_fetch_by
45
- Item.cache_index :title, :unique => true
46
- @record.save!
47
- assert_equal @record, Item.fetch_by_title('bob')
48
- assert_equal @record.id, backend.read(cache_key(unique: true))
49
- end
50
-
51
- def test_unique_index_expired_by_updated_record
52
- Item.cache_index :title, :unique => true
53
- @record.save!
54
- old_cache_key = cache_key(unique: true)
55
- IdentityCache.cache.write(old_cache_key, @record.id)
56
-
57
- @record.title = 'robert'
58
- new_cache_key = cache_key(unique: true)
59
- IdentityCache.cache.write(new_cache_key, IdentityCache::CACHED_NIL)
60
- @record.save!
61
- assert_equal IdentityCache::DELETED, backend.read(old_cache_key)
62
- assert_equal IdentityCache::DELETED, backend.read(new_cache_key)
63
- end
64
-
65
- def test_non_unique_index_caches_empty_result
66
- Item.cache_index :title
67
- assert_equal [], Item.fetch_by_title('bob')
68
- assert_equal [], backend.read(cache_key)
69
- end
70
-
71
- def test_non_unique_index_expired_by_new_record
72
- Item.cache_index :title
73
- IdentityCache.cache.write(cache_key, [])
74
- @record.save!
75
- assert_equal IdentityCache::DELETED, backend.read(cache_key)
76
- end
77
-
78
- def test_non_unique_index_filled_on_fetch_by
79
- Item.cache_index :title
80
- @record.save!
81
- assert_equal [@record], Item.fetch_by_title('bob')
82
- assert_equal [@record.id], backend.read(cache_key)
83
- end
84
-
85
- def test_non_unique_index_fetches_multiple_records
86
- Item.cache_index :title
87
- @record.save!
88
- record2 = Item.create(:title => 'bob') { |item| item.id = 2 }
89
-
90
- assert_equal [@record, record2], Item.fetch_by_title('bob')
91
- assert_equal [1, 2], backend.read(cache_key)
92
- end
93
-
94
- def test_non_unique_index_expired_by_updating_record
95
- Item.cache_index :title
96
- @record.save!
97
- old_cache_key = cache_key
98
- IdentityCache.cache.write(old_cache_key, [@record.id])
99
-
100
- @record.title = 'robert'
101
- new_cache_key = cache_key
102
- IdentityCache.cache.write(new_cache_key, [])
103
- @record.save!
104
- assert_equal IdentityCache::DELETED, backend.read(old_cache_key)
105
- assert_equal IdentityCache::DELETED, backend.read(new_cache_key)
106
- end
107
-
108
- def test_non_unique_index_expired_by_destroying_record
109
- Item.cache_index :title
110
- @record.save!
111
- IdentityCache.cache.write(cache_key, [@record.id])
112
- @record.destroy
113
- assert_equal IdentityCache::DELETED, backend.read(cache_key)
114
- end
115
-
116
- def test_set_table_name_cache_fetch
117
- Item.cache_index :title
118
- Item.table_name = 'items2'
119
- @record.save!
120
- assert_equal [@record], Item.fetch_by_title('bob')
121
- assert_equal [@record.id], backend.read(cache_key)
122
- end
123
-
124
- def test_fetch_by_index_raises_when_called_on_a_scope
125
- Item.cache_index :title
126
- assert_raises(IdentityCache::UnsupportedScopeError) do
127
- Item.where(updated_at: nil).fetch_by_title('bob')
128
- end
129
- end
130
-
131
- def test_fetch_by_unique_index_raises_when_called_on_a_scope
132
- Item.cache_index :title, :id, :unique => true
133
- assert_raises(IdentityCache::UnsupportedScopeError) do
134
- Item.where(updated_at: nil).fetch_by_title_and_id('bob', 2)
135
- end
136
- end
137
-
138
- def test_cache_index_on_derived_model_raises
139
- assert_raises(IdentityCache::DerivedModelError) do
140
- StiRecordTypeA.cache_index :name, :id
141
- end
142
- end
143
-
144
- def test_cache_index_with_non_id_primary_key
145
- KeyedRecord.cache_index :value
146
- KeyedRecord.create!(value: "a") { |r| r.hashed_key = 123 }
147
- assert_equal [123], KeyedRecord.fetch_by_value('a').map(&:id)
148
- end
149
-
150
- def test_unique_cache_index_with_non_id_primary_key
151
- KeyedRecord.cache_index :value, unique: true
152
- KeyedRecord.create!(value: "a") { |r| r.hashed_key = 123 }
153
- assert_equal 123, KeyedRecord.fetch_by_value('a').id
154
- end
155
-
156
- private
157
-
158
- def cache_key(unique: false)
159
- "#{NAMESPACE}attr#{unique ? '' : 's'}:Item:id:title:#{cache_hash(@record.title)}"
160
- end
161
- end
@@ -1,49 +0,0 @@
1
- require "test_helper"
2
-
3
- class MemoizedAttributesTest < IdentityCache::TestCase
4
- def test_memoization_should_not_break_dirty_tracking_with_empty_cache
5
- item = Item.create!
6
-
7
- IdentityCache.cache.with_memoization do
8
- Item.fetch(item.id).title = "my title"
9
- Item.fetch(item.id).update!(title: "my title")
10
- end
11
-
12
- assert_equal "my title", Item.find(item.id).title
13
- end
14
-
15
- def test_memoization_should_not_break_dirty_tracking_with_filled_cache
16
- item = Item.create!
17
-
18
- IdentityCache.cache.with_memoization do
19
- Item.fetch(item.id)
20
- Item.fetch(item.id).title = "my title"
21
- Item.fetch(item.id).update!(title: "my title")
22
- end
23
-
24
- assert_equal "my title", Item.find(item.id).title
25
- end
26
-
27
- def test_memoization_with_fetch_multi_should_not_break_dirty_tracking_with_empty_cache
28
- item = Item.create!
29
-
30
- IdentityCache.cache.with_memoization do
31
- Item.fetch_multi(item.id).first.title = "my title"
32
- Item.fetch_multi(item.id).first.update!(title: "my title")
33
- end
34
-
35
- assert_equal "my title", Item.find(item.id).title
36
- end
37
-
38
- def test_memoization_with_fetch_multi_should_not_break_dirty_tracking_with_filled_cache
39
- item = Item.create!
40
-
41
- IdentityCache.cache.with_memoization do
42
- Item.fetch_multi(item.id)
43
- Item.fetch_multi(item.id).first.title = "my title"
44
- Item.fetch_multi(item.id).first.update!(title: "my title")
45
- end
46
-
47
- assert_equal "my title", Item.find(item.id).title
48
- end
49
- end