identity_cache 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|