identity_cache 0.0.2 → 0.0.3

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