identity_cache 0.0.1
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.
- 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
|