identity_cache 0.0.4 → 0.0.5

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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- YmQyZjllMTVlOTNiMmUxMDkxMzJlNzYzYjY0ODZlMmQ4Y2E0YTNkNA==
5
- data.tar.gz: !binary |-
6
- OGQ2MDc5Zjk2NmY3MTcwMmIxNDkzYjczNTZjMmJhNjMwMWI1ZjJjYg==
2
+ SHA1:
3
+ metadata.gz: 2692fa11451bb0243fcb3450d6f6c2550110afea
4
+ data.tar.gz: dbd4fa2555037a5ddd90e36f2aea9ae03fff6907
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MmM4NmJmMTY2MDRkNDdkYzRjMDg3NTQ4OTRlMzgyOWJiYzY4Y2RjMDQxZDQ0
10
- NDA2NzYwZGIyN2UzMjVkNGU1MTI1ZTViNDU4YThkNTAxNGYyY2UxMWU5Y2U4
11
- MzhjN2JjMDkyMWVmYjhkMWRmMmIyMmVmZjI4NzJkYTExZGZiYmM=
12
- data.tar.gz: !binary |-
13
- Yjg4MTdkOGI0OTJmY2ZkMWEwMDY0NjViMDE2MjdiMDg4NTkzZTc0MmU1YjEy
14
- NzI1Nzc0ZjQ0ODk1YWY0M2ExMDdjNWNjMjQ5OGY3Y2YyMTcyNzQ3MjA3OTY3
15
- YWMxMTlkZTgxNjQ0OTgyYmY3YzY5OWYzY2QyY2NlZjZkNzUwYTQ=
6
+ metadata.gz: 61e4b2e382f4a3706a5851ab2d8268bac77941a318b1ef81ad4babc4439b45a3e6c361a123e8d0bfeec2e136f8b0b8815825d1163a5d2a4c28f92c2bb11024d1
7
+ data.tar.gz: 830d6ef767b8b62167bd7f016966d56f6c1d1bf99c501c75293d4731cd61f58683396f622a654190c39e91ab7fd5e6b6cdad5051ead943306ffff27ad86399c1
data/.gitignore CHANGED
@@ -3,7 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
6
+ *.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
9
  coverage
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.0
data/.travis.yml CHANGED
@@ -2,12 +2,16 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
- - jruby-19mode
5
+ - 2.1.0
6
+ - jruby
7
+ gemfile:
8
+ - Gemfile
9
+ - Gemfile.rails4
6
10
  services:
7
11
  - memcache
8
12
  - mysql
9
13
  matrix:
10
14
  allow_failures:
11
- - rvm: jruby-19mode
15
+ - rvm: jruby
12
16
  before_script:
13
17
  - mysql -e 'create database identity_cache_test'
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in identity_cache.gemspec
4
4
  gemspec
5
+
6
+ gem 'activerecord', '~> 3.2.16'
7
+ gem 'activesupport', '~> 3.2.16'
data/Gemfile.rails4 ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in identity_cache.gemspec
4
+ gemspec
5
+
6
+ gem 'activerecord', '~> 4.0.1'
7
+ gem 'activesupport', '~> 4.0.1'
8
+ gem 'dalli'
data/README.md CHANGED
@@ -244,4 +244,5 @@ Harry Brundage (@hornairs)
244
244
  Dylan Smith (@dylanahsmith)
245
245
  Tobias Lütke (@tobi)
246
246
  John Duff (@jduff)
247
- Francis Bogsany (@fbogsany)
247
+ Francis Bogsany (@fbogsany)
248
+ Arthur Neves (@arthurnn)
@@ -15,10 +15,8 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = IdentityCache::VERSION
17
17
 
18
-
19
- gem.add_dependency('ar_transaction_changes', '0.0.1')
20
- gem.add_dependency('activerecord', '~> 3.2.12')
21
- gem.add_dependency('activesupport', '~> 3.2.12')
18
+ gem.add_dependency('ar_transaction_changes', '= 0.0.3')
19
+ gem.add_dependency('activerecord', '>= 3.2', '< 4.1')
22
20
 
23
21
  gem.add_development_dependency('memcache-client')
24
22
  gem.add_development_dependency('rake')
@@ -29,8 +27,6 @@ Gem::Specification.new do |gem|
29
27
  gem.add_development_dependency 'activerecord-jdbcmysql-adapter'
30
28
  gem.add_development_dependency 'jdbc-mysql'
31
29
  else
32
- gem.add_development_dependency('debugger')
33
- gem.add_development_dependency('ruby-prof')
34
30
  gem.add_development_dependency('cityhash', '0.6.0')
35
31
  gem.add_development_dependency('mysql2')
36
32
  end
@@ -1,4 +1,5 @@
1
1
  require 'active_record'
2
+ require 'active_support/core_ext/module/attribute_accessors'
2
3
  require 'ar_transaction_changes'
3
4
 
4
5
  require "identity_cache/version"
@@ -26,6 +27,9 @@ module IdentityCache
26
27
  attr_accessor :readonly
27
28
  attr_writer :logger
28
29
 
30
+ mattr_accessor :cache_namespace
31
+ self.cache_namespace = "IDC:#{CACHE_VERSION}:".freeze
32
+
29
33
  def included(base) #:nodoc:
30
34
  raise AlreadyIncludedError if base.respond_to? :cache_indexes
31
35
 
@@ -43,7 +47,11 @@ module IdentityCache
43
47
  # +cache_adaptor+ - A ActiveSupport::Cache::Store
44
48
  #
45
49
  def cache_backend=(cache_adaptor)
46
- cache.cache_backend = cache_adaptor
50
+ if @cache
51
+ cache.cache_backend = cache_adaptor
52
+ else
53
+ @cache = MemoizedCacheProxy.new(cache_adaptor)
54
+ end
47
55
  end
48
56
 
49
57
  def cache
@@ -24,7 +24,7 @@ module IdentityCache
24
24
  end
25
25
 
26
26
  def build_normalized_belongs_to_cache(association, options)
27
- self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
27
+ self.class_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
28
28
  def #{options[:cached_accessor_name]}
29
29
  if IdentityCache.should_cache? && #{options[:foreign_key]}.present? && !association(:#{association}).loaded?
30
30
  self.#{association} = #{options[:association_class]}.fetch_by_id(#{options[:foreign_key]})
@@ -19,8 +19,8 @@ module IdentityCache
19
19
  end
20
20
 
21
21
  def self.embedded_associations(klass)
22
- if klass.respond_to?(:all_cached_associations_needing_population)
23
- klass.all_cached_associations_needing_population
22
+ if klass.respond_to?(:all_embedded_associations)
23
+ klass.all_embedded_associations
24
24
  else
25
25
  {}
26
26
  end
@@ -45,7 +45,8 @@ module IdentityCache
45
45
  end
46
46
 
47
47
  def rails_cache_key_namespace
48
- DEFAULT_NAMESPACE
48
+ ns = IdentityCache.cache_namespace
49
+ ns.is_a?(Proc) ? ns.call(self) : ns
49
50
  end
50
51
 
51
52
  private
@@ -59,17 +60,25 @@ module IdentityCache
59
60
  end
60
61
 
61
62
  def secondary_cache_index_key_for_current_values(fields) # :nodoc:
62
- self.class.rails_cache_index_key_for_fields_and_values(fields, fields.collect {|field| self.send(field)})
63
+ self.class.rails_cache_index_key_for_fields_and_values(fields, current_values_for_fields(fields))
63
64
  end
64
65
 
65
66
  def secondary_cache_index_key_for_previous_values(fields) # :nodoc:
66
67
  self.class.rails_cache_index_key_for_fields_and_values(fields, old_values_for_fields(fields))
67
68
  end
68
69
 
70
+ def attribute_cache_key_for_attribute_and_current_values(attribute, fields) # :nodoc:
71
+ self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, current_values_for_fields(fields))
72
+ end
73
+
69
74
  def attribute_cache_key_for_attribute_and_previous_values(attribute, fields) # :nodoc:
70
75
  self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields))
71
76
  end
72
77
 
78
+ def current_values_for_fields(fields) # :nodoc:
79
+ fields.collect {|field| self.send(field)}
80
+ end
81
+
73
82
  def old_values_for_fields(fields) # :nodoc:
74
83
  fields.map do |field|
75
84
  field_string = field.to_s
@@ -52,7 +52,7 @@ module IdentityCache
52
52
  arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
53
53
 
54
54
  if options[:unique]
55
- self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
55
+ self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
56
56
  def fetch_by_#{field_list}(#{arg_list})
57
57
  identity_cache_single_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}])
58
58
  end
@@ -63,7 +63,7 @@ module IdentityCache
63
63
  end
64
64
  CODE
65
65
  else
66
- self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
66
+ self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
67
67
  def fetch_by_#{field_list}(#{arg_list})
68
68
  identity_cache_multiple_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}])
69
69
  end
@@ -170,7 +170,7 @@ module IdentityCache
170
170
  field_list = fields.join("_and_")
171
171
  arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
172
172
 
173
- self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
173
+ self.instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
174
174
  def fetch_#{attribute}_by_#{field_list}(#{arg_list})
175
175
  attribute_dynamic_fetcher(#{attribute.inspect}, #{fields.inspect}, [#{arg_list}])
176
176
  end
@@ -210,7 +210,7 @@ module IdentityCache
210
210
 
211
211
 
212
212
  unless instance_methods.include?(options[:cached_accessor_name].to_sym)
213
- self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
213
+ self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
214
214
  def #{options[:cached_accessor_name]}
215
215
  fetch_denormalized_cached_association('#{options[:records_variable_name]}', :#{association})
216
216
  end
@@ -235,7 +235,7 @@ module IdentityCache
235
235
  options[:population_method_name] ||= "populate_#{association}_cache"
236
236
  options[:prepopulate_method_name] ||= "prepopulate_fetched_#{association}"
237
237
 
238
- self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
238
+ self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
239
239
  attr_reader :#{options[:ids_variable_name]}
240
240
 
241
241
  def #{options[:cached_ids_name]}
@@ -281,7 +281,7 @@ module IdentityCache
281
281
  child_class.send(:include, ArTransactionChanges) unless child_class.include?(ArTransactionChanges)
282
282
  child_class.send(:include, ParentModelExpiration) unless child_class.include?(ParentModelExpiration)
283
283
 
284
- child_class.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
284
+ child_class.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
285
285
  after_commit :expire_parent_cache
286
286
  after_touch :expire_parent_cache
287
287
 
@@ -9,7 +9,7 @@ module IdentityCache
9
9
  base.private_class_method :set_embedded_association
10
10
  base.private_class_method :get_embedded_association
11
11
  base.private_class_method :add_cached_associations_to_coder
12
- base.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
12
+ base.instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
13
13
  private :expire_cache, :was_new_record?, :fetch_denormalized_cached_association,
14
14
  :populate_denormalized_cached_association
15
15
  CODE
@@ -26,8 +26,9 @@ module IdentityCache
26
26
  end
27
27
 
28
28
  # Default fetcher added to the model on inclusion, it behaves like
29
- # ActiveRecord::Base.find_by_id
29
+ # ActiveRecord::Base.where(id: id).first
30
30
  def fetch_by_id(id)
31
+ return unless id
31
32
  raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
32
33
  if IdentityCache.should_cache?
33
34
 
@@ -40,7 +41,7 @@ module IdentityCache
40
41
  end
41
42
 
42
43
  else
43
- self.find_by_id(id)
44
+ self.where(id: id).first
44
45
  end
45
46
  end
46
47
 
@@ -95,8 +96,10 @@ module IdentityCache
95
96
  @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
96
97
  @association_cache = {}
97
98
  @aggregation_cache = {}
99
+ @_start_transaction_state = {}
98
100
  @readonly = @destroyed = @marked_for_destruction = false
99
101
  @new_record = false
102
+ @column_types = self.class.column_types if self.class.respond_to?(:column_types)
100
103
  end
101
104
  else
102
105
  record.init_with(coder)
@@ -115,7 +118,7 @@ module IdentityCache
115
118
  association = reflection.association_class.new(record, reflection)
116
119
  association.target = coder_or_array.map {|e| record_from_coder(e) }
117
120
  association.target.each {|e| association.set_inverse_instance(e) }
118
- association.proxy
121
+ association
119
122
  else
120
123
  record_from_coder(coder_or_array)
121
124
  end
@@ -175,9 +178,9 @@ module IdentityCache
175
178
  end
176
179
 
177
180
  def resolve_cache_miss(id)
178
- self.find_by_id(id, :include => cache_fetch_includes).tap do |object|
179
- object.try(:populate_association_caches)
180
- end
181
+ object = self.includes(cache_fetch_includes).where(id: id).try(:first)
182
+ object.try(:populate_association_caches)
183
+ object
181
184
  end
182
185
 
183
186
  def all_embedded_associations
@@ -223,7 +226,7 @@ module IdentityCache
223
226
  def find_batch(ids, options = {})
224
227
  @id_column ||= columns.detect {|c| c.name == "id"}
225
228
  ids = ids.map{ |id| @id_column.type_cast(id) }
226
- records = where('id IN (?)', ids).includes(cache_fetch_includes(options[:includes])).all
229
+ records = where('id IN (?)', ids).includes(cache_fetch_includes(options[:includes])).to_a
227
230
  records_by_id = records.index_by(&:id)
228
231
  records = ids.map{ |id| records_by_id[id] }
229
232
  mismatching_ids = records.compact.map(&:id) - ids
@@ -334,7 +337,8 @@ module IdentityCache
334
337
  ivar_full_name = :"@#{ivar_name}"
335
338
  if IdentityCache.should_cache?
336
339
  populate_denormalized_cached_association(ivar_name, association_name)
337
- IdentityCache.unmap_cached_nil_for(instance_variable_get(ivar_full_name))
340
+ assoc = IdentityCache.unmap_cached_nil_for(instance_variable_get(ivar_full_name))
341
+ assoc.is_a?(ActiveRecord::Associations::CollectionAssociation) ? assoc.reader : assoc
338
342
  else
339
343
  send(association_name.to_sym)
340
344
  end
@@ -388,7 +392,14 @@ module IdentityCache
388
392
 
389
393
  def expire_attribute_indexes # :nodoc:
390
394
  cache_attributes.try(:each) do |(attribute, fields)|
391
- IdentityCache.cache.delete(attribute_cache_key_for_attribute_and_previous_values(attribute, fields)) unless was_new_record?
395
+ unless was_new_record?
396
+ old_cache_attribute_key = attribute_cache_key_for_attribute_and_previous_values(attribute, fields)
397
+ IdentityCache.cache.delete(old_cache_attribute_key)
398
+ end
399
+ new_cache_attribute_key = attribute_cache_key_for_attribute_and_current_values(attribute, fields)
400
+ if new_cache_attribute_key != old_cache_attribute_key
401
+ IdentityCache.cache.delete(new_cache_attribute_key)
402
+ end
392
403
  end
393
404
  end
394
405
 
@@ -1,4 +1,4 @@
1
1
  module IdentityCache
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  CACHE_VERSION = 3
4
4
  end
@@ -18,11 +18,11 @@ require File.dirname(__FILE__) + '/../test/helpers/database_connection'
18
18
  require File.dirname(__FILE__) + '/../test/helpers/cache'
19
19
 
20
20
  def create_record(id)
21
- r = Record.new(id)
21
+ Item.new(id)
22
22
  end
23
23
 
24
24
  def database_ready(count)
25
- Record.where(:id => (1..count)).count == count
25
+ Item.where(:id => (1..count)).count == count
26
26
  rescue
27
27
  false
28
28
  end
@@ -35,10 +35,10 @@ def create_database(count)
35
35
 
36
36
  DatabaseConnection.setup
37
37
  # set up associations
38
- Record.cache_has_one :associated
39
- Record.cache_has_many :associated_records, :embed => true
40
- Record.cache_has_many :normalized_associated_records, :embed => false
41
- Record.cache_index :title, :unique => :true
38
+ Item.cache_has_one :associated
39
+ Item.cache_has_many :associated_records, :embed => true
40
+ Item.cache_has_many :normalized_associated_records, :embed => false
41
+ Item.cache_index :title, :unique => :true
42
42
  AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
43
43
 
44
44
  return if database_ready(count)
@@ -46,10 +46,10 @@ def create_database(count)
46
46
 
47
47
  DatabaseConnection.drop_tables
48
48
  DatabaseConnection.create_tables
49
- existing = Record.all
49
+ existing = Item.all
50
50
  (1..count).to_a.each do |i|
51
51
  unless existing.any? { |e| e.id == i }
52
- a = Record.new
52
+ a = Item.new
53
53
  a.id = i
54
54
  a.associated = AssociatedRecord.new(name: "Associated for #{i}")
55
55
  a.associated_records
@@ -77,7 +77,7 @@ end
77
77
  class FindRunner < CacheRunner
78
78
  def run
79
79
  (1..@count).each do |i|
80
- ::Record.find(i)
80
+ ::Item.find(i)
81
81
  end
82
82
  end
83
83
  end
@@ -93,7 +93,7 @@ class FetchMissRunner < CacheRunner
93
93
 
94
94
  def run
95
95
  (1..@count).each do |i|
96
- rec = ::Record.fetch(i)
96
+ rec = ::Item.fetch(i)
97
97
  rec.fetch_associated
98
98
  rec.fetch_associated_records
99
99
  end
@@ -105,7 +105,7 @@ class DoubleFetchMissRunner < CacheRunner
105
105
 
106
106
  def run
107
107
  (1..@count).each do |i|
108
- rec = ::Record.fetch(i)
108
+ rec = ::Item.fetch(i)
109
109
  rec.fetch_associated
110
110
  rec.fetch_associated_records
111
111
  rec.fetch_normalized_associated_records
@@ -117,7 +117,7 @@ module HitRunner
117
117
  def prepare
118
118
  IdentityCache.cache.clear
119
119
  (1..@count).each do |i|
120
- rec = ::Record.fetch(i)
120
+ rec = ::Item.fetch(i)
121
121
  rec.fetch_normalized_associated_records
122
122
  end
123
123
  end
@@ -128,7 +128,7 @@ class FetchHitRunner < CacheRunner
128
128
 
129
129
  def run
130
130
  (1..@count).each do |i|
131
- rec = ::Record.fetch(i)
131
+ rec = ::Item.fetch(i)
132
132
  # these should all be no cost
133
133
  rec.fetch_associated
134
134
  rec.fetch_associated_records
@@ -141,7 +141,7 @@ class DoubleFetchHitRunner < CacheRunner
141
141
 
142
142
  def run
143
143
  (1..@count).each do |i|
144
- rec = ::Record.fetch(i)
144
+ rec = ::Item.fetch(i)
145
145
  # these should all be no cost
146
146
  rec.fetch_associated
147
147
  rec.fetch_associated_records
@@ -149,4 +149,3 @@ class DoubleFetchHitRunner < CacheRunner
149
149
  end
150
150
  end
151
151
  end
152
-
@@ -6,12 +6,11 @@ class AttributeCacheTest < IdentityCache::TestCase
6
6
  def setup
7
7
  super
8
8
  AssociatedRecord.cache_attribute :name
9
- AssociatedRecord.cache_attribute :record, :by => [:id, :name]
10
9
 
11
- @parent = Record.create!(:title => 'bob')
10
+ @parent = Item.create!(:title => 'bob')
12
11
  @record = @parent.associated_records.create!(:name => 'foo')
13
12
  @name_attribute_key = "#{NAMESPACE}attribute:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s)}"
14
- @blob_key = "#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:1"
13
+ @blob_key = "#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,item_id:integer,name:string")}:1"
15
14
  end
16
15
 
17
16
  def test_attribute_values_are_returned_on_cache_hits
@@ -21,17 +20,16 @@ class AttributeCacheTest < IdentityCache::TestCase
21
20
 
22
21
  def test_attribute_values_are_fetched_and_returned_on_cache_misses
23
22
  IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
24
- Record.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
23
+ Item.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
25
24
  assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
26
25
  end
27
26
 
28
27
  def test_attribute_values_are_stored_in_the_cache_on_cache_misses
29
-
30
28
  # Cache miss, so
31
29
  IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
32
30
 
33
31
  # Grab the value of the attribute from the DB
34
- Record.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
32
+ Item.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
35
33
 
36
34
  # And write it back to the cache
37
35
  IdentityCache.cache.expects(:write).with(@name_attribute_key, 'foo').returns(nil)
@@ -39,6 +37,19 @@ class AttributeCacheTest < IdentityCache::TestCase
39
37
  assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
40
38
  end
41
39
 
40
+ def test_nil_is_stored_in_the_cache_on_cache_misses
41
+ # Cache miss, so
42
+ IdentityCache.cache.expects(:read).with(@name_attribute_key).returns(nil)
43
+
44
+ # 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)
46
+
47
+ # And write it back to the cache
48
+ IdentityCache.cache.expects(:write).with(@name_attribute_key, IdentityCache::CACHED_NIL).returns(nil)
49
+
50
+ assert_equal nil, AssociatedRecord.fetch_name_by_id(1)
51
+ end
52
+
42
53
  def test_cached_attribute_values_are_expired_from_the_cache_when_an_existing_record_is_saved
43
54
  IdentityCache.cache.expects(:delete).with(@name_attribute_key)
44
55
  IdentityCache.cache.expects(:delete).with(@blob_key)
@@ -59,17 +70,27 @@ class AttributeCacheTest < IdentityCache::TestCase
59
70
  end
60
71
 
61
72
  def test_cached_attribute_values_are_expired_from_the_cache_when_a_new_record_is_saved
62
- IdentityCache.cache.expects(:delete).with("#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:2")
73
+ new_id = 2.to_s
74
+ # primary index delete
75
+ IdentityCache.cache.expects(:delete).with("#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,item_id:integer,name:string")}:#{new_id}")
76
+ # attribute cache delete
77
+ IdentityCache.cache.expects(:delete).with("#{NAMESPACE}attribute:AssociatedRecord:name:id:#{cache_hash(new_id)}")
63
78
  @parent.associated_records.create(:name => 'bar')
64
79
  end
65
80
 
66
81
  def test_fetching_by_attribute_delegates_to_block_if_transactions_are_open
67
82
  IdentityCache.cache.expects(:read).with(@name_attribute_key).never
68
83
 
69
- Record.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
84
+ Item.connection.expects(:select_value).with("SELECT `name` FROM `associated_records` WHERE `id` = 1 LIMIT 1").returns('foo')
70
85
 
71
86
  @record.transaction do
72
87
  assert_equal 'foo', AssociatedRecord.fetch_name_by_id(1)
73
88
  end
74
89
  end
90
+
91
+ def test_previously_stored_cached_nils_are_busted_by_new_record_saves
92
+ assert_equal nil, AssociatedRecord.fetch_name_by_id(2)
93
+ AssociatedRecord.create(:name => "Jim")
94
+ assert_equal "Jim", AssociatedRecord.fetch_name_by_id(2)
95
+ end
75
96
  end