Class: ActiveRecord::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/cs_active_record.rb

Overview

Some monkeypatches upon ActiveRecord::Base

Constant Summary collapse

@@cache_local =
{}
@@use_local_cache =
false
@@ttl =
60 * 15

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.cache_delete(klass, id) ⇒ Object

Invalidate the cache entry for an record. The update method will automatically invalidate the cache when updates are made through ActiveRecord model record. However, several methods update tables with direct sql queries for effeciency. These methods should call this method to invalidate the cache after making those changes.

NOTE - if a SQL query updates multiple rows with one query, there is currently no way to invalidate the affected entries unless the entire cache is dumped or until the TTL expires, so try not to do this.



211
212
213
214
215
# File 'lib/cs_active_record.rb', line 211

def self.cache_delete(klass, id)
  self.cache_local.delete self.cache_key_local(klass, id) if self.use_local_cache?
  CACHE.delete self.cache_key_memcache(klass, id)
  logger.debug("deleted #{self.cache_key_memcache(klass, id)}")
end

.cache_key_local(klass, id) ⇒ Object



372
373
374
375
# File 'lib/cs_active_record.rb', line 372

def self.cache_key_local(klass, id)
  klass = Module.const_get(klass) unless Class === klass
  return "#{klass.base_class.name}:#{id}"
end

.cache_key_memcache(klass, id) ⇒ Object

The memcache key for this record.



384
385
386
# File 'lib/cs_active_record.rb', line 384

def self.cache_key_memcache(klass, id)
  return "active_record:#{self.cache_key_local(klass, id)}"
end

.cache_resetObject

Invalidate the local process cache. This should be called from a before filter at the beginning of each request.



221
222
223
# File 'lib/cs_active_record.rb', line 221

def self.cache_reset
  self.cache_local.clear if self.use_local_cache?
end

.cached_finders(*args) ⇒ Object

Tell this class to use memcache_memoize to cache certain finders.

The results of the finders will be automatically cached and reused (unless you give extra conditions, which will turn off the caching completely!).

Invalidation will occur on save and destroy.

Usage:

class MyModel < ActiveRecord::Base

cached_finders :find_by_id_and_name_and_something_else

end



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/cs_active_record.rb', line 43

def self.cached_finders(*args)
  args.each do |finder|
    if (match = finder.to_s.match(/^find_(all_)?by_(.*)$/))
      attributes = match[2].to_s.split(/_and_/)
      define_method("invalidate_cached_#{finder}") do
        unless self.new_record?
          begin
            self.class.find(self.id).send("non_recursive_invalidate_cached_#{finder}")
          rescue Exception => e
            # This can fail just because our saving broke a transaction, in that case
            # we wont exist, even if we happen to have an id (and !new_record?)
          end
        end
        self.send("non_recursive_invalidate_cached_#{finder}")
      end
      define_method("non_recursive_invalidate_cached_#{finder}") do
        attribute_values = attributes.collect do |attribute|
          self[attribute.to_sym]
        end
        expire_cached_namespace("CachedSupermodel:" + self.class.name + ":#{finder}:" + attribute_values.inspect)
      end
    else
       raise "Unknown finder type #{finder}"
    end

    class_eval "
def self.#{finder}_get_ids(*args)
  match = '#{finder}'.match(/^find_(all_)?by_(.*)$/)
  # flag for find / find_all
  find_all =  '#{finder}'.match(/^find_all_by/) ? true : false
  attributes = match[2].to_s.split(/_and_/)
  normal_arguments = args[0...attributes.size]
  key_type = columns_hash[self.primary_key].type
  
  rest = args[attributes.size..-1]
  hash_args = rest.first || {}
  cache_value(['CachedSupermodel:' + self.name + ':#{finder}:' + normal_arguments.inspect, rest.inspect]) do
     params = []
     query = 'SELECT ' + self.primary_key.to_s  + ' from ' + self.table_name
     where = []
     attributes.each_with_index do |a, i|
       if args[i].nil?
        where << a + ' IS NULL' 
       else
         where << a + ' = ?'
         params << args[i]
       end
     end

     unless descends_from_active_record?
       where << type_condition
     end

     query << (' WHERE ' + where.join(' AND '))

     if hash_args.include?(:order)
       query << ' ORDER BY ' << hash_args[:order]
     end

     if find_all
       if hash_args.include?(:limit)
         query << ' LIMIT ?'
         params << hash_args[:limit]
       end
     else
       query << ' LIMIT 1'
     end

     if hash_args.include?(:offset)
       query << ' OFFSET ?'
       params << hash_args[:offset]
     end
     
     rval = self.connection.select_all(self.send(:sanitize_sql, [query, *params])).collect{ |row| 
                                                                                    if key_type == :integer
                                                                                      row[self.primary_key.to_s].to_i
                                                                                    else
                                                                                      row[self.primary_key.to_s]
                                                                                    end
                                                                                  }.compact


     if find_all
       rval
     else
       if rval.blank?
         nil
       else
         rval.first
       end
     end
  end
end
def self.#{finder}(*args)
begin
  self.#{finder}_orig(*args) 
rescue ActiveRecord::RecordNotFound
    nil
end
end
def self.#{finder}!(*args)
self.#{finder}_orig(*args) 
end
def self.#{finder}_orig(*args)
  match = '#{finder}'.match(/^find_(all_)?by_(.*)$/)
  attributes = match[2].to_s.split(/_and_/)
  normal_arguments = args[0...attributes.size]
  rest = args[attributes.size..-1]
  extra_conditions = false
  rest.each do |arg|
    if Hash === arg
      extra_conditions = extra_conditions || arg.include?(:conditions)
    end
  end
  if extra_conditions
    method_missing(:#{finder}, *args)
  else
    rval = self.#{finder}_get_ids(*args)
    if Array === rval
      rval.collect do |oid|
         self.base_class.find(oid) rescue nil
      end.compact
    else
      if rval.nil?
        raise ActiveRecord::RecordNotFound
      else
        self.base_class.find(rval)
      end
    end
  end
end
before_save :invalidate_cached_#{finder}
before_destroy :invalidate_cached_#{finder}
"
  end
end

.cached_supermodel_decrement_counterObject



431
# File 'lib/cs_active_record.rb', line 431

alias_method :cached_supermodel_decrement_counter, :decrement_counter

.cached_supermodel_deleteObject



447
# File 'lib/cs_active_record.rb', line 447

alias_method :cached_supermodel_delete, :delete

.cached_supermodel_findObject



241
# File 'lib/cs_active_record.rb', line 241

alias_method :cached_supermodel_find, :find

.cached_supermodel_find_by_sqlObject



308
# File 'lib/cs_active_record.rb', line 308

alias_method :cached_supermodel_find_by_sql, :find_by_sql

.cached_supermodel_increment_counterObject



415
# File 'lib/cs_active_record.rb', line 415

alias_method :cached_supermodel_increment_counter, :increment_counter

.decrement_counter(counter_name, id) ⇒ Object



434
435
436
437
438
439
440
# File 'lib/cs_active_record.rb', line 434

def self.decrement_counter(counter_name, id)
  begin
    return cached_supermodel_decrement_counter(counter_name, id)
  ensure
    cache_delete(self, id)
  end
end

.delete(id) ⇒ Object



450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/cs_active_record.rb', line 450

def self.delete(id)
  begin
    return cached_supermodel_delete(id)
    ensure
      if id.respond_to?(:each)
        id.each do |i|
          cache_delete(self.class, i)
        end
      else 
        cache_delete(self.class, id)
      end
  end
end

.find(*args) ⇒ Object



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/cs_active_record.rb', line 244

def self.find(*args)
  args.reject! do |arg|
    arg.is_a?(Hash) && arg.values.compact.empty?
  end
  args[0] = args.first.to_i if args.first =~ /\A\d+\Z/
  # Only handle simple find requests.  If the request was more complicated,
  # let the base class handle it, but store the retrieved records in the
  # local cache in case we need them later.
  if args.length != 1 or !ok_primary_key(args.first) then
    records = cached_supermodel_find(*args)
    # Rails requires two levels of indirection to look up a record
    return records if args.first == :all and @skip_find_hack
    case records
    when Array then
      records.each { |r| r.cache_store }
    when ActiveRecord then
      records.cache_store # Model.find 1 gets cached here
    end
    return records
  end

  # Try to find the record in the local cache.
  id = args.first
  if self.use_local_cache? then
    record = self.cache_local[self.cache_key_local(name, id)]
    return record unless record.nil?
  end

  # Try to find the record in memcache and add it to the local cache
  record = CACHE.get self.cache_key_memcache(name, id)
  unless record == :MemCache_no_such_entry then
    logger.debug("found #{self.cache_key_memcache(name, id)}")
    record = nil if record == :MemCache_nil
    if self.use_local_cache? then
      self.cache_local[self.cache_key_local(name, id)] = record
    end
    return record
  end
  
  # Fetch the record from the DB.  Inside the multiple levels of indirection
  # of find it will get cached. (no it wont, so i added a cache_store below //martin)
  #
  # We don't want the subsequent find_by_sql to loop back here, so guard
  # the call.
  #
  # NOTE This guard is not thread safe, beware use of cached ActiveRecord where
  # ActiveRecord's thread safety is disabled.
  begin
    @skip_find_hack = true
    record = cached_supermodel_find(args).first
    record.cache_store
  ensure
    @skip_find_hack = false
  end

  return record
end

.find_by_sql(*args) ⇒ Object



311
312
313
314
315
316
317
318
319
# File 'lib/cs_active_record.rb', line 311

def self.find_by_sql(*args)
  unless @skip_find_hack
    if args.first =~ /SELECT \* FROM #{table_name} WHERE \(#{table_name}\.#{primary_key} = '?(\d+)'?\) LIMIT 1/ then
      return [find($1.to_i)]
    end
  end

  return cached_supermodel_find_by_sql(*args)
end

.increment_counter(counter_name, id) ⇒ Object



418
419
420
421
422
423
424
# File 'lib/cs_active_record.rb', line 418

def self.increment_counter(counter_name, id)
  begin
    return cached_supermodel_increment_counter(counter_name, id)
  ensure
    cache_delete(self, id)
  end
end

.ok_primary_key(key) ⇒ Object



225
226
227
228
229
230
231
232
233
234
# File 'lib/cs_active_record.rb', line 225

def self.ok_primary_key(key)
  case self.columns_hash[self.primary_key].type
  when :integer
    key.is_a?(Fixnum)
  when :string
    key.is_a?(String)
  else
    raise "I dont know about this column type: #{self.columns_hash[self.primary_key_column].type}"
  end
end

.ttlObject



184
185
186
# File 'lib/cs_active_record.rb', line 184

def self.ttl
  @@ttl
end

.ttl=(t) ⇒ Object



188
189
190
# File 'lib/cs_active_record.rb', line 188

def self.ttl=(t)
  @@ttl = t
end

.use_local_cache=(u) ⇒ Object



196
197
198
# File 'lib/cs_active_record.rb', line 196

def self.use_local_cache=(u)
  @@use_local_cache = u
end

.use_local_cache?Boolean

Returns:

  • (Boolean)


192
193
194
# File 'lib/cs_active_record.rb', line 192

def self.use_local_cache?
  @@use_local_cache
end

Instance Method Details

#cache_deleteObject

Remove this record from the cache.



359
360
361
362
363
# File 'lib/cs_active_record.rb', line 359

def cache_delete
  cache_local.delete cache_key_local if self.class.use_local_cache?
  CACHE.delete cache_key_memcache
  logger.debug("deleted #{cache_key_memcache}")
end

#cache_key_localObject



377
378
379
# File 'lib/cs_active_record.rb', line 377

def cache_key_local
  self.class.cache_key_local(self.class, self.id)
end

#cache_key_memcacheObject



388
389
390
# File 'lib/cs_active_record.rb', line 388

def cache_key_memcache
  self.class.cache_key_memcache(self.class, self.id)
end

#cache_localObject

The local object cache.



395
396
397
# File 'lib/cs_active_record.rb', line 395

def cache_local
  return self.class.cache_local
end

#cache_storeObject

Store this record in the cache without associations. Storing associations leads to wasted cache space and hard-to-debug problems.



402
403
404
405
406
407
408
# File 'lib/cs_active_record.rb', line 402

def cache_store
  if self.class.use_local_cache? then
    cache_local[cache_key_local] = remove_associations
  end
  CACHE.set cache_key_memcache, remove_associations, self.class.ttl
  logger.debug("stored #{cache_key_memcache}")
end

#cached_supermodel_destroyObject

Delete the entry from the cache now that it isn’t in the DB.



324
# File 'lib/cs_active_record.rb', line 324

alias_method :cached_supermodel_destroy, :destroy

#cached_supermodel_reloadObject

Invalidate the cache for this record before reloading from the DB.



335
# File 'lib/cs_active_record.rb', line 335

alias_method :cached_supermodel_reload, :reload

#cached_supermodel_updateObject

Store a new copy of ourselves into the cache.



347
# File 'lib/cs_active_record.rb', line 347

alias_method :cached_supermodel_update, :update

#destroyObject



326
327
328
329
330
# File 'lib/cs_active_record.rb', line 326

def destroy
  return cached_supermodel_destroy
ensure
  cache_delete
end

#reloadObject



337
338
339
340
341
342
# File 'lib/cs_active_record.rb', line 337

def reload
  cache_delete
  return cached_supermodel_reload
ensure
  cache_store
end

#remove_associationsObject

Return a copy of this instance with all association-created attributes removed.



24
25
26
27
28
# File 'lib/cs_active_record.rb', line 24

def remove_associations
  obj = dup
  obj.remove_associations!
  obj
end

#remove_associations!Object

Removes all association-created attributes from this instance.



11
12
13
14
15
16
17
18
19
20
# File 'lib/cs_active_record.rb', line 11

def remove_associations!
  instance_variable_set(:@attributes, attributes_before_type_cast)
  instance_variables.collect do |var|
    var[1..-1]
  end.each do |var|
    unless self.class.columns_hash.merge({"new_record" => true, "attributes" => true}).include?(var)
      instance_variable_set("@#{var}", nil)
    end
  end
end

#updateObject



349
350
351
352
353
# File 'lib/cs_active_record.rb', line 349

def update
  return cached_supermodel_update
ensure
  cache_store
end