identity_cache 0.0.7 → 0.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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +3 -2
- data/CHANGELOG +13 -1
- data/Gemfile +2 -2
- data/{Gemfile.rails4 → Gemfile.rails41} +2 -2
- data/Gemfile32 +7 -0
- data/README.md +3 -4
- data/identity_cache.gemspec +3 -2
- data/lib/identity_cache/belongs_to_caching.rb +1 -1
- data/lib/identity_cache/cache_hash.rb +2 -2
- data/lib/identity_cache/cache_invalidation.rb +26 -0
- data/lib/identity_cache/cache_key_generation.rb +3 -2
- data/lib/identity_cache/configuration_dsl.rb +38 -32
- data/lib/identity_cache/query_api.rb +52 -52
- data/lib/identity_cache/version.rb +1 -1
- data/lib/identity_cache.rb +20 -8
- data/performance/cache_runner.rb +7 -4
- data/performance/profile.rb +13 -3
- data/test/attribute_cache_test.rb +13 -4
- data/test/cache_fetch_includes_test.rb +1 -44
- data/test/cache_invalidation_test.rb +52 -0
- data/test/denormalized_has_many_test.rb +4 -4
- data/test/fetch_multi_test.rb +54 -0
- data/test/fetch_multi_with_batched_associations_test.rb +13 -35
- data/test/fetch_test.rb +20 -6
- data/test/fixtures/serialized_record +0 -0
- data/test/helpers/active_record_objects.rb +8 -2
- data/test/helpers/database_connection.rb +8 -5
- data/test/helpers/serialization_format.rb +9 -5
- data/test/helpers/update_serialization_format.rb +1 -0
- data/test/index_cache_test.rb +21 -1
- data/test/memoized_cache_proxy_test.rb +9 -17
- data/test/normalized_belongs_to_test.rb +2 -0
- data/test/normalized_has_many_test.rb +4 -4
- data/test/recursive_denormalized_has_many_test.rb +1 -1
- data/test/schema_change_test.rb +2 -2
- data/test/test_helper.rb +7 -3
- metadata +26 -17
- data/test/helpers/cache.rb +0 -15
data/lib/identity_cache.rb
CHANGED
@@ -10,9 +10,11 @@ require 'identity_cache/configuration_dsl'
|
|
10
10
|
require 'identity_cache/parent_model_expiration'
|
11
11
|
require 'identity_cache/query_api'
|
12
12
|
require "identity_cache/cache_hash"
|
13
|
+
require "identity_cache/cache_invalidation"
|
13
14
|
|
14
15
|
module IdentityCache
|
15
16
|
CACHED_NIL = :idc_cached_nil
|
17
|
+
BATCH_SIZE = 1000
|
16
18
|
|
17
19
|
class AlreadyIncludedError < StandardError; end
|
18
20
|
class InverseAssociationError < StandardError
|
@@ -38,6 +40,7 @@ module IdentityCache
|
|
38
40
|
base.send(:include, IdentityCache::CacheKeyGeneration)
|
39
41
|
base.send(:include, IdentityCache::ConfigurationDSL)
|
40
42
|
base.send(:include, IdentityCache::QueryAPI)
|
43
|
+
base.send(:include, IdentityCache::CacheInvalidation)
|
41
44
|
end
|
42
45
|
|
43
46
|
# Sets the cache adaptor IdentityCache will be using
|
@@ -73,7 +76,7 @@ module IdentityCache
|
|
73
76
|
# == Parameters
|
74
77
|
# +key+ A cache key string
|
75
78
|
#
|
76
|
-
def fetch(key
|
79
|
+
def fetch(key)
|
77
80
|
result = cache.read(key) if should_cache?
|
78
81
|
|
79
82
|
if result.nil?
|
@@ -99,16 +102,17 @@ module IdentityCache
|
|
99
102
|
end
|
100
103
|
|
101
104
|
# Same as +fetch+, except that it will try a collection of keys, using the
|
102
|
-
# multiget operation of the cache adaptor
|
105
|
+
# multiget operation of the cache adaptor.
|
103
106
|
#
|
104
107
|
# == Parameters
|
105
|
-
# +keys+ A collection of key strings
|
106
|
-
def fetch_multi(*keys
|
108
|
+
# +keys+ A collection or array of key strings
|
109
|
+
def fetch_multi(*keys)
|
110
|
+
keys.flatten!(1)
|
107
111
|
return {} if keys.size == 0
|
108
112
|
result = {}
|
109
|
-
result =
|
113
|
+
result = read_in_batches(keys) if should_cache?
|
110
114
|
|
111
|
-
hit_keys = result.
|
115
|
+
hit_keys = result.reject {|key, value| value == nil }.keys
|
112
116
|
missed_keys = keys - hit_keys
|
113
117
|
|
114
118
|
if missed_keys.size > 0
|
@@ -127,11 +131,19 @@ module IdentityCache
|
|
127
131
|
end
|
128
132
|
|
129
133
|
|
130
|
-
result.
|
131
|
-
result[key] = unmap_cached_nil_for(
|
134
|
+
result.each do |key, value|
|
135
|
+
result[key] = unmap_cached_nil_for(value)
|
132
136
|
end
|
133
137
|
|
134
138
|
result
|
135
139
|
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def read_in_batches(keys)
|
144
|
+
keys.each_slice(BATCH_SIZE).each_with_object Hash.new do |slice, result|
|
145
|
+
result.merge! cache.read_multi(*slice)
|
146
|
+
end
|
147
|
+
end
|
136
148
|
end
|
137
149
|
end
|
data/performance/cache_runner.rb
CHANGED
@@ -16,7 +16,10 @@ end
|
|
16
16
|
|
17
17
|
require File.dirname(__FILE__) + '/../test/helpers/active_record_objects'
|
18
18
|
require File.dirname(__FILE__) + '/../test/helpers/database_connection'
|
19
|
-
|
19
|
+
|
20
|
+
IdentityCache.logger = Logger.new(nil)
|
21
|
+
IdentityCache.cache_backend = ActiveSupport::Cache::MemcachedStore.new("localhost:#{$memcached_port}")
|
22
|
+
|
20
23
|
|
21
24
|
def create_record(id)
|
22
25
|
Item.new(id)
|
@@ -85,7 +88,7 @@ CACHE_RUNNERS = []
|
|
85
88
|
class FindRunner < CacheRunner
|
86
89
|
def run
|
87
90
|
(1..@count).each do |i|
|
88
|
-
::Item.
|
91
|
+
::Item.includes(:associated, {:associated_records => :deeply_associated_records}).find(i)
|
89
92
|
end
|
90
93
|
end
|
91
94
|
end
|
@@ -137,8 +140,8 @@ class NormalizedRunner < CacheRunner
|
|
137
140
|
def setup_models
|
138
141
|
super
|
139
142
|
Item.cache_has_one :associated # :embed => false isn't supported
|
140
|
-
Item.cache_has_many :associated_records, :embed =>
|
141
|
-
AssociatedRecord.cache_has_many :deeply_associated_records, :embed =>
|
143
|
+
Item.cache_has_many :associated_records, :embed => :ids
|
144
|
+
AssociatedRecord.cache_has_many :deeply_associated_records, :embed => :ids
|
142
145
|
end
|
143
146
|
|
144
147
|
def run
|
data/performance/profile.rb
CHANGED
@@ -6,13 +6,14 @@ require_relative 'cache_runner'
|
|
6
6
|
|
7
7
|
RUNS = 1000
|
8
8
|
|
9
|
-
def run(obj)
|
9
|
+
def run(obj, filename: nil)
|
10
10
|
puts "#{obj.class.name}:"
|
11
11
|
obj.prepare
|
12
12
|
data = StackProf.run(mode: :cpu) do
|
13
13
|
obj.run
|
14
14
|
end
|
15
15
|
StackProf::Report.new(data).print_text(false, 20)
|
16
|
+
File.write(filename, Marshal.dump(data)) if filename
|
16
17
|
puts
|
17
18
|
ensure
|
18
19
|
obj.cleanup
|
@@ -20,6 +21,15 @@ end
|
|
20
21
|
|
21
22
|
create_database(RUNS)
|
22
23
|
|
23
|
-
|
24
|
-
|
24
|
+
if runner_name = ENV['RUNNER']
|
25
|
+
if runner = CACHE_RUNNERS.find{ |r| r.name == runner_name }
|
26
|
+
run(runner.new(RUNS), filename: ENV['FILENAME'])
|
27
|
+
else
|
28
|
+
puts "Couldn't find cache runner #{runner_name.inspect}"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
else
|
32
|
+
CACHE_RUNNERS.each do |runner|
|
33
|
+
run(runner.new(RUNS))
|
34
|
+
end
|
25
35
|
end
|
@@ -20,7 +20,10 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
20
20
|
|
21
21
|
def test_attribute_values_are_fetched_and_returned_on_cache_misses
|
22
22
|
IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
|
23
|
-
Item.connection.expects(:
|
23
|
+
Item.connection.expects(:exec_query)
|
24
|
+
.with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
|
25
|
+
.returns(ActiveRecord::Result.new(['name'], [['foo']]))
|
26
|
+
|
24
27
|
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
25
28
|
end
|
26
29
|
|
@@ -29,7 +32,9 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
29
32
|
IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
|
30
33
|
|
31
34
|
# Grab the value of the attribute from the DB
|
32
|
-
Item.connection.expects(:
|
35
|
+
Item.connection.expects(:exec_query)
|
36
|
+
.with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
|
37
|
+
.returns(ActiveRecord::Result.new(['name'], [['foo']]))
|
33
38
|
|
34
39
|
# And write it back to the cache
|
35
40
|
IdentityCache.cache.expects(:write).with(@name_attribute_key, 'foo').returns(nil)
|
@@ -42,7 +47,9 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
42
47
|
IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
|
43
48
|
|
44
49
|
# Grab the value of the attribute from the DB
|
45
|
-
Item.connection.expects(:
|
50
|
+
Item.connection.expects(:exec_query)
|
51
|
+
.with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
|
52
|
+
.returns(ActiveRecord::Result.new(['name'], []))
|
46
53
|
|
47
54
|
# And write it back to the cache
|
48
55
|
IdentityCache.cache.expects(:write).with(@name_attribute_key, IdentityCache::CACHED_NIL).returns(nil)
|
@@ -81,7 +88,9 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
81
88
|
def test_fetching_by_attribute_delegates_to_block_if_transactions_are_open
|
82
89
|
IdentityCache.cache.expects(:read).with(@name_attribute_key).never
|
83
90
|
|
84
|
-
Item.connection.expects(:
|
91
|
+
Item.connection.expects(:exec_query)
|
92
|
+
.with('SELECT `associated_records`.`name` FROM `associated_records` WHERE `associated_records`.`id` = 1 LIMIT 1', anything)
|
93
|
+
.returns(ActiveRecord::Result.new(['name'], [['foo']]))
|
85
94
|
|
86
95
|
@record.transaction do
|
87
96
|
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
@@ -11,7 +11,7 @@ class CacheFetchIncludesTest < IdentityCache::TestCase
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def test_cached_nonembedded_has_manys_are_included_in_includes
|
14
|
-
Item.send(:cache_has_many, :associated_records, :embed =>
|
14
|
+
Item.send(:cache_has_many, :associated_records, :embed => :ids)
|
15
15
|
assert_equal [], Item.send(:cache_fetch_includes)
|
16
16
|
end
|
17
17
|
|
@@ -42,47 +42,4 @@ class CacheFetchIncludesTest < IdentityCache::TestCase
|
|
42
42
|
{:associated => [:deeply_associated_records]}
|
43
43
|
], Item.send(:cache_fetch_includes)
|
44
44
|
end
|
45
|
-
|
46
|
-
def test_empty_additions_for_top_level_associations_makes_no_difference
|
47
|
-
Item.send(:cache_has_many, :associated_records, :embed => true)
|
48
|
-
assert_equal [:associated_records], Item.send(:cache_fetch_includes, {})
|
49
|
-
end
|
50
|
-
|
51
|
-
def test_top_level_additions_are_included_in_includes
|
52
|
-
assert_equal [{:associated_records => []}], Item.send(:cache_fetch_includes, {:associated_records => []})
|
53
|
-
end
|
54
|
-
|
55
|
-
def test_top_level_additions_alongside_top_level_cached_associations_are_included_in_includes
|
56
|
-
Item.send(:cache_has_many, :associated_records, :embed => true)
|
57
|
-
assert_equal [
|
58
|
-
:associated_records,
|
59
|
-
{:polymorphic_records => []}
|
60
|
-
], Item.send(:cache_fetch_includes, {:polymorphic_records => []})
|
61
|
-
end
|
62
|
-
|
63
|
-
def test_child_level_additions_for_top_level_cached_associations_are_included_in_includes
|
64
|
-
Item.send(:cache_has_many, :associated_records, :embed => true)
|
65
|
-
assert_equal [
|
66
|
-
{:associated_records => [{:deeply_associated_records => []}]}
|
67
|
-
], Item.send(:cache_fetch_includes, {:associated_records => :deeply_associated_records})
|
68
|
-
end
|
69
|
-
|
70
|
-
def test_array_child_level_additions_for_top_level_cached_associations_are_included_in_includes
|
71
|
-
Item.send(:cache_has_many, :associated_records, :embed => true)
|
72
|
-
assert_equal [
|
73
|
-
{:associated_records => [{:deeply_associated_records => []}]}
|
74
|
-
], Item.send(:cache_fetch_includes, {:associated_records => [:deeply_associated_records]})
|
75
|
-
end
|
76
|
-
|
77
|
-
def test_array_child_level_additions_for_child_level_cached_associations_are_included_in_includes
|
78
|
-
Item.send(:cache_has_many, :associated_records, :embed => true)
|
79
|
-
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => true)
|
80
|
-
assert_equal [
|
81
|
-
{:associated_records => [
|
82
|
-
:deeply_associated_records,
|
83
|
-
{:record => []}
|
84
|
-
]}
|
85
|
-
], Item.send(:cache_fetch_includes, {:associated_records => [:record]})
|
86
|
-
end
|
87
|
-
|
88
45
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class CacheInvalidationTest < IdentityCache::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
Item.cache_has_many :associated_records, :embed => :ids
|
7
|
+
|
8
|
+
@record = Item.new(:title => 'foo')
|
9
|
+
@record.associated_records << AssociatedRecord.new(:name => 'bar')
|
10
|
+
@record.associated_records << AssociatedRecord.new(:name => 'baz')
|
11
|
+
@record.save
|
12
|
+
@record.reload
|
13
|
+
@baz, @bar = @record.associated_records[0], @record.associated_records[1]
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_reload_invalidate_cached_ids
|
17
|
+
variable_name = "@#{@record.class.send(:embedded_associations)[:associated_records][:ids_variable_name]}"
|
18
|
+
|
19
|
+
@record.fetch_associated_record_ids
|
20
|
+
assert_equal [@baz.id, @bar.id], @record.instance_variable_get(variable_name)
|
21
|
+
|
22
|
+
@record.reload
|
23
|
+
assert_equal nil, @record.instance_variable_get(variable_name)
|
24
|
+
|
25
|
+
@record.fetch_associated_record_ids
|
26
|
+
assert_equal [@baz.id, @bar.id], @record.instance_variable_get(variable_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_reload_invalidate_cached_objects
|
30
|
+
variable_name = "@#{@record.class.send(:embedded_associations)[:associated_records][:records_variable_name]}"
|
31
|
+
|
32
|
+
@record.fetch_associated_records
|
33
|
+
assert_equal [@baz, @bar], @record.instance_variable_get(variable_name)
|
34
|
+
|
35
|
+
@record.reload
|
36
|
+
assert_equal nil, @record.instance_variable_get(variable_name)
|
37
|
+
|
38
|
+
@record.fetch_associated_records
|
39
|
+
assert_equal [@baz, @bar], @record.instance_variable_get(variable_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_after_a_reload_the_cache_perform_as_expected
|
43
|
+
assert_equal [@baz, @bar], @record.associated_records
|
44
|
+
assert_equal [@baz, @bar], @record.fetch_associated_records
|
45
|
+
|
46
|
+
@baz.destroy
|
47
|
+
@record.reload
|
48
|
+
|
49
|
+
assert_equal [@bar], @record.associated_records
|
50
|
+
assert_equal [@bar], @record.fetch_associated_records
|
51
|
+
end
|
52
|
+
end
|
@@ -54,13 +54,13 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
|
|
54
54
|
def test_changes_in_associated_records_foreign_keys_should_expire_new_parent_and_old_parents_cache
|
55
55
|
@associatated_record = @record.associated_records.first
|
56
56
|
old_key = @record.primary_cache_index_key
|
57
|
-
@new_record = Item.create
|
57
|
+
@new_record = Item.create!
|
58
58
|
new_key = @new_record.primary_cache_index_key
|
59
59
|
|
60
60
|
IdentityCache.cache.expects(:delete).with(@associatated_record.primary_cache_index_key)
|
61
61
|
IdentityCache.cache.expects(:delete).with(old_key)
|
62
62
|
IdentityCache.cache.expects(:delete).with(new_key)
|
63
|
-
@associatated_record.
|
63
|
+
@associatated_record.item = @new_record
|
64
64
|
@associatated_record.save!
|
65
65
|
end
|
66
66
|
|
@@ -82,10 +82,10 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
|
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
|
-
def
|
85
|
+
def test_saving_associated_records_should_expire_itself_and_the_parents_cache
|
86
86
|
child = @record.associated_records.first
|
87
87
|
IdentityCache.cache.expects(:delete).with(child.primary_cache_index_key).once
|
88
88
|
IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
|
89
|
-
child.
|
89
|
+
child.save!
|
90
90
|
end
|
91
91
|
end
|
data/test/fetch_multi_test.rb
CHANGED
@@ -100,6 +100,18 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
100
100
|
assert_equal fetch_result, results
|
101
101
|
end
|
102
102
|
|
103
|
+
def test_fetch_multi_works_with_blanks
|
104
|
+
cache_result = {1 => false, 2 => ' '}
|
105
|
+
|
106
|
+
IdentityCache.cache.expects(:read_multi).with(1,2).returns(cache_result)
|
107
|
+
|
108
|
+
results = IdentityCache.fetch_multi(1,2) do |keys|
|
109
|
+
flunk "Contents should have been fetched from cache successfully"
|
110
|
+
end
|
111
|
+
|
112
|
+
assert_equal cache_result, results
|
113
|
+
end
|
114
|
+
|
103
115
|
def test_fetch_multi_duplicate_ids
|
104
116
|
assert_equal [@joe, @bob, @joe], Item.fetch_multi(@joe.id, @bob.id, @joe.id)
|
105
117
|
end
|
@@ -115,6 +127,11 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
115
127
|
assert_equal [@joe, @bob, @fred], Item.fetch_multi(@joe.id, @bob.id, @fred.id)
|
116
128
|
end
|
117
129
|
|
130
|
+
def test_fetch_multi_with_open_transactions_should_compacts_returned_array
|
131
|
+
Item.connection.expects(:open_transactions).at_least_once.returns(1)
|
132
|
+
assert_equal [@joe, @fred], Item.fetch_multi(@joe.id, 0, @fred.id)
|
133
|
+
end
|
134
|
+
|
118
135
|
def test_fetch_multi_with_duplicate_ids_in_transaction_returns_results_in_the_order_of_the_passed_ids
|
119
136
|
Item.connection.expects(:open_transactions).at_least_once.returns(1)
|
120
137
|
assert_equal [@joe, @bob, @joe], Item.fetch_multi(@joe.id, @bob.id, @joe.id)
|
@@ -138,6 +155,33 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
138
155
|
Item.fetch_multi(@bob.id, @joe.id)
|
139
156
|
end
|
140
157
|
|
158
|
+
def test_fetch_multi_array
|
159
|
+
assert_equal [@joe, @bob], Item.fetch_multi([@joe.id, @bob.id])
|
160
|
+
end
|
161
|
+
|
162
|
+
def test_fetch_multi_reads_in_batches
|
163
|
+
cache_response = {}
|
164
|
+
cache_response[@bob_blob_key] = cache_response_for(@bob)
|
165
|
+
cache_response[@joe_blob_key] = cache_response_for(@joe)
|
166
|
+
|
167
|
+
with_batch_size 1 do
|
168
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key).returns(cache_response).once
|
169
|
+
IdentityCache.cache.expects(:read_multi).with(@joe_blob_key).returns(cache_response).once
|
170
|
+
assert_equal [@bob, @joe], Item.fetch_multi(@bob.id, @joe.id)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_fetch_multi_max_stack_level
|
175
|
+
cache_response = { @fred_blob_key => cache_response_for(@fred) }
|
176
|
+
IdentityCache.cache.stubs(:read_multi).returns(cache_response)
|
177
|
+
assert_nothing_raised { Item.fetch_multi([@fred.id] * 200_000) }
|
178
|
+
end
|
179
|
+
|
180
|
+
def test_fetch_multi_with_non_id_primary_key
|
181
|
+
fixture = KeyedRecord.create!(:value => "a") { |r| r.hashed_key = 123 }
|
182
|
+
assert_equal [fixture], KeyedRecord.fetch_multi(123, 456)
|
183
|
+
end
|
184
|
+
|
141
185
|
private
|
142
186
|
|
143
187
|
def populate_only_fred
|
@@ -153,4 +197,14 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
153
197
|
record.encode_with(coder)
|
154
198
|
coder
|
155
199
|
end
|
200
|
+
|
201
|
+
def with_batch_size(size)
|
202
|
+
previous_batch_size = IdentityCache::BATCH_SIZE
|
203
|
+
IdentityCache.send(:remove_const, :BATCH_SIZE)
|
204
|
+
IdentityCache.const_set(:BATCH_SIZE, size)
|
205
|
+
yield
|
206
|
+
ensure
|
207
|
+
IdentityCache.send(:remove_const, :BATCH_SIZE)
|
208
|
+
IdentityCache.const_set(:BATCH_SIZE, previous_batch_size)
|
209
|
+
end
|
156
210
|
end
|
@@ -14,44 +14,22 @@ class FetchMultiWithBatchedAssociationsTest < IdentityCache::TestCase
|
|
14
14
|
@tenth_blob_key = "#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:10"
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
18
|
-
Item.send(:cache_has_many, :associated_records, :embed => true)
|
19
|
-
Item.send(:cache_has_one, :associated)
|
20
|
-
Item.send(:cache_belongs_to, :item)
|
21
|
-
|
22
|
-
cache_response = {}
|
23
|
-
cache_response[@bob_blob_key] = nil
|
24
|
-
cache_response[@joe_blob_key] = nil
|
25
|
-
cache_response[@fred_blob_key] = nil
|
26
|
-
|
27
|
-
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
28
|
-
|
29
|
-
mock_relation = mock("ActiveRecord::Relation")
|
30
|
-
Item.expects(:where).returns(mock_relation)
|
31
|
-
mock_relation.expects(:includes).with([:associated_records, :associated]).returns(stub(:to_a => [@bob, @joe, @fred]))
|
32
|
-
assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id)
|
33
|
-
end
|
34
|
-
|
35
|
-
def test_fetch_multi_includes_cached_associations_and_other_asked_for_associations_in_the_database_find
|
36
|
-
Item.send(:cache_has_many, :associated_records, :embed => true)
|
37
|
-
Item.send(:cache_has_one, :associated)
|
17
|
+
def test_fetch_multi_with_includes_option_preloads_associations
|
38
18
|
Item.send(:cache_belongs_to, :item)
|
19
|
+
john = Item.create!(:title => 'john')
|
20
|
+
jim = Item.create!(:title => 'jim')
|
21
|
+
@bob.update_column(:item_id, john)
|
22
|
+
@joe.update_column(:item_id, jim)
|
39
23
|
|
40
|
-
|
41
|
-
cache_response[@bob_blob_key] = nil
|
42
|
-
cache_response[@joe_blob_key] = nil
|
43
|
-
cache_response[@fred_blob_key] = nil
|
24
|
+
spy = Spy.on(Item, :fetch_multi).and_call_through
|
44
25
|
|
45
|
-
|
26
|
+
items = Item.fetch_multi(@bob.id, @joe.id, @fred.id, :includes => :item)
|
46
27
|
|
47
|
-
|
48
|
-
Item.expects(:where).returns(mock_relation)
|
49
|
-
mock_relation.expects(:includes).with([:associated_records, :associated, {:item => []}]).returns(stub(:to_a => [@bob, @joe, @fred]))
|
50
|
-
assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id, {:includes => :item})
|
28
|
+
assert spy.calls.one?{ |call| call.args == [[john.id, jim.id]] }
|
51
29
|
end
|
52
30
|
|
53
31
|
def test_fetch_multi_batch_fetches_non_embedded_first_level_has_many_associations
|
54
|
-
Item.send(:cache_has_many, :associated_records, :embed =>
|
32
|
+
Item.send(:cache_has_many, :associated_records, :embed => :ids)
|
55
33
|
|
56
34
|
child_records = []
|
57
35
|
[@bob, @joe].each do |parent|
|
@@ -103,8 +81,8 @@ class FetchMultiWithBatchedAssociationsTest < IdentityCache::TestCase
|
|
103
81
|
end
|
104
82
|
|
105
83
|
def test_fetch_multi_batch_fetches_non_embedded_second_level_has_many_associations
|
106
|
-
Item.send(:cache_has_many, :associated_records, :embed =>
|
107
|
-
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed =>
|
84
|
+
Item.send(:cache_has_many, :associated_records, :embed => :ids)
|
85
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => :ids)
|
108
86
|
|
109
87
|
child_records, grandchildren = setup_has_many_children_and_grandchildren(@bob, @joe)
|
110
88
|
|
@@ -159,7 +137,7 @@ class FetchMultiWithBatchedAssociationsTest < IdentityCache::TestCase
|
|
159
137
|
|
160
138
|
def test_fetch_multi_batch_fetches_non_embedded_second_level_associations_through_embedded_first_level_has_many_associations
|
161
139
|
Item.send(:cache_has_many, :associated_records, :embed => true)
|
162
|
-
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed =>
|
140
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => :ids)
|
163
141
|
|
164
142
|
child_records, grandchildren = setup_has_many_children_and_grandchildren(@bob, @joe)
|
165
143
|
|
@@ -179,7 +157,7 @@ class FetchMultiWithBatchedAssociationsTest < IdentityCache::TestCase
|
|
179
157
|
|
180
158
|
def test_fetch_multi_batch_fetches_non_embedded_second_level_associations_through_embedded_first_level_has_one_associations
|
181
159
|
Item.send(:cache_has_one, :associated, :embed => true)
|
182
|
-
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed =>
|
160
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => :ids)
|
183
161
|
|
184
162
|
@bob_child = @bob.create_associated!(:name => "bob child")
|
185
163
|
@joe_child = @joe.create_associated!(:name => "joe child")
|
data/test/fetch_test.rb
CHANGED
@@ -17,6 +17,14 @@ class FetchTest < IdentityCache::TestCase
|
|
17
17
|
@index_key = "#{NAMESPACE}index:Item:title:#{cache_hash('bob')}"
|
18
18
|
end
|
19
19
|
|
20
|
+
def test_fetch_with_garbage_input
|
21
|
+
Item.connection.expects(:exec_query)
|
22
|
+
.with('SELECT `items`.* FROM `items` WHERE `items`.`id` = 0 LIMIT 1', anything)
|
23
|
+
.returns(ActiveRecord::Result.new([], []))
|
24
|
+
|
25
|
+
assert_equal nil, Item.fetch_by_id('garbage')
|
26
|
+
end
|
27
|
+
|
20
28
|
def test_fetch_cache_hit
|
21
29
|
IdentityCache.cache.expects(:read).with(@blob_key).returns(@cached_value)
|
22
30
|
|
@@ -64,6 +72,12 @@ class FetchTest < IdentityCache::TestCase
|
|
64
72
|
assert_equal @record, Item.fetch(1)
|
65
73
|
end
|
66
74
|
|
75
|
+
def test_fetch_miss_with_non_id_primary_key
|
76
|
+
hashed_key = Zlib::crc32("foo") % (2 ** 30 - 1)
|
77
|
+
fixture = KeyedRecord.create!(:value => "foo") { |r| r.hashed_key = hashed_key }
|
78
|
+
assert_equal fixture, KeyedRecord.fetch(hashed_key)
|
79
|
+
end
|
80
|
+
|
67
81
|
def test_fetch_by_id_not_found_should_return_nil
|
68
82
|
nonexistent_record_id = 10
|
69
83
|
IdentityCache.cache.expects(:write).with(@blob_key + '0', IdentityCache::CACHED_NIL)
|
@@ -93,7 +107,7 @@ class FetchTest < IdentityCache::TestCase
|
|
93
107
|
IdentityCache.cache.expects(:read).with(@index_key).returns(nil)
|
94
108
|
|
95
109
|
# - not found, use sql, SELECT id FROM records WHERE title = '...' LIMIT 1"
|
96
|
-
Item.connection.expects(:
|
110
|
+
Item.connection.expects(:exec_query).returns(ActiveRecord::Result.new(['id'], [[1]]))
|
97
111
|
|
98
112
|
# cache sql result
|
99
113
|
IdentityCache.cache.expects(:write).with(@index_key, 1)
|
@@ -113,17 +127,17 @@ class FetchTest < IdentityCache::TestCase
|
|
113
127
|
end
|
114
128
|
|
115
129
|
def test_fetch_by_title_stores_idcnil
|
116
|
-
Item.connection.expects(:
|
117
|
-
|
118
|
-
|
119
|
-
assert_equal nil, Item.fetch_by_title('bob') #
|
130
|
+
Item.connection.expects(:exec_query).once.returns(ActiveRecord::Result.new([], []))
|
131
|
+
IdentityCache.cache.expects(:write).with(@index_key, IdentityCache::CACHED_NIL)
|
132
|
+
IdentityCache.cache.expects(:read).with(@index_key).times(3).returns(nil, IdentityCache::CACHED_NIL, IdentityCache::CACHED_NIL)
|
133
|
+
assert_equal nil, Item.fetch_by_title('bob') # exec_query => nil
|
120
134
|
|
121
135
|
assert_equal nil, Item.fetch_by_title('bob') # returns cached nil
|
122
136
|
assert_equal nil, Item.fetch_by_title('bob') # returns cached nil
|
123
137
|
end
|
124
138
|
|
125
139
|
def test_fetch_by_bang_method
|
126
|
-
Item.connection.expects(:
|
140
|
+
Item.connection.expects(:exec_query).returns(ActiveRecord::Result.new([], []))
|
127
141
|
assert_raises ActiveRecord::RecordNotFound do
|
128
142
|
Item.fetch_by_title!('bob')
|
129
143
|
end
|
Binary file
|
@@ -26,7 +26,7 @@ module ActiveRecordObjects
|
|
26
26
|
|
27
27
|
Object.send :const_set, 'AssociatedRecord', Class.new(base) {
|
28
28
|
include IdentityCache
|
29
|
-
belongs_to :item
|
29
|
+
belongs_to :item, inverse_of: :associated_records
|
30
30
|
has_many :deeply_associated_records
|
31
31
|
default_scope { order('id DESC') }
|
32
32
|
}
|
@@ -49,13 +49,18 @@ module ActiveRecordObjects
|
|
49
49
|
Object.send :const_set, 'Item', Class.new(base) {
|
50
50
|
include IdentityCache
|
51
51
|
belongs_to :item
|
52
|
-
has_many :associated_records
|
52
|
+
has_many :associated_records, inverse_of: :item
|
53
53
|
has_many :normalized_associated_records
|
54
54
|
has_many :not_cached_records
|
55
55
|
has_many :polymorphic_records, :as => 'owner'
|
56
56
|
has_one :polymorphic_record, :as => 'owner'
|
57
57
|
has_one :associated, :class_name => 'AssociatedRecord'
|
58
58
|
}
|
59
|
+
|
60
|
+
Object.send :const_set, 'KeyedRecord', Class.new(base) {
|
61
|
+
include IdentityCache
|
62
|
+
self.primary_key = "hashed_key"
|
63
|
+
}
|
59
64
|
end
|
60
65
|
|
61
66
|
def teardown_models
|
@@ -67,5 +72,6 @@ module ActiveRecordObjects
|
|
67
72
|
Object.send :remove_const, 'AssociatedRecord'
|
68
73
|
Object.send :remove_const, 'NotCachedRecord'
|
69
74
|
Object.send :remove_const, 'Item'
|
75
|
+
Object.send :remove_const, 'KeyedRecord'
|
70
76
|
end
|
71
77
|
end
|
@@ -17,7 +17,9 @@ module DatabaseConnection
|
|
17
17
|
|
18
18
|
def self.create_tables
|
19
19
|
TABLES.each do |table, fields|
|
20
|
-
|
20
|
+
fields = fields.dup
|
21
|
+
options = fields.last.is_a?(Hash) ? fields.pop : {}
|
22
|
+
ActiveRecord::Base.connection.create_table(table, options) do |t|
|
21
23
|
fields.each do |column_type, *args|
|
22
24
|
t.send(column_type, *args)
|
23
25
|
end
|
@@ -27,12 +29,13 @@ module DatabaseConnection
|
|
27
29
|
|
28
30
|
TABLES = {
|
29
31
|
:polymorphic_records => [[:string, :owner_type], [:integer, :owner_id], [:timestamps]],
|
30
|
-
:deeply_associated_records => [[:string, :name], [:integer, :associated_record_id]],
|
32
|
+
:deeply_associated_records => [[:string, :name], [:integer, :associated_record_id], [:timestamps]],
|
31
33
|
:associated_records => [[:string, :name], [:integer, :item_id]],
|
32
|
-
:normalized_associated_records => [[:string, :name], [:integer, :item_id]],
|
33
|
-
:not_cached_records => [[:string, :name], [:integer, :item_id]],
|
34
|
+
:normalized_associated_records => [[:string, :name], [:integer, :item_id], [:timestamps]],
|
35
|
+
:not_cached_records => [[:string, :name], [:integer, :item_id], [:timestamps]],
|
34
36
|
:items => [[:integer, :item_id], [:string, :title], [:timestamps]],
|
35
|
-
:items2 => [[:integer, :item_id], [:string, :title], [:timestamps]]
|
37
|
+
:items2 => [[:integer, :item_id], [:string, :title], [:timestamps]],
|
38
|
+
:keyed_records => [[:string, :value], :primary_key => "hashed_key"],
|
36
39
|
}
|
37
40
|
|
38
41
|
DATABASE_CONFIG = {
|
@@ -4,17 +4,20 @@ module SerializationFormat
|
|
4
4
|
AssociatedRecord.cache_belongs_to :item, :embed => false
|
5
5
|
Item.cache_has_many :associated_records, :embed => true
|
6
6
|
Item.cache_has_one :associated
|
7
|
+
time = Time.parse('1970-01-01T00:00:00 UTC')
|
7
8
|
|
8
9
|
record = Item.new(:title => 'foo')
|
9
10
|
record.associated_records << AssociatedRecord.new(:name => 'bar')
|
10
11
|
record.associated_records << AssociatedRecord.new(:name => 'baz')
|
11
12
|
record.associated = AssociatedRecord.new(:name => 'bork')
|
12
|
-
record.not_cached_records << NotCachedRecord.new(:name => 'NoCache')
|
13
|
-
record.associated.deeply_associated_records << DeeplyAssociatedRecord.new(:name => "corge")
|
14
|
-
record.associated.deeply_associated_records << DeeplyAssociatedRecord.new(:name => "qux")
|
15
|
-
record.created_at =
|
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
|
16
17
|
record.save
|
17
|
-
Item
|
18
|
+
[Item, NotCachedRecord, DeeplyAssociatedRecord].each do |model|
|
19
|
+
model.update_all(updated_at: time)
|
20
|
+
end
|
18
21
|
record.reload
|
19
22
|
Item.fetch(record.id)
|
20
23
|
IdentityCache.fetch(record.primary_cache_index_key)
|
@@ -29,6 +32,7 @@ module SerializationFormat
|
|
29
32
|
:version => IdentityCache::CACHE_VERSION,
|
30
33
|
:record => record
|
31
34
|
}
|
35
|
+
|
32
36
|
if anIO
|
33
37
|
Marshal.dump(hash, anIO)
|
34
38
|
else
|