identity_cache 0.0.7 → 0.1.0

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