identity_cache 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +231 -0
- data/Rakefile +16 -0
- data/identity_cache.gemspec +24 -0
- data/lib/belongs_to_caching.rb +39 -0
- data/lib/identity_cache.rb +570 -0
- data/lib/identity_cache/version.rb +3 -0
- data/lib/memoized_cache_proxy.rb +71 -0
- data/test/attribute_cache_test.rb +73 -0
- data/test/denormalized_has_many_test.rb +89 -0
- data/test/denormalized_has_one_test.rb +99 -0
- data/test/fetch_multi_test.rb +144 -0
- data/test/fetch_test.rb +108 -0
- data/test/helpers/cache.rb +60 -0
- data/test/helpers/database_connection.rb +41 -0
- data/test/identity_cache_test.rb +17 -0
- data/test/index_cache_test.rb +96 -0
- data/test/memoized_cache_proxy_test.rb +60 -0
- data/test/normalized_belongs_to_test.rb +46 -0
- data/test/normalized_has_many_test.rb +125 -0
- data/test/recursive_denormalized_has_many_test.rb +97 -0
- data/test/save_test.rb +64 -0
- data/test/test_helper.rb +73 -0
- metadata +154 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
class MemoizedCacheProxy
|
5
|
+
attr_writer :memcache
|
6
|
+
|
7
|
+
def initialize(memcache = nil)
|
8
|
+
@memcache = memcache || Rails.cache
|
9
|
+
@key_value_maps = Hash.new {|h, k| h[k] = {} }
|
10
|
+
end
|
11
|
+
|
12
|
+
def memoized_key_values
|
13
|
+
@key_value_maps[Thread.current.object_id]
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_memoization(&block)
|
17
|
+
Thread.current[:memoizing_idc] = true
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
clear_memoization
|
21
|
+
Thread.current[:memoizing_idc] = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(key, value)
|
25
|
+
memoized_key_values[key] = value if memoizing?
|
26
|
+
@memcache.write(key, value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def read(key)
|
30
|
+
if memoizing?
|
31
|
+
memoized_key_values[key] ||= @memcache.read(key)
|
32
|
+
else
|
33
|
+
@memcache.read(key)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete(key)
|
38
|
+
memoized_key_values.delete(key) if memoizing?
|
39
|
+
@memcache.delete(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_multi(*keys)
|
43
|
+
hash = {}
|
44
|
+
|
45
|
+
if memoizing?
|
46
|
+
keys.reduce({}) do |hash, key|
|
47
|
+
hash[key] = memoized_key_values[key] if memoized_key_values[key].present?
|
48
|
+
hash
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
missing_keys = keys - hash.keys
|
53
|
+
hash.merge(@memcache.read_multi(*missing_keys))
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear
|
57
|
+
clear_memoization
|
58
|
+
@memcache.clear
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def clear_memoization
|
64
|
+
@key_value_maps.delete(Thread.current.object_id)
|
65
|
+
end
|
66
|
+
|
67
|
+
def memoizing?
|
68
|
+
Thread.current[:memoizing_idc]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class AttributeCacheTest < IdentityCache::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
AssociatedRecord.cache_attribute :name
|
7
|
+
AssociatedRecord.cache_attribute :record, :by => [:id, :name]
|
8
|
+
|
9
|
+
@parent = Record.create!(:title => 'bob')
|
10
|
+
@record = @parent.associated_records.create!(:name => 'foo')
|
11
|
+
@name_attribute_key = "IDC:attribute:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s)}"
|
12
|
+
@blob_key = "IDC:blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:1"
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_attribute_values_are_returned_on_cache_hits
|
16
|
+
IdentityCache.cache.expects(:read).with(@name_attribute_key).returns('foo')
|
17
|
+
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_attribute_values_are_fetched_and_returned_on_cache_misses
|
21
|
+
IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
|
22
|
+
Record.connection.expects(:select_value).with("SELECT name FROM associated_records WHERE id = 1 LIMIT 1").returns('foo')
|
23
|
+
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_attribute_values_are_stored_in_the_cache_on_cache_misses
|
27
|
+
|
28
|
+
# Cache miss, so
|
29
|
+
IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
|
30
|
+
|
31
|
+
# Grab the value of the attribute from the DB
|
32
|
+
Record.connection.expects(:select_value).with("SELECT name FROM associated_records WHERE id = 1 LIMIT 1").returns('foo')
|
33
|
+
|
34
|
+
# And write it back to the cache
|
35
|
+
IdentityCache.cache.expects(:write).with(@name_attribute_key, 'foo').returns(nil)
|
36
|
+
|
37
|
+
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_saved
|
41
|
+
IdentityCache.cache.expects(:delete).with(@name_attribute_key)
|
42
|
+
IdentityCache.cache.expects(:delete).with(@blob_key)
|
43
|
+
@record.save!
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_with_changed_attributes_is_saved
|
47
|
+
IdentityCache.cache.expects(:delete).with(@name_attribute_key)
|
48
|
+
IdentityCache.cache.expects(:delete).with(@blob_key)
|
49
|
+
@record.name = 'bar'
|
50
|
+
@record.save!
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_destroyed
|
54
|
+
IdentityCache.cache.expects(:delete).with(@name_attribute_key)
|
55
|
+
IdentityCache.cache.expects(:delete).with(@blob_key)
|
56
|
+
@record.destroy
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_cached_attribute_values_are_expired_from_the_cache_when_a_new_record_is_saved
|
60
|
+
IdentityCache.cache.expects(:delete).with("IDC:blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:2")
|
61
|
+
@parent.associated_records.create(:name => 'bar')
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_fetching_by_attribute_delegates_to_block_if_transactions_are_open
|
65
|
+
IdentityCache.cache.expects(:read).with(@name_attribute_key).never
|
66
|
+
|
67
|
+
Record.connection.expects(:select_value).with("SELECT name FROM associated_records WHERE id = 1 LIMIT 1").returns('foo')
|
68
|
+
|
69
|
+
@record.transaction do
|
70
|
+
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class DenormalizedHasManyTest < IdentityCache::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
Record.cache_has_many :associated_records, :embed => true
|
7
|
+
|
8
|
+
@record = Record.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
|
+
end
|
14
|
+
|
15
|
+
def test_uncached_record_from_the_db_will_use_normal_association
|
16
|
+
expected = @record.associated_records
|
17
|
+
record_from_db = Record.find(@record.id)
|
18
|
+
|
19
|
+
Record.any_instance.expects(:associated_records).returns(expected)
|
20
|
+
|
21
|
+
assert_equal @record, record_from_db
|
22
|
+
assert_equal expected, record_from_db.fetch_associated_records
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_on_cache_hit_record_should_come_back_with_cached_association
|
26
|
+
Record.fetch(@record.id) # warm cache
|
27
|
+
|
28
|
+
record_from_cache_hit = Record.fetch(@record.id)
|
29
|
+
assert_equal @record, record_from_cache_hit
|
30
|
+
|
31
|
+
expected = @record.associated_records
|
32
|
+
Record.any_instance.expects(:associated_records).never
|
33
|
+
assert_equal expected, record_from_cache_hit.fetch_associated_records
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_on_cache_miss_record_should_embed_associated_objects_and_return
|
37
|
+
record_from_cache_miss = Record.fetch(@record.id)
|
38
|
+
expected = @record.associated_records
|
39
|
+
|
40
|
+
assert_equal @record, record_from_cache_miss
|
41
|
+
assert_equal expected, record_from_cache_miss.fetch_associated_records
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_changes_in_associated_records_should_expire_the_parents_cache
|
45
|
+
Record.fetch(@record.id)
|
46
|
+
key = @record.primary_cache_index_key
|
47
|
+
assert_not_nil IdentityCache.cache.read(key)
|
48
|
+
|
49
|
+
IdentityCache.cache.expects(:delete).with(key)
|
50
|
+
@record.associated_records.first.save
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_changes_in_associated_records_foreign_keys_should_expire_new_parent_and_old_parents_cache
|
54
|
+
@associatated_record = @record.associated_records.first
|
55
|
+
old_key = @record.primary_cache_index_key
|
56
|
+
@new_record = Record.create
|
57
|
+
new_key = @new_record.primary_cache_index_key
|
58
|
+
|
59
|
+
IdentityCache.cache.expects(:delete).with(old_key)
|
60
|
+
IdentityCache.cache.expects(:delete).with(new_key)
|
61
|
+
@associatated_record.record_id = @new_record.id
|
62
|
+
@associatated_record.save!
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_cached_associations_after_commit_hook_will_not_fail_on_undefined_parent_association
|
66
|
+
ar = AssociatedRecord.new
|
67
|
+
ar.save
|
68
|
+
assert_nothing_raised { ar.expire_parent_cache }
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_cache_without_guessable_inverse_name_raises
|
72
|
+
assert_raises IdentityCache::InverseAssociationError do
|
73
|
+
Record.cache_has_many :polymorphic_records, :embed => true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_cache_without_guessable_inverse_name_does_not_raise_when_inverse_name_specified
|
78
|
+
assert_nothing_raised do
|
79
|
+
Record.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_touching_associated_records_should_expire_itself_and_the_parents_cache
|
84
|
+
child = @record.associated_records.first
|
85
|
+
IdentityCache.cache.expects(:delete).with(child.primary_cache_index_key).once
|
86
|
+
IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
|
87
|
+
child.touch
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class DenormalizedHasOneTest < IdentityCache::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
Record.cache_has_one :associated
|
7
|
+
Record.cache_index :title, :unique => true
|
8
|
+
@record = Record.new(:title => 'foo')
|
9
|
+
@record.associated = AssociatedRecord.new(:name => 'bar')
|
10
|
+
@record.save
|
11
|
+
|
12
|
+
@record.reload
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_on_cache_miss_record_should_embed_associated_object
|
16
|
+
IdentityCache.cache.expects(:read).with(@record.secondary_cache_index_key_for_current_values([:title]))
|
17
|
+
IdentityCache.cache.expects(:read).with(@record.primary_cache_index_key)
|
18
|
+
|
19
|
+
record_from_cache_miss = Record.fetch_by_title('foo')
|
20
|
+
|
21
|
+
assert_equal @record, record_from_cache_miss
|
22
|
+
assert_not_nil @record.fetch_associated
|
23
|
+
assert_equal @record.associated, record_from_cache_miss.fetch_associated
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_on_cache_miss_record_should_embed_nil_object
|
27
|
+
@record.expects(:associated => nil)
|
28
|
+
Record.expects(:find_by_id).with(@record.id, :include => Record.cache_fetch_includes).returns(@record)
|
29
|
+
IdentityCache.cache.expects(:read).with(@record.secondary_cache_index_key_for_current_values([:title]))
|
30
|
+
IdentityCache.cache.expects(:read).with(@record.primary_cache_index_key)
|
31
|
+
|
32
|
+
record_from_cache_miss = Record.fetch_by_title('foo')
|
33
|
+
record_from_cache_miss.expects(:associated).never
|
34
|
+
|
35
|
+
assert_equal @record, record_from_cache_miss
|
36
|
+
5.times do
|
37
|
+
assert_nil record_from_cache_miss.fetch_associated
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_on_record_from_the_db_will_use_normal_association
|
42
|
+
record_from_db = Record.find_by_title('foo')
|
43
|
+
|
44
|
+
assert_equal @record, record_from_db
|
45
|
+
assert_not_nil record_from_db.fetch_associated
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_on_cache_hit_record_should_come_back_with_cached_association
|
49
|
+
Record.expects(:find_by_id).with(1, :include => Record.cache_fetch_includes).once.returns(@record)
|
50
|
+
Record.fetch_by_title('foo')
|
51
|
+
|
52
|
+
record_from_cache_hit = Record.fetch_by_title('foo')
|
53
|
+
expected = @record.associated
|
54
|
+
|
55
|
+
assert_equal @record, record_from_cache_hit
|
56
|
+
assert_equal expected, record_from_cache_hit.fetch_associated
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_on_cache_hit_record_should_come_back_with_cached_nil_association
|
60
|
+
@record.expects(:associated => nil)
|
61
|
+
Record.expects(:find_by_id).with(1, :include => Record.cache_fetch_includes).once.returns(@record)
|
62
|
+
Record.fetch_by_title('foo')
|
63
|
+
|
64
|
+
record_from_cache_hit = Record.fetch_by_title('foo')
|
65
|
+
record_from_cache_hit.expects(:associated).never
|
66
|
+
|
67
|
+
assert_equal @record, record_from_cache_hit
|
68
|
+
5.times do
|
69
|
+
assert_nil record_from_cache_hit.fetch_associated
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_changes_in_associated_record_should_expire_the_parents_cache
|
74
|
+
Record.fetch_by_title('foo')
|
75
|
+
key = @record.primary_cache_index_key
|
76
|
+
assert_not_nil IdentityCache.cache.read(key)
|
77
|
+
|
78
|
+
IdentityCache.cache.expects(:delete).at_least(1).with(key)
|
79
|
+
@record.associated.save
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_cached_associations_after_commit_hook_will_not_fail_on_undefined_parent_association
|
83
|
+
ar = AssociatedRecord.new
|
84
|
+
ar.save
|
85
|
+
assert_nothing_raised { ar.expire_parent_cache }
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_cache_without_guessable_inverse_name_raises
|
89
|
+
assert_raises IdentityCache::InverseAssociationError do
|
90
|
+
Record.cache_has_one :polymorphic_record, :embed => true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_cache_without_guessable_inverse_name_does_not_raise_when_inverse_name_specified
|
95
|
+
assert_nothing_raised do
|
96
|
+
Record.cache_has_one :polymorphic_record, :inverse_name => :owner, :embed => true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class FetchMultiTest < IdentityCache::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
@bob = Record.create!(:title => 'bob')
|
7
|
+
@joe = Record.create!(:title => 'joe')
|
8
|
+
@fred = Record.create!(:title => 'fred')
|
9
|
+
@bob_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:1"
|
10
|
+
@joe_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:2"
|
11
|
+
@fred_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:3"
|
12
|
+
@tenth_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:10"
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_fetch_multi_with_no_records
|
16
|
+
assert_equal [], Record.fetch_multi
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_fetch_multi_with_all_hits
|
20
|
+
cache_response = {}
|
21
|
+
cache_response[@bob_blob_key] = @bob
|
22
|
+
cache_response[@joe_blob_key] = @joe
|
23
|
+
cache_response[@fred_blob_key] = @fred
|
24
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
25
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_fetch_multi_with_all_misses
|
29
|
+
cache_response = {}
|
30
|
+
cache_response[@bob_blob_key] = nil
|
31
|
+
cache_response[@joe_blob_key] = nil
|
32
|
+
cache_response[@fred_blob_key] = nil
|
33
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
34
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_fetch_multi_with_mixed_hits_and_misses
|
38
|
+
cache_response = {}
|
39
|
+
cache_response[@bob_blob_key] = @bob
|
40
|
+
cache_response[@joe_blob_key] = nil
|
41
|
+
cache_response[@fred_blob_key] = @fred
|
42
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
43
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_fetch_multi_with_mixed_hits_and_misses_and_responses_in_the_wrong_order
|
47
|
+
cache_response = {}
|
48
|
+
cache_response[@bob_blob_key] = nil
|
49
|
+
cache_response[@joe_blob_key] = nil
|
50
|
+
cache_response[@fred_blob_key] = @fred
|
51
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @fred_blob_key, @joe_blob_key).returns(cache_response)
|
52
|
+
assert_equal [@bob, @fred, @joe], Record.fetch_multi(@bob.id, @fred.id, @joe.id)
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_fetch_multi_with_mixed_hits_and_misses_and_non_existant_keys_1
|
56
|
+
populate_only_fred
|
57
|
+
|
58
|
+
IdentityCache.cache.expects(:read_multi).with(@tenth_blob_key, @bob_blob_key, @joe_blob_key, @fred_blob_key).returns(@cache_response)
|
59
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(10, @bob.id, @joe.id, @fred.id)
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_fetch_multi_with_mixed_hits_and_misses_and_non_existant_keys_2
|
63
|
+
populate_only_fred
|
64
|
+
|
65
|
+
IdentityCache.cache.expects(:read_multi).with(@fred_blob_key, @bob_blob_key, @tenth_blob_key, @joe_blob_key).returns(@cache_response)
|
66
|
+
assert_equal [@fred, @bob, @joe], Record.fetch_multi(@fred.id, @bob.id, 10, @joe.id)
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def test_fetch_multi_works_with_nils
|
71
|
+
cache_result = {1 => IdentityCache::CACHED_NIL, 2 => IdentityCache::CACHED_NIL}
|
72
|
+
fetch_result = {1 => nil, 2 => nil}
|
73
|
+
|
74
|
+
IdentityCache.cache.expects(:read_multi).with(1,2).times(2).returns({1 => nil, 2 => nil}, cache_result)
|
75
|
+
IdentityCache.cache.expects(:write).with(1, IdentityCache::CACHED_NIL).once
|
76
|
+
IdentityCache.cache.expects(:write).with(2, IdentityCache::CACHED_NIL).once
|
77
|
+
|
78
|
+
results = IdentityCache.fetch_multi(1,2) do |keys|
|
79
|
+
[nil, nil]
|
80
|
+
end
|
81
|
+
assert_equal fetch_result, results
|
82
|
+
|
83
|
+
results = IdentityCache.fetch_multi(1,2) do |keys|
|
84
|
+
flunk "Contents should have been fetched from cache successfully"
|
85
|
+
end
|
86
|
+
|
87
|
+
assert_equal fetch_result, results
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_fetch_multi_duplicate_ids
|
91
|
+
assert_equal [@joe, @bob, @joe], Record.fetch_multi(@joe.id, @bob.id, @joe.id)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_fetch_multi_with_open_transactions_hits_the_database
|
95
|
+
Record.connection.expects(:open_transactions).at_least_once.returns(1)
|
96
|
+
IdentityCache.cache.expects(:read_multi).never
|
97
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_fetch_multi_with_open_transactions_returns_results_in_the_order_of_the_passed_ids
|
101
|
+
Record.connection.expects(:open_transactions).at_least_once.returns(1)
|
102
|
+
assert_equal [@joe, @bob, @fred], Record.fetch_multi(@joe.id, @bob.id, @fred.id)
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_fetch_multi_with_duplicate_ids_in_transaction_returns_results_in_the_order_of_the_passed_ids
|
106
|
+
Record.connection.expects(:open_transactions).at_least_once.returns(1)
|
107
|
+
assert_equal [@joe, @bob, @joe], Record.fetch_multi(@joe.id, @bob.id, @joe.id)
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_fetch_multi_includes_cached_associations
|
111
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
112
|
+
Record.send(:cache_has_one, :associated)
|
113
|
+
|
114
|
+
cache_response = {}
|
115
|
+
cache_response[@bob_blob_key] = nil
|
116
|
+
cache_response[@joe_blob_key] = nil
|
117
|
+
cache_response[@fred_blob_key] = nil
|
118
|
+
|
119
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
120
|
+
|
121
|
+
mock_relation = mock("ActiveRecord::Relation")
|
122
|
+
Record.expects(:where).returns(mock_relation)
|
123
|
+
mock_relation.expects(:includes).with([:associated_records, :associated]).returns(stub(:all => [@bob, @joe, @fred]))
|
124
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_find_batch_coerces_ids_to_primary_key_type
|
128
|
+
mock_relation = mock("ActiveRecord::Relation")
|
129
|
+
Record.expects(:where).returns(mock_relation)
|
130
|
+
mock_relation.expects(:includes).returns(stub(:all => [@bob, @joe, @fred]))
|
131
|
+
|
132
|
+
Record.find_batch([@bob, @joe, @fred].map(&:id).map(&:to_s))
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def populate_only_fred
|
138
|
+
@cache_response = {}
|
139
|
+
@cache_response[@bob_blob_key] = nil
|
140
|
+
@cache_response[@joe_blob_key] = nil
|
141
|
+
@cache_response[@tenth_blob_key] = nil
|
142
|
+
@cache_response[@fred_blob_key] = @fred
|
143
|
+
end
|
144
|
+
end
|