identity_cache 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,23 @@
1
- require "identity_cache/version"
2
1
  require 'cityhash'
3
2
  require 'ar_transaction_changes'
4
- require File.dirname(__FILE__) + '/memoized_cache_proxy'
5
- require File.dirname(__FILE__) + '/belongs_to_caching'
3
+ require "identity_cache/version"
4
+ require 'identity_cache/memoized_cache_proxy'
5
+ require 'identity_cache/belongs_to_caching'
6
+ require 'identity_cache/cache_key_generation'
7
+ require 'identity_cache/configuration_dsl'
8
+ require 'identity_cache/parent_model_expiration'
9
+ require 'identity_cache/query_api'
6
10
 
7
11
  module IdentityCache
8
12
  CACHED_NIL = :idc_cached_nil
9
13
 
14
+ class AlreadyIncludedError < StandardError; end
15
+ class InverseAssociationError < StandardError
16
+ def initialize
17
+ super "Inverse name for association could not be determined. Please use the :inverse_name option to specify the inverse association name for this cache."
18
+ end
19
+ end
20
+
10
21
  class << self
11
22
 
12
23
  attr_accessor :logger, :readonly
@@ -46,18 +57,13 @@ module IdentityCache
46
57
 
47
58
  if result.nil?
48
59
  if block_given?
49
- ActiveRecord::Base.connection.with_master do
50
- result = yield
51
- end
60
+ result = yield
52
61
  result = map_cached_nil_for(result)
53
62
 
54
63
  if should_cache?
55
64
  cache.write(key, result)
56
65
  end
57
66
  end
58
- logger.debug "[IdentityCache] cache miss for #{key}"
59
- else
60
- logger.debug "[IdentityCache] cache hit for #{key}"
61
67
  end
62
68
 
63
69
  unmap_cached_nil_for(result)
@@ -82,29 +88,25 @@ module IdentityCache
82
88
  result = {}
83
89
  result = cache.read_multi(*keys) if should_cache?
84
90
 
85
- missed_keys = keys - result.select {|key, value| value.present? }.keys
91
+ hit_keys = result.select {|key, value| value.present? }.keys
92
+ missed_keys = keys - hit_keys
86
93
 
87
94
  if missed_keys.size > 0
88
95
  if block_given?
89
96
  replacement_results = nil
90
- ActiveRecord::Base.connection.with_master do
91
- replacement_results = yield missed_keys
92
- end
97
+ replacement_results = yield missed_keys
93
98
  missed_keys.zip(replacement_results) do |(key, replacement_result)|
94
99
  if should_cache?
95
100
  replacement_result = map_cached_nil_for(replacement_result )
96
101
  cache.write(key, replacement_result)
97
- logger.debug "[IdentityCache] cache miss for #{key} (multi)"
102
+ logger.debug { "[IdentityCache] cache miss for #{key} (multi)" }
98
103
  end
99
104
  result[key] = replacement_result
100
105
  end
101
106
  end
102
- else
103
- result.keys.each do |key|
104
- logger.debug "[IdentityCache] cache hit for #{key} (multi)"
105
- end
106
107
  end
107
108
 
109
+
108
110
  result.keys.each do |key|
109
111
  result[key] = unmap_cached_nil_for(result[key])
110
112
  end
@@ -113,630 +115,32 @@ module IdentityCache
113
115
  end
114
116
 
115
117
  def schema_to_string(columns)
116
- columns.sort_by(&:name).map {|c| "#{c.name}:#{c.type}"} * ","
118
+ columns.sort_by(&:name).map{|c| "#{c.name}:#{c.type}"}.join(',')
119
+ end
120
+
121
+ def denormalized_schema_hash(klass)
122
+ schema_string = schema_to_string(klass.columns)
123
+ if klass.respond_to?(:all_cached_associations_needing_population) && !(embeded_associations = klass.all_cached_associations_needing_population).empty?
124
+ embedded_schema = embeded_associations.map do |name, options|
125
+ "#{name}:(#{denormalized_schema_hash(options[:association_class])})"
126
+ end.sort.join(',')
127
+ schema_string << "," << embedded_schema
128
+ end
129
+ IdentityCache.memcache_hash(schema_string)
117
130
  end
118
131
 
119
132
  def included(base) #:nodoc:
120
133
  raise AlreadyIncludedError if base.respond_to? :cache_indexes
121
134
 
122
- unless ActiveRecord::Base.connection.respond_to?(:with_master)
123
- ActiveRecord::Base.connection.class.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
124
- def with_master
125
- yield
126
- end
127
- CODE
128
- end
129
-
130
135
  base.send(:include, ArTransactionChanges) unless base.include?(ArTransactionChanges)
131
136
  base.send(:include, IdentityCache::BelongsToCaching)
132
- base.after_commit :expire_cache
133
- base.after_touch :expire_cache
134
- base.class_attribute :cache_indexes
135
- base.class_attribute :cache_attributes
136
- base.class_attribute :cached_has_manys
137
- base.class_attribute :cached_has_ones
138
- base.class_attribute :embedded_schema_hashes
139
- base.send(:extend, ClassMethods)
140
-
141
- base.cached_has_manys = {}
142
- base.cached_has_ones = {}
143
- base.embedded_schema_hashes = {}
144
- base.cache_attributes = []
145
- base.cache_indexes = []
146
-
147
- base.private_class_method :require_if_necessary, :build_normalized_has_many_cache, :build_denormalized_association_cache, :add_parent_expiry_hook,
148
- :identity_cache_multiple_value_dynamic_fetcher, :identity_cache_single_value_dynamic_fetcher
149
-
150
-
151
- base.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
152
- private :expire_cache, :was_new_record?, :fetch_denormalized_cached_association, :populate_denormalized_cached_association
153
- CODE
137
+ base.send(:include, IdentityCache::CacheKeyGeneration)
138
+ base.send(:include, IdentityCache::ConfigurationDSL)
139
+ base.send(:include, IdentityCache::QueryAPI)
154
140
  end
155
141
 
156
142
  def memcache_hash(key) #:nodoc:
157
143
  CityHash.hash64(key)
158
144
  end
159
145
  end
160
-
161
- module ClassMethods
162
-
163
- # Declares a new index in the cache for the class where IdentityCache was
164
- # included.
165
- #
166
- # IdentityCache will add a fetch_by_field1_and_field2_and_...field for every
167
- # index.
168
- #
169
- # == Example:
170
- #
171
- # class Product
172
- # include IdentityCache
173
- # cache_index :name, :vendor
174
- # end
175
- #
176
- # Will add Product.fetch_by_name_and_vendor
177
- #
178
- # == Parameters
179
- #
180
- # +fields+ Array of symbols or strings representing the fields in the index
181
- #
182
- # == Options
183
- # * unique: if the index would only have unique values
184
- #
185
- def cache_index(*fields)
186
- options = fields.extract_options!
187
- self.cache_indexes.push fields
188
-
189
- field_list = fields.join("_and_")
190
- arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
191
- where_list = fields.each_with_index.collect { |f, i| "#{f} = \#{quote_value(arg#{i})}" }.join(" AND ")
192
-
193
- if options[:unique]
194
- self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
195
- def fetch_by_#{field_list}(#{arg_list})
196
- sql = "SELECT id FROM #{table_name} WHERE #{where_list} LIMIT 1"
197
- identity_cache_single_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}], sql)
198
- end
199
-
200
- # exception throwing variant
201
- def fetch_by_#{field_list}!(#{arg_list})
202
- fetch_by_#{field_list}(#{arg_list}) or raise ActiveRecord::RecordNotFound
203
- end
204
- CODE
205
- else
206
- self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
207
- def fetch_by_#{field_list}(#{arg_list})
208
- sql = "SELECT id FROM #{table_name} WHERE #{where_list}"
209
- identity_cache_multiple_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}], sql)
210
- end
211
- CODE
212
- end
213
- end
214
-
215
- def identity_cache_single_value_dynamic_fetcher(fields, values, sql_on_miss) # :nodoc:
216
- cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
217
- id = IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
218
- unless id.nil?
219
- record = fetch_by_id(id.to_i)
220
- IdentityCache.cache.delete(cache_key) unless record
221
- end
222
-
223
- record
224
- end
225
-
226
- def identity_cache_multiple_value_dynamic_fetcher(fields, values, sql_on_miss) # :nodoc:
227
- cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
228
- ids = IdentityCache.fetch(cache_key) { connection.select_values(sql_on_miss) }
229
-
230
- ids.empty? ? [] : fetch_multi(*ids)
231
- end
232
-
233
-
234
- # Will cache an association to the class including IdentityCache.
235
- # The embed option, if set, will make IdentityCache keep the association
236
- # values in the same cache entry as the parent.
237
- #
238
- # Embedded associations are more effective in offloading database work,
239
- # however they will increase the size of the cache entries and make the
240
- # whole entry expire when any of the embedded members change.
241
- #
242
- # == Example:
243
- # class Product
244
- # cached_has_many :options, :embed => false
245
- # cached_has_many :orders
246
- # cached_has_many :buyers, :inverse_name => 'line_item'
247
- # end
248
- #
249
- # == Parameters
250
- # +association+ Name of the association being cached as a symbol
251
- #
252
- # == Options
253
- #
254
- # * embed: If set will cause IdentityCache to keep the values for this
255
- # association in the same cache entry as the parent, instead of its own.
256
- # * inverse_name: The name of the parent in the association if the name is
257
- # not the lowercase pluralization of the parent object's class
258
- def cache_has_many(association, options = {})
259
- options[:embed] ||= false
260
- options[:inverse_name] ||= self.name.underscore.to_sym
261
- raise InverseAssociationError unless self.reflect_on_association(association)
262
- self.cached_has_manys[association] = options
263
-
264
- if options[:embed]
265
- build_denormalized_association_cache(association, options)
266
- else
267
- build_normalized_has_many_cache(association, options)
268
- end
269
- end
270
-
271
- # Will cache an association to the class including IdentityCache.
272
- # The embed option if set will make IdentityCache keep the association
273
- # values in the same cache entry as the parent.
274
- #
275
- # Embedded associations are more effective in offloading database work,
276
- # however they will increase the size of the cache entries and make the
277
- # whole entry expire with the change of any of the embedded members
278
- #
279
- # == Example:
280
- # class Product
281
- # cached_has_one :store, :embed => false
282
- # cached_has_one :vendor
283
- # end
284
- #
285
- # == Parameters
286
- # +association+ Symbol with the name of the association being cached
287
- #
288
- # == Options
289
- #
290
- # * embed: If set will cause IdentityCache to keep the values for this
291
- # association in the same cache entry as the parent, instead of its own.
292
- # * inverse_name: The name of the parent in the association ( only
293
- # necessary if the name is not the lowercase pluralization of the
294
- # parent object's class)
295
- def cache_has_one(association, options = {})
296
- options[:embed] ||= true
297
- options[:inverse_name] ||= self.name.underscore.to_sym
298
- raise InverseAssociationError unless self.reflect_on_association(association)
299
- self.cached_has_ones[association] = options
300
-
301
- build_denormalized_association_cache(association, options)
302
- end
303
-
304
- def build_denormalized_association_cache(association, options) #:nodoc:
305
- options[:cached_accessor_name] ||= "fetch_#{association}"
306
- options[:cache_variable_name] ||= "cached_#{association}"
307
- options[:population_method_name] ||= "populate_#{association}_cache"
308
-
309
-
310
- unless instance_methods.include?(options[:cached_accessor_name].to_sym)
311
- self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
312
- def #{options[:cached_accessor_name]}
313
- fetch_denormalized_cached_association('#{options[:cache_variable_name]}', :#{association})
314
- end
315
-
316
- def #{options[:population_method_name]}
317
- populate_denormalized_cached_association('#{options[:cache_variable_name]}', :#{association})
318
- end
319
- CODE
320
-
321
- association_class = reflect_on_association(association).klass
322
- add_parent_expiry_hook(association_class, options.merge(:only_on_foreign_key_change => false))
323
- end
324
- end
325
-
326
- def build_normalized_has_many_cache(association, options) #:nodoc:
327
- singular_association = association.to_s.singularize
328
- association_class = reflect_on_association(association).klass
329
- options[:cached_accessor_name] ||= "fetch_#{association}"
330
- options[:ids_name] ||= "#{singular_association}_ids"
331
- options[:ids_cache_name] ||= "cached_#{options[:ids_name]}"
332
- options[:population_method_name] ||= "populate_#{association}_cache"
333
-
334
- self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
335
- attr_reader :#{options[:ids_cache_name]}
336
-
337
- def #{options[:population_method_name]}
338
- @#{options[:ids_cache_name]} = #{options[:ids_name]}
339
- end
340
-
341
- def #{options[:cached_accessor_name]}
342
- if IdentityCache.should_cache? || #{association}.loaded?
343
- populate_#{association}_cache unless @#{options[:ids_cache_name]}
344
- @cached_#{association} ||= #{association_class}.fetch_multi(*@#{options[:ids_cache_name]})
345
- else
346
- #{association}
347
- end
348
- end
349
- CODE
350
-
351
- add_parent_expiry_hook(association_class, options.merge(:only_on_foreign_key_change => true))
352
- end
353
-
354
-
355
- # Will cache a single attribute on its own blob, it will add a
356
- # fetch_attribute_by_id (or the value of the by option).
357
- #
358
- # == Example:
359
- # class Product
360
- # cache_attribute :quantity, :by => :name
361
- # cache_attribute :quantity :by => [:name, :vendor]
362
- # end
363
- #
364
- # == Parameters
365
- # +attribute+ Symbol with the name of the attribute being cached
366
- #
367
- # == Options
368
- #
369
- # * by: Other attribute or attributes in the model to keep values indexed. Default is :id
370
- def cache_attribute(attribute, options = {})
371
- options[:by] ||= :id
372
- fields = Array(options[:by])
373
-
374
- self.cache_attributes.push [attribute, fields]
375
-
376
- field_list = fields.join("_and_")
377
- arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
378
- where_list = fields.each_with_index.collect { |f, i| "#{f} = \#{quote_value(arg#{i})}" }.join(" AND ")
379
-
380
- self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
381
- def fetch_#{attribute}_by_#{field_list}(#{arg_list})
382
- sql = "SELECT #{attribute} FROM #{table_name} WHERE #{where_list} LIMIT 1"
383
- attribute_dynamic_fetcher(#{attribute.inspect}, #{fields.inspect}, [#{arg_list}], sql)
384
- end
385
- CODE
386
- end
387
-
388
- def attribute_dynamic_fetcher(attribute, fields, values, sql_on_miss) #:nodoc:
389
- cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
390
- IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
391
- end
392
-
393
- # Similar to ActiveRecord::Base#exists? will return true if the id can be
394
- # found in the cache.
395
- def exists_with_identity_cache?(id)
396
- !!fetch_by_id(id)
397
- end
398
-
399
- # Default fetcher added to the model on inclusion, it behaves like
400
- # ActiveRecord::Base.find_by_id
401
- def fetch_by_id(id)
402
- if IdentityCache.should_cache?
403
-
404
- require_if_necessary do
405
- object = IdentityCache.fetch(rails_cache_key(id)){ resolve_cache_miss(id) }
406
- IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]} " if object && object.id != id.to_i
407
- object
408
- end
409
-
410
- else
411
- self.find_by_id(id)
412
- end
413
- end
414
-
415
- # Default fetcher added to the model on inclusion, it behaves like
416
- # ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
417
- # if id is not in the cache or the db.
418
- def fetch(id)
419
- fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.class.name} with ID=#{id}")
420
- end
421
-
422
-
423
- # Default fetcher added to the model on inclusion, if behaves like
424
- # ActiveRecord::Base.find_all_by_id
425
- def fetch_multi(*ids)
426
- options = ids.extract_options!
427
- if IdentityCache.should_cache?
428
-
429
- require_if_necessary do
430
- cache_keys = ids.map {|id| rails_cache_key(id) }
431
- key_to_id_map = Hash[ cache_keys.zip(ids) ]
432
-
433
- objects_by_key = IdentityCache.fetch_multi(*key_to_id_map.keys) do |unresolved_keys|
434
- ids = unresolved_keys.map {|key| key_to_id_map[key] }
435
- records = find_batch(ids, options)
436
- records.compact.each(&:populate_association_caches)
437
- records
438
- end
439
-
440
- cache_keys.map {|key| objects_by_key[key] }.compact
441
- end
442
-
443
- else
444
- find_batch(ids, options)
445
- end
446
- end
447
-
448
- def require_if_necessary #:nodoc:
449
- # mem_cache_store returns raw value if unmarshal fails
450
- rval = yield
451
- case rval
452
- when String
453
- rval = Marshal.load(rval)
454
- when Array
455
- rval.map!{ |v| v.kind_of?(String) ? Marshal.load(v) : v }
456
- end
457
- rval
458
- rescue ArgumentError => e
459
- if e.message =~ /undefined [\w\/]+ (\w+)/
460
- ok = Kernel.const_get($1) rescue nil
461
- retry if ok
462
- end
463
- raise
464
- end
465
-
466
- module ParentModelExpiration # :nodoc:
467
- def expire_parent_cache_on_changes(parent_name, foreign_key, parent_class, options = {})
468
- new_parent = send(parent_name)
469
-
470
- if new_parent && new_parent.respond_to?(:expire_primary_index, true)
471
- if should_expire_identity_cache_parent?(foreign_key, options[:only_on_foreign_key_change])
472
- new_parent.expire_primary_index
473
- new_parent.expire_parent_cache if new_parent.respond_to?(:expire_parent_cache)
474
- end
475
- end
476
-
477
- if transaction_changed_attributes[foreign_key].present?
478
- begin
479
- old_parent = parent_class.find(transaction_changed_attributes[foreign_key])
480
- old_parent.expire_primary_index if old_parent.respond_to?(:expire_primary_index)
481
- old_parent.expire_parent_cache if old_parent.respond_to?(:expire_parent_cache)
482
- rescue ActiveRecord::RecordNotFound => e
483
- # suppress errors finding the old parent if its been destroyed since it will have expired itself in that case
484
- end
485
- end
486
-
487
- true
488
- end
489
-
490
- def should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
491
- if only_on_foreign_key_change
492
- destroyed? || was_new_record? || transaction_changed_attributes[foreign_key].present?
493
- else
494
- true
495
- end
496
- end
497
- end
498
-
499
- def add_parent_expiry_hook(child_class, options = {})
500
- child_association = child_class.reflect_on_association(options[:inverse_name])
501
- raise InverseAssociationError unless child_association
502
- foreign_key = child_association.association_foreign_key
503
- parent_class ||= self.name
504
- new_parent = options[:inverse_name]
505
-
506
- child_class.send(:include, ArTransactionChanges) unless child_class.include?(ArTransactionChanges)
507
- child_class.send(:include, ParentModelExpiration) unless child_class.include?(ParentModelExpiration)
508
-
509
- child_class.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
510
- after_commit :expire_parent_cache
511
- after_touch :expire_parent_cache
512
-
513
- def expire_parent_cache
514
- expire_parent_cache_on_changes(:#{options[:inverse_name]}, '#{foreign_key}', #{parent_class}, #{options.inspect})
515
- end
516
- CODE
517
- end
518
-
519
- def resolve_cache_miss(id)
520
- self.find_by_id(id, :include => cache_fetch_includes).tap do |object|
521
- object.try(:populate_association_caches)
522
- end
523
- end
524
-
525
- def all_cached_associations
526
- (cached_has_manys || {}).merge(cached_has_ones || {}).merge(cached_belongs_tos || {})
527
- end
528
-
529
- def all_cached_associations_needing_population
530
- all_cached_associations.select do |cached_association, options|
531
- options[:population_method_name].present? # non-embedded belongs_to associations don't need population
532
- end
533
- end
534
-
535
- def cache_fetch_includes(additions = {})
536
- additions = hashify_includes_structure(additions)
537
- embedded_associations = all_cached_associations.select { |name, options| options[:embed] }
538
-
539
- associations_for_identity_cache = embedded_associations.map do |child_association, options|
540
- child_class = reflect_on_association(child_association).try(:klass)
541
-
542
- child_includes = additions.delete(child_association)
543
-
544
- if child_class.respond_to?(:cache_fetch_includes)
545
- child_includes = child_class.cache_fetch_includes(child_includes)
546
- end
547
-
548
- if child_includes.blank?
549
- child_association
550
- else
551
- { child_association => child_includes }
552
- end
553
- end
554
-
555
- associations_for_identity_cache.push(additions) if additions.keys.size > 0
556
- associations_for_identity_cache.compact
557
- end
558
-
559
- def find_batch(ids, options = {})
560
- @id_column ||= columns.detect {|c| c.name == "id"}
561
- ids = ids.map{ |id| @id_column.type_cast(id) }
562
- records = where('id IN (?)', ids).includes(cache_fetch_includes(options[:includes])).all
563
- records_by_id = records.index_by(&:id)
564
- records = ids.map{ |id| records_by_id[id] }
565
- mismatching_ids = records.compact.map(&:id) - ids
566
- IdentityCache.logger.error "[IDC id mismatch] fetch_batch_requested=#{ids.inspect} fetch_batch_got=#{mismatchig_ids.inspect} mismatching ids " unless mismatching_ids.empty?
567
- records
568
- end
569
-
570
- def rails_cache_key(id)
571
- rails_cache_key_prefix + id.to_s
572
- end
573
-
574
-
575
- def rails_cache_key_prefix
576
- @rails_cache_key_prefix ||= begin
577
- "IDC:blob:#{base_class.name}:#{IdentityCache.memcache_hash(IdentityCache.schema_to_string(columns))}:"
578
- end
579
- end
580
-
581
- def rails_cache_index_key_for_fields_and_values(fields, values)
582
- "IDC:index:#{base_class.name}:#{rails_cache_string_for_fields_and_values(fields, values)}"
583
- end
584
-
585
- def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
586
- "IDC:attribute:#{base_class.name}:#{attribute}:#{rails_cache_string_for_fields_and_values(fields, values)}"
587
- end
588
-
589
- def rails_cache_string_for_fields_and_values(fields, values)
590
- "#{fields.join('/')}:#{IdentityCache.memcache_hash(values.join('/'))}"
591
- end
592
-
593
- private
594
-
595
- def hashify_includes_structure(structure)
596
- case structure
597
- when nil
598
- {}
599
- when Symbol
600
- {structure => []}
601
- when Hash
602
- structure.clone
603
- when Array
604
- structure.each_with_object({}) do |member, hash|
605
- case member
606
- when Hash
607
- hash.merge(hash)
608
- when Symbol
609
- hash[member] = []
610
- end
611
- end
612
- end
613
- end
614
- end
615
-
616
- def populate_association_caches # :nodoc:
617
- self.class.all_cached_associations_needing_population.each do |cached_association, options|
618
- send(options[:population_method_name])
619
- reflection = options[:embed] && self.class.reflect_on_association(cached_association)
620
- if reflection && reflection.klass.respond_to?(:cached_has_manys)
621
- child_objects = Array.wrap(send(options[:cached_accessor_name]))
622
- child_objects.each(&:populate_association_caches)
623
- end
624
- end
625
- self.clear_association_cache if self.respond_to?(:clear_association_cache)
626
- end
627
-
628
- def fetch_denormalized_cached_association(ivar_name, association_name) # :nodoc:
629
- ivar_full_name = :"@#{ivar_name}"
630
- if IdentityCache.should_cache?
631
- populate_denormalized_cached_association(ivar_name, association_name)
632
- IdentityCache.unmap_cached_nil_for(instance_variable_get(ivar_full_name))
633
- else
634
- send(association_name.to_sym)
635
- end
636
- end
637
-
638
- def populate_denormalized_cached_association(ivar_name, association_name) # :nodoc:
639
- ivar_full_name = :"@#{ivar_name}"
640
- schema_hash_ivar = :"@#{ivar_name}_schema_hash"
641
- reflection = association(association_name)
642
-
643
- current_schema_hash = self.class.embedded_schema_hashes[association_name] ||= begin
644
- IdentityCache.memcache_hash(IdentityCache.schema_to_string(reflection.klass.columns))
645
- end
646
-
647
- saved_schema_hash = instance_variable_get(schema_hash_ivar)
648
-
649
- if saved_schema_hash == current_schema_hash
650
- value = instance_variable_get(ivar_full_name)
651
- return value unless value.nil?
652
- end
653
-
654
- reflection.load_target unless reflection.loaded?
655
-
656
- loaded_association = send(association_name)
657
-
658
- instance_variable_set(schema_hash_ivar, current_schema_hash)
659
- instance_variable_set(ivar_full_name, IdentityCache.map_cached_nil_for(loaded_association))
660
- end
661
-
662
- def primary_cache_index_key # :nodoc:
663
- self.class.rails_cache_key(id)
664
- end
665
-
666
- def secondary_cache_index_key_for_current_values(fields) # :nodoc:
667
- self.class.rails_cache_index_key_for_fields_and_values(fields, fields.collect {|field| self.send(field)})
668
- end
669
-
670
- def secondary_cache_index_key_for_previous_values(fields) # :nodoc:
671
- self.class.rails_cache_index_key_for_fields_and_values(fields, old_values_for_fields(fields))
672
- end
673
-
674
- def attribute_cache_key_for_attribute_and_previous_values(attribute, fields) # :nodoc:
675
- self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields))
676
- end
677
-
678
- def old_values_for_fields(fields) # :nodoc:
679
- fields.map do |field|
680
- field_string = field.to_s
681
- if destroyed? && transaction_changed_attributes.has_key?(field_string)
682
- transaction_changed_attributes[field_string]
683
- elsif persisted? && transaction_changed_attributes.has_key?(field_string)
684
- transaction_changed_attributes[field_string]
685
- else
686
- self.send(field)
687
- end
688
- end
689
- end
690
-
691
- def expire_primary_index # :nodoc:
692
- extra_keys = if respond_to? :updated_at
693
- old_updated_at = old_values_for_fields([:updated_at]).first
694
- "expiring_last_updated_at=#{old_updated_at}"
695
- else
696
- ""
697
- end
698
- IdentityCache.logger.debug "[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}"
699
-
700
- IdentityCache.cache.delete(primary_cache_index_key)
701
- end
702
-
703
- def expire_secondary_indexes # :nodoc:
704
- cache_indexes.try(:each) do |fields|
705
- if self.destroyed?
706
- IdentityCache.cache.delete(secondary_cache_index_key_for_previous_values(fields))
707
- else
708
- new_cache_index_key = secondary_cache_index_key_for_current_values(fields)
709
- IdentityCache.cache.delete(new_cache_index_key)
710
-
711
- if !was_new_record?
712
- old_cache_index_key = secondary_cache_index_key_for_previous_values(fields)
713
- IdentityCache.cache.delete(old_cache_index_key) unless old_cache_index_key == new_cache_index_key
714
- end
715
- end
716
- end
717
- end
718
-
719
- def expire_attribute_indexes # :nodoc:
720
- cache_attributes.try(:each) do |(attribute, fields)|
721
- IdentityCache.cache.delete(attribute_cache_key_for_attribute_and_previous_values(attribute, fields)) unless was_new_record?
722
- end
723
- end
724
-
725
- def expire_cache # :nodoc:
726
- expire_primary_index
727
- expire_secondary_indexes
728
- expire_attribute_indexes
729
- true
730
- end
731
-
732
- def was_new_record? # :nodoc:
733
- !destroyed? && transaction_changed_attributes.has_key?('id') && transaction_changed_attributes['id'].nil?
734
- end
735
-
736
- class AlreadyIncludedError < StandardError; end
737
- class InverseAssociationError < StandardError
738
- def initialize
739
- super "Inverse name for association could not be determined. Please use the :inverse_name option to specify the inverse association name for this cache."
740
- end
741
- end
742
146
  end