identity_cache 0.0.2 → 0.0.3
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 +7 -0
- data/.travis.yml +1 -0
- data/CHANGELOG +7 -0
- data/README.md +8 -0
- data/Rakefile +19 -0
- data/identity_cache.gemspec +2 -1
- data/lib/{belongs_to_caching.rb → identity_cache/belongs_to_caching.rb} +12 -8
- data/lib/identity_cache/cache_key_generation.rb +58 -0
- data/lib/identity_cache/configuration_dsl.rb +301 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +118 -0
- data/lib/identity_cache/parent_model_expiration.rb +34 -0
- data/lib/identity_cache/query_api.rb +312 -0
- data/lib/identity_cache/version.rb +1 -1
- data/lib/identity_cache.rb +35 -631
- data/performance/cache_runner.rb +123 -0
- data/performance/cpu.rb +28 -0
- data/performance/externals.rb +45 -0
- data/performance/profile.rb +26 -0
- data/test/attribute_cache_test.rb +3 -3
- data/test/fetch_multi_test.rb +13 -39
- data/test/fetch_multi_with_batched_associations_test.rb +236 -0
- data/test/fetch_test.rb +1 -1
- data/test/helpers/active_record_objects.rb +43 -0
- data/test/helpers/cache.rb +3 -12
- data/test/helpers/database_connection.rb +2 -1
- data/test/index_cache_test.rb +7 -0
- data/test/memoized_cache_proxy_test.rb +46 -1
- data/test/normalized_has_many_test.rb +13 -0
- data/test/recursive_denormalized_has_many_test.rb +17 -2
- data/test/save_test.rb +2 -2
- data/test/schema_change_test.rb +8 -28
- data/test/test_helper.rb +49 -43
- metadata +76 -76
- data/lib/memoized_cache_proxy.rb +0 -71
@@ -0,0 +1,123 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
require 'active_support/cache'
|
5
|
+
require 'identity_cache'
|
6
|
+
require 'memcache'
|
7
|
+
|
8
|
+
if ENV['BOXEN_HOME'].present?
|
9
|
+
$memcached_port = 21211
|
10
|
+
$mysql_port = 13306
|
11
|
+
else
|
12
|
+
$memcached_port = 11211
|
13
|
+
$mysql_port = 3306
|
14
|
+
end
|
15
|
+
|
16
|
+
require File.dirname(__FILE__) + '/../test/helpers/active_record_objects'
|
17
|
+
require File.dirname(__FILE__) + '/../test/helpers/database_connection'
|
18
|
+
require File.dirname(__FILE__) + '/../test/helpers/cache'
|
19
|
+
|
20
|
+
def create_record(id)
|
21
|
+
r = Record.new(id)
|
22
|
+
end
|
23
|
+
|
24
|
+
def database_ready(count)
|
25
|
+
Record.where(:id => (1..count)).count == count
|
26
|
+
rescue
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_database(count)
|
31
|
+
DatabaseConnection.setup
|
32
|
+
a = CacheRunner.new(count)
|
33
|
+
|
34
|
+
a.setup_models
|
35
|
+
|
36
|
+
DatabaseConnection.setup
|
37
|
+
# set up associations
|
38
|
+
Record.cache_has_one :associated
|
39
|
+
Record.cache_has_many :associated_records, :embed => true
|
40
|
+
Record.cache_index :title, :unique => :true
|
41
|
+
AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
|
42
|
+
|
43
|
+
return if database_ready(count)
|
44
|
+
puts "Database not ready for performance testing, generating records"
|
45
|
+
|
46
|
+
DatabaseConnection.drop_tables
|
47
|
+
DatabaseConnection.create_tables
|
48
|
+
existing = Record.all
|
49
|
+
(1..count).to_a.each do |i|
|
50
|
+
unless existing.any? { |e| e.id == i }
|
51
|
+
a = Record.new
|
52
|
+
a.id = i
|
53
|
+
a.associated = AssociatedRecord.new(name: "Associated for #{i}")
|
54
|
+
a.associated_records
|
55
|
+
(1..5).each do |j|
|
56
|
+
a.associated_records << AssociatedRecord.new(name: "Has Many #{j} for #{i}")
|
57
|
+
end
|
58
|
+
a.save
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class CacheRunner
|
64
|
+
include ActiveRecordObjects
|
65
|
+
include DatabaseConnection
|
66
|
+
|
67
|
+
def initialize(count)
|
68
|
+
@count = count
|
69
|
+
end
|
70
|
+
|
71
|
+
def prepare
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class FindRunner < CacheRunner
|
76
|
+
def run
|
77
|
+
i = 1
|
78
|
+
@count.times do
|
79
|
+
::Record.find(i)
|
80
|
+
i+=1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class FetchMissRunner < CacheRunner
|
86
|
+
def prepare
|
87
|
+
IdentityCache.cache.clear
|
88
|
+
end
|
89
|
+
|
90
|
+
def run
|
91
|
+
i = 1
|
92
|
+
@count.times do
|
93
|
+
rec = ::Record.fetch(i)
|
94
|
+
rec.fetch_associated
|
95
|
+
rec.fetch_associated_records
|
96
|
+
|
97
|
+
i+=1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class FetchHitRunner < CacheRunner
|
103
|
+
def prepare
|
104
|
+
IdentityCache.cache.clear
|
105
|
+
i = 1
|
106
|
+
@count.times do
|
107
|
+
::Record.fetch(i)
|
108
|
+
i+=1
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def run
|
113
|
+
i = 1
|
114
|
+
@count.times do
|
115
|
+
rec = ::Record.fetch(i)
|
116
|
+
# these should all be no cost
|
117
|
+
rec.fetch_associated
|
118
|
+
rec.fetch_associated_records
|
119
|
+
i+=1
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
data/performance/cpu.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
require_relative 'cache_runner'
|
5
|
+
|
6
|
+
RUNS = 4000
|
7
|
+
|
8
|
+
class ARCreator
|
9
|
+
include ActiveRecordObjects
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(obj, bench)
|
13
|
+
bench.report("#{obj.class.name}:") do
|
14
|
+
obj.prepare
|
15
|
+
obj.run
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
create_database(RUNS)
|
20
|
+
|
21
|
+
Benchmark.bmbm do |x|
|
22
|
+
run(FindRunner.new(RUNS), x)
|
23
|
+
|
24
|
+
run(FetchMissRunner.new(RUNS), x)
|
25
|
+
|
26
|
+
run(FetchHitRunner.new(RUNS), x)
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'benchmark'
|
3
|
+
require 'ruby-prof'
|
4
|
+
|
5
|
+
require_relative 'cache_runner'
|
6
|
+
|
7
|
+
RUNS = 1000
|
8
|
+
RubyProf.measure_mode = RubyProf::CPU_TIME
|
9
|
+
|
10
|
+
EXTERNALS = {"Memcache" => ["MemCache#set", "MemCache#get"],
|
11
|
+
"Database" => ["Mysql2::Client#query"]}
|
12
|
+
|
13
|
+
def run(obj)
|
14
|
+
obj.prepare
|
15
|
+
RubyProf.start
|
16
|
+
obj.run
|
17
|
+
result = RubyProf.stop
|
18
|
+
puts "Results for #{obj.class.name}:"
|
19
|
+
results = StringIO.new
|
20
|
+
printer = RubyProf::FlatPrinter.new(result)
|
21
|
+
printer.print(results)
|
22
|
+
count_externals(results.string)
|
23
|
+
end
|
24
|
+
|
25
|
+
def count_externals(results)
|
26
|
+
count = {}
|
27
|
+
results.split(/\n/).each do |line|
|
28
|
+
fields = line.split
|
29
|
+
if ext = EXTERNALS.detect { |e| e[1].any? { |method| method == fields[-1] } }
|
30
|
+
count[ext[0]] ||= 0
|
31
|
+
count[ext[0]] += fields[-2].to_i
|
32
|
+
end
|
33
|
+
end
|
34
|
+
EXTERNALS.each do |ext|
|
35
|
+
puts "#{ext[0]}: #{count[ext[0]] || 0}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
create_database(RUNS)
|
40
|
+
|
41
|
+
run(FindRunner.new(RUNS))
|
42
|
+
|
43
|
+
run(FetchHitRunner.new(RUNS))
|
44
|
+
|
45
|
+
run(FetchMissRunner.new(RUNS))
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'benchmark'
|
3
|
+
require 'ruby-prof'
|
4
|
+
|
5
|
+
require_relative 'cache_runner'
|
6
|
+
|
7
|
+
RUNS = 1000
|
8
|
+
RubyProf.measure_mode = RubyProf::CPU_TIME
|
9
|
+
|
10
|
+
def run(obj)
|
11
|
+
obj.prepare
|
12
|
+
RubyProf.start
|
13
|
+
obj.run
|
14
|
+
result = RubyProf.stop
|
15
|
+
puts "Results for #{obj.class.name}:"
|
16
|
+
printer = RubyProf::FlatPrinter.new(result)
|
17
|
+
printer.print(STDOUT)
|
18
|
+
end
|
19
|
+
|
20
|
+
create_database(RUNS)
|
21
|
+
|
22
|
+
run(FindRunner.new(RUNS))
|
23
|
+
|
24
|
+
run(FetchMissRunner.new(RUNS))
|
25
|
+
|
26
|
+
run(FetchHitRunner.new(RUNS))
|
@@ -19,7 +19,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
19
19
|
|
20
20
|
def test_attribute_values_are_fetched_and_returned_on_cache_misses
|
21
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')
|
22
|
+
Record.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
|
23
23
|
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
24
24
|
end
|
25
25
|
|
@@ -29,7 +29,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
29
29
|
IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
|
30
30
|
|
31
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')
|
32
|
+
Record.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
|
33
33
|
|
34
34
|
# And write it back to the cache
|
35
35
|
IdentityCache.cache.expects(:write).with(@name_attribute_key, 'foo').returns(nil)
|
@@ -64,7 +64,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
64
64
|
def test_fetching_by_attribute_delegates_to_block_if_transactions_are_open
|
65
65
|
IdentityCache.cache.expects(:read).with(@name_attribute_key).never
|
66
66
|
|
67
|
-
Record.connection.expects(:select_value).with("SELECT name FROM associated_records WHERE id = 1 LIMIT 1").returns('foo')
|
67
|
+
Record.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
|
68
68
|
|
69
69
|
@record.transaction do
|
70
70
|
assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
|
data/test/fetch_multi_test.rb
CHANGED
@@ -6,10 +6,10 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
6
6
|
@bob = Record.create!(:title => 'bob')
|
7
7
|
@joe = Record.create!(:title => 'joe')
|
8
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"
|
9
|
+
@bob_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:1"
|
10
|
+
@joe_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:2"
|
11
|
+
@fred_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:3"
|
12
|
+
@tenth_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:10"
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_fetch_multi_with_no_records
|
@@ -107,48 +107,22 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
107
107
|
assert_equal [@joe, @bob, @joe], Record.fetch_multi(@joe.id, @bob.id, @joe.id)
|
108
108
|
end
|
109
109
|
|
110
|
-
def
|
111
|
-
Record.send(:cache_has_many, :associated_records, :embed => true)
|
112
|
-
Record.send(:cache_has_one, :associated)
|
113
|
-
Record.send(:cache_belongs_to, :record)
|
114
|
-
|
115
|
-
cache_response = {}
|
116
|
-
cache_response[@bob_blob_key] = nil
|
117
|
-
cache_response[@joe_blob_key] = nil
|
118
|
-
cache_response[@fred_blob_key] = nil
|
119
|
-
|
120
|
-
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
121
|
-
|
110
|
+
def test_find_batch_coerces_ids_to_primary_key_type
|
122
111
|
mock_relation = mock("ActiveRecord::Relation")
|
123
112
|
Record.expects(:where).returns(mock_relation)
|
124
|
-
mock_relation.expects(:includes).
|
125
|
-
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
126
|
-
end
|
113
|
+
mock_relation.expects(:includes).returns(stub(:all => [@bob, @joe, @fred]))
|
127
114
|
|
128
|
-
|
129
|
-
|
130
|
-
Record.send(:cache_has_one, :associated)
|
131
|
-
Record.send(:cache_belongs_to, :record)
|
115
|
+
Record.find_batch([@bob, @joe, @fred].map(&:id).map(&:to_s))
|
116
|
+
end
|
132
117
|
|
118
|
+
def test_fetch_multi_doesnt_freeze_keys
|
133
119
|
cache_response = {}
|
134
|
-
cache_response[@bob_blob_key] =
|
135
|
-
cache_response[@joe_blob_key] =
|
136
|
-
cache_response[@fred_blob_key] = nil
|
137
|
-
|
138
|
-
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
139
|
-
|
140
|
-
mock_relation = mock("ActiveRecord::Relation")
|
141
|
-
Record.expects(:where).returns(mock_relation)
|
142
|
-
mock_relation.expects(:includes).with([:associated_records, :associated, {:record => []}]).returns(stub(:all => [@bob, @joe, @fred]))
|
143
|
-
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id, {:includes => :record})
|
144
|
-
end
|
120
|
+
cache_response[@bob_blob_key] = @bob
|
121
|
+
cache_response[@joe_blob_key] = @fred
|
145
122
|
|
146
|
-
|
147
|
-
mock_relation = mock("ActiveRecord::Relation")
|
148
|
-
Record.expects(:where).returns(mock_relation)
|
149
|
-
mock_relation.expects(:includes).returns(stub(:all => [@bob, @joe, @fred]))
|
123
|
+
IdentityCache.expects(:fetch_multi).with{ |*args| args.none?(&:frozen?) }.returns(cache_response)
|
150
124
|
|
151
|
-
Record.
|
125
|
+
Record.fetch_multi(@bob.id, @joe.id)
|
152
126
|
end
|
153
127
|
|
154
128
|
private
|
@@ -0,0 +1,236 @@
|
|
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,record_id:integer,title:string,updated_at:datetime")}:1"
|
10
|
+
@joe_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:2"
|
11
|
+
@fred_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:3"
|
12
|
+
@tenth_blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:10"
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_fetch_multi_includes_cached_associations_in_the_database_find
|
16
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
17
|
+
Record.send(:cache_has_one, :associated)
|
18
|
+
Record.send(:cache_belongs_to, :record)
|
19
|
+
|
20
|
+
cache_response = {}
|
21
|
+
cache_response[@bob_blob_key] = nil
|
22
|
+
cache_response[@joe_blob_key] = nil
|
23
|
+
cache_response[@fred_blob_key] = nil
|
24
|
+
|
25
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
26
|
+
|
27
|
+
mock_relation = mock("ActiveRecord::Relation")
|
28
|
+
Record.expects(:where).returns(mock_relation)
|
29
|
+
mock_relation.expects(:includes).with([:associated_records, :associated]).returns(stub(:all => [@bob, @joe, @fred]))
|
30
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_fetch_multi_includes_cached_associations_and_other_asked_for_associations_in_the_database_find
|
34
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
35
|
+
Record.send(:cache_has_one, :associated)
|
36
|
+
Record.send(:cache_belongs_to, :record)
|
37
|
+
|
38
|
+
cache_response = {}
|
39
|
+
cache_response[@bob_blob_key] = nil
|
40
|
+
cache_response[@joe_blob_key] = nil
|
41
|
+
cache_response[@fred_blob_key] = nil
|
42
|
+
|
43
|
+
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
44
|
+
|
45
|
+
mock_relation = mock("ActiveRecord::Relation")
|
46
|
+
Record.expects(:where).returns(mock_relation)
|
47
|
+
mock_relation.expects(:includes).with([:associated_records, :associated, {:record => []}]).returns(stub(:all => [@bob, @joe, @fred]))
|
48
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id, {:includes => :record})
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_fetch_multi_batch_fetches_non_embedded_first_level_has_many_associations
|
52
|
+
Record.send(:cache_has_many, :associated_records, :embed => false)
|
53
|
+
|
54
|
+
child_records = []
|
55
|
+
[@bob, @joe].each do |parent|
|
56
|
+
3.times do |i|
|
57
|
+
child_records << (child_record = parent.associated_records.create!(:name => i.to_s))
|
58
|
+
AssociatedRecord.fetch(child_record.id)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
Record.fetch_multi(@bob.id, @joe.id) # populate the cache entries and associated children ID variables
|
63
|
+
|
64
|
+
assert_memcache_operations(2) do
|
65
|
+
@cached_bob, @cached_joe = Record.fetch_multi(@bob.id, @joe.id, :includes => :associated_records)
|
66
|
+
assert_equal child_records[0..2].sort, @cached_bob.fetch_associated_records.sort
|
67
|
+
assert_equal child_records[3..5].sort, @cached_joe.fetch_associated_records.sort
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_fetch_multi_batch_fetches_first_level_belongs_to_associations
|
72
|
+
AssociatedRecord.send(:cache_belongs_to, :record, :embed => false)
|
73
|
+
|
74
|
+
@bob_child = @bob.associated_records.create!(:name => "bob child")
|
75
|
+
@fred_child = @fred.associated_records.create!(:name => "fred child")
|
76
|
+
|
77
|
+
# populate the cache entries and associated children ID variables
|
78
|
+
AssociatedRecord.fetch_multi(@bob_child.id, @fred_child.id)
|
79
|
+
Record.fetch_multi(@bob.id, @fred.id)
|
80
|
+
|
81
|
+
assert_memcache_operations(2) do
|
82
|
+
@cached_bob_child, @cached_fred_child = AssociatedRecord.fetch_multi(@bob_child.id, @fred_child.id, :includes => :record)
|
83
|
+
assert_equal @bob, @cached_bob_child.fetch_record
|
84
|
+
assert_equal @fred, @cached_fred_child.fetch_record
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_fetch_multi_batch_fetches_first_level_associations_who_dont_include_identity_cache
|
89
|
+
Record.send(:has_many, :not_cached_records)
|
90
|
+
Record.send(:cache_has_many, :not_cached_records, :embed => true)
|
91
|
+
|
92
|
+
@bob_child = @bob.not_cached_records.create!(:name => "bob child")
|
93
|
+
@fred_child = @fred.not_cached_records.create!(:name => "fred child")
|
94
|
+
|
95
|
+
# populate the cache entries and associated children ID variables
|
96
|
+
Record.fetch_multi(@bob.id, @fred.id)
|
97
|
+
|
98
|
+
assert_memcache_operations(1) do
|
99
|
+
@cached_bob_child, @cached_fred_child = Record.fetch_multi(@bob.id, @fred.id, :includes => :not_cached_records)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_fetch_multi_batch_fetches_non_embedded_second_level_has_many_associations
|
104
|
+
Record.send(:cache_has_many, :associated_records, :embed => false)
|
105
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => false)
|
106
|
+
|
107
|
+
child_records, grandchildren = setup_has_many_children_and_grandchildren(@bob, @joe)
|
108
|
+
|
109
|
+
assert_memcache_operations(3) do
|
110
|
+
@cached_bob, @cached_joe = Record.fetch_multi(@bob.id, @joe.id, :includes => {:associated_records => :deeply_associated_records})
|
111
|
+
bob_children = @cached_bob.fetch_associated_records.sort
|
112
|
+
joe_children = @cached_joe.fetch_associated_records.sort
|
113
|
+
|
114
|
+
assert_equal grandchildren[0..2].sort, bob_children[0].fetch_deeply_associated_records.sort
|
115
|
+
assert_equal grandchildren[3..5].sort, bob_children[1].fetch_deeply_associated_records.sort
|
116
|
+
assert_equal grandchildren[6..8].sort, bob_children[2].fetch_deeply_associated_records.sort
|
117
|
+
assert_equal grandchildren[9..11].sort, joe_children[0].fetch_deeply_associated_records.sort
|
118
|
+
assert_equal grandchildren[12..14].sort, joe_children[1].fetch_deeply_associated_records.sort
|
119
|
+
assert_equal grandchildren[15..17].sort, joe_children[2].fetch_deeply_associated_records.sort
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_fetch_multi_batch_fetches_non_embedded_second_level_belongs_to_associations
|
124
|
+
Record.send(:cache_belongs_to, :record, :embed => false)
|
125
|
+
AssociatedRecord.send(:cache_belongs_to, :record, :embed => false)
|
126
|
+
|
127
|
+
@bob_child = @bob.associated_records.create!(:name => "bob child")
|
128
|
+
@fred_child = @fred.associated_records.create!(:name => "fred child")
|
129
|
+
@bob.update_attribute(:record_id, @bob.id)
|
130
|
+
@fred.update_attribute(:record_id, @fred.id)
|
131
|
+
|
132
|
+
# populate the cache entries and associated children ID variables
|
133
|
+
AssociatedRecord.fetch_multi(@bob_child.id, @fred_child.id)
|
134
|
+
Record.fetch_multi(@bob.id, @fred.id)
|
135
|
+
|
136
|
+
assert_memcache_operations(3) do
|
137
|
+
@cached_bob_child, @cached_fred_child = AssociatedRecord.fetch_multi(@bob_child.id, @fred_child.id, :includes => {:record => :record})
|
138
|
+
|
139
|
+
@cached_bob_parent = @cached_bob_child.fetch_record
|
140
|
+
@cached_fred_parent = @cached_fred_child.fetch_record
|
141
|
+
assert_equal @bob, @cached_bob_parent.fetch_record
|
142
|
+
assert_equal @fred, @cached_fred_parent.fetch_record
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_fetch_multi_doesnt_batch_fetches_belongs_to_associations_if_the_foreign_key_isnt_present
|
147
|
+
AssociatedRecord.send(:cache_belongs_to, :record, :embed => false)
|
148
|
+
@child = AssociatedRecord.create!(:name => "bob child")
|
149
|
+
# populate the cache entry
|
150
|
+
AssociatedRecord.fetch_multi(@child.id)
|
151
|
+
|
152
|
+
assert_memcache_operations(1) do
|
153
|
+
@cached_child = AssociatedRecord.fetch_multi(@child.id, :includes => :record)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def test_fetch_multi_batch_fetches_non_embedded_second_level_associations_through_embedded_first_level_has_many_associations
|
159
|
+
Record.send(:cache_has_many, :associated_records, :embed => true)
|
160
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => false)
|
161
|
+
|
162
|
+
child_records, grandchildren = setup_has_many_children_and_grandchildren(@bob, @joe)
|
163
|
+
|
164
|
+
assert_memcache_operations(2) do
|
165
|
+
@cached_bob, @cached_joe = Record.fetch_multi(@bob.id, @joe.id, :includes => {:associated_records => :deeply_associated_records})
|
166
|
+
bob_children = @cached_bob.fetch_associated_records.sort
|
167
|
+
joe_children = @cached_joe.fetch_associated_records.sort
|
168
|
+
|
169
|
+
assert_equal grandchildren[0..2].sort, bob_children[0].fetch_deeply_associated_records.sort
|
170
|
+
assert_equal grandchildren[3..5].sort, bob_children[1].fetch_deeply_associated_records.sort
|
171
|
+
assert_equal grandchildren[6..8].sort, bob_children[2].fetch_deeply_associated_records.sort
|
172
|
+
assert_equal grandchildren[9..11].sort, joe_children[0].fetch_deeply_associated_records.sort
|
173
|
+
assert_equal grandchildren[12..14].sort, joe_children[1].fetch_deeply_associated_records.sort
|
174
|
+
assert_equal grandchildren[15..17].sort, joe_children[2].fetch_deeply_associated_records.sort
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_fetch_multi_batch_fetches_non_embedded_second_level_associations_through_embedded_first_level_has_one_associations
|
179
|
+
Record.send(:cache_has_one, :associated, :embed => true)
|
180
|
+
AssociatedRecord.send(:cache_has_many, :deeply_associated_records, :embed => false)
|
181
|
+
|
182
|
+
@bob_child = @bob.create_associated!(:name => "bob child")
|
183
|
+
@joe_child = @joe.create_associated!(:name => "joe child")
|
184
|
+
|
185
|
+
grandchildren = setup_grandchildren(@bob_child, @joe_child)
|
186
|
+
AssociatedRecord.fetch_multi(@bob_child.id, @joe_child.id)
|
187
|
+
Record.fetch_multi(@bob.id, @joe.id)
|
188
|
+
|
189
|
+
assert_memcache_operations(2) do
|
190
|
+
@cached_bob, @cached_joe = Record.fetch_multi(@bob.id, @joe.id, :includes => {:associated => :deeply_associated_records})
|
191
|
+
bob_child = @cached_bob.fetch_associated
|
192
|
+
joe_child = @cached_joe.fetch_associated
|
193
|
+
|
194
|
+
assert_equal grandchildren[0..2].sort, bob_child.fetch_deeply_associated_records.sort
|
195
|
+
assert_equal grandchildren[3..5].sort, joe_child.fetch_deeply_associated_records.sort
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def test_find_batch_coerces_ids_to_primary_key_type
|
200
|
+
mock_relation = mock("ActiveRecord::Relation")
|
201
|
+
Record.expects(:where).returns(mock_relation)
|
202
|
+
mock_relation.expects(:includes).returns(stub(:all => [@bob, @joe, @fred]))
|
203
|
+
|
204
|
+
Record.find_batch([@bob, @joe, @fred].map(&:id).map(&:to_s))
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def setup_has_many_children_and_grandchildren(*parents)
|
210
|
+
child_records = []
|
211
|
+
grandchildren = []
|
212
|
+
|
213
|
+
parents.each do |parent|
|
214
|
+
3.times do |i|
|
215
|
+
child_records << (child = parent.associated_records.create!(:name => i.to_s))
|
216
|
+
grandchildren.concat setup_grandchildren(child)
|
217
|
+
AssociatedRecord.fetch(child.id)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
Record.fetch_multi(*parents.map(&:id)) # populate the cache entries and associated children ID variables
|
222
|
+
|
223
|
+
return child_records, grandchildren
|
224
|
+
end
|
225
|
+
|
226
|
+
def setup_grandchildren(*children)
|
227
|
+
grandchildren = []
|
228
|
+
children.each do |child|
|
229
|
+
3.times do |j|
|
230
|
+
grandchildren << (grandchild = child.deeply_associated_records.create!(:name => j.to_s))
|
231
|
+
DeeplyAssociatedRecord.fetch(grandchild.id)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
grandchildren
|
235
|
+
end
|
236
|
+
end
|
data/test/fetch_test.rb
CHANGED
@@ -9,7 +9,7 @@ class FetchTest < IdentityCache::TestCase
|
|
9
9
|
@record = Record.new
|
10
10
|
@record.id = 1
|
11
11
|
@record.title = 'bob'
|
12
|
-
@blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,title:string,updated_at:datetime")}:1"
|
12
|
+
@blob_key = "IDC:blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:1"
|
13
13
|
@index_key = "IDC:index:Record:title:#{cache_hash('bob')}"
|
14
14
|
end
|
15
15
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveRecordObjects
|
2
|
+
def setup_models(base = ActiveRecord::Base)
|
3
|
+
Object.send :const_set, 'DeeplyAssociatedRecord', Class.new(base).tap {|klass|
|
4
|
+
klass.send :include, IdentityCache
|
5
|
+
klass.belongs_to :associated_record
|
6
|
+
}
|
7
|
+
|
8
|
+
Object.send :const_set, 'AssociatedRecord', Class.new(base).tap {|klass|
|
9
|
+
klass.send :include, IdentityCache
|
10
|
+
klass.belongs_to :record
|
11
|
+
klass.has_many :deeply_associated_records, :order => "name DESC"
|
12
|
+
}
|
13
|
+
|
14
|
+
Object.send :const_set, 'NotCachedRecord', Class.new(base).tap {|klass|
|
15
|
+
klass.belongs_to :record, :touch => true
|
16
|
+
}
|
17
|
+
|
18
|
+
Object.send :const_set, 'PolymorphicRecord', Class.new(base).tap {|klass|
|
19
|
+
klass.belongs_to :owner, :polymorphic => true
|
20
|
+
}
|
21
|
+
|
22
|
+
Object.send :const_set, 'Record', Class.new(base).tap {|klass|
|
23
|
+
klass.send :include, IdentityCache
|
24
|
+
klass.belongs_to :record
|
25
|
+
klass.has_many :associated_records, :order => "id DESC"
|
26
|
+
klass.has_many :not_cached_records, :order => "id DESC"
|
27
|
+
klass.has_many :polymorphic_records, :as => 'owner'
|
28
|
+
klass.has_one :polymorphic_record, :as => 'owner'
|
29
|
+
klass.has_one :associated, :class_name => 'AssociatedRecord', :order => "id ASC"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def teardown_models
|
34
|
+
ActiveSupport::DescendantsTracker.clear
|
35
|
+
ActiveSupport::Dependencies.clear
|
36
|
+
Object.send :remove_const, 'DeeplyAssociatedRecord'
|
37
|
+
Object.send :remove_const, 'PolymorphicRecord'
|
38
|
+
Object.send :remove_const, 'AssociatedRecord'
|
39
|
+
Object.send :remove_const, 'NotCachedRecord'
|
40
|
+
Object.send :remove_const, 'Record'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
data/test/helpers/cache.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
1
3
|
module Rails
|
2
4
|
|
3
5
|
class Cache < ActiveSupport::Cache::MemCacheStore
|
@@ -7,18 +9,7 @@ module Rails
|
|
7
9
|
@@cache ||= Cache.new("localhost:#{$memcached_port}")
|
8
10
|
end
|
9
11
|
|
10
|
-
class Logger
|
11
|
-
def info(string)
|
12
|
-
end
|
13
|
-
|
14
|
-
def debug(string)
|
15
|
-
end
|
16
|
-
|
17
|
-
def error(string)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
12
|
def self.logger
|
22
|
-
@logger
|
13
|
+
@logger ||= Logger.new(nil)
|
23
14
|
end
|
24
15
|
end
|
@@ -30,7 +30,8 @@ module DatabaseConnection
|
|
30
30
|
:deeply_associated_records => [[:string, :name], [:integer, :associated_record_id]],
|
31
31
|
:associated_records => [[:string, :name], [:integer, :record_id]],
|
32
32
|
:not_cached_records => [[:string, :name], [:integer, :record_id]],
|
33
|
-
:records => [[:string, :title], [:timestamps]]
|
33
|
+
:records => [[:integer, :record_id], [:string, :title], [:timestamps]],
|
34
|
+
:records2 => [[:integer, :record_id], [:string, :title], [:timestamps]]
|
34
35
|
}
|
35
36
|
|
36
37
|
DATABASE_CONFIG = {
|
data/test/index_cache_test.rb
CHANGED
@@ -93,4 +93,11 @@ class ExpirationTest < IdentityCache::TestCase
|
|
93
93
|
assert_equal nil, IdentityCache.cache.read(@cache_key)
|
94
94
|
end
|
95
95
|
|
96
|
+
def test_set_table_name_cache_fetch
|
97
|
+
Record.cache_index :title
|
98
|
+
Record.table_name = 'records2'
|
99
|
+
@record.save!
|
100
|
+
assert_equal [@record], Record.fetch_by_title('bob')
|
101
|
+
assert_equal [@record.id], IdentityCache.cache.read(@cache_key)
|
102
|
+
end
|
96
103
|
end
|