identity_cache 0.0.7 → 0.1.0

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.
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'