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.



207
208
209
210
211
# File 'lib/cs_active_record.rb', line 207

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

The local cache key for this record.



364
365
366
# File 'lib/cs_active_record.rb', line 364

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

.cache_key_memcache(klass, id) ⇒ Object

The memcache key for this record.



375
376
377
# File 'lib/cs_active_record.rb', line 375

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.



217
218
219
# File 'lib/cs_active_record.rb', line 217

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
# 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

     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.sanitizeSQL([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.find(oid) rescue nil
      end.compact
    else
      if rval.nil?
        raise ActiveRecord::RecordNotFound
      else
        self.find(rval)
      end
    end
  end
end
before_save :invalidate_cached_#{finder}
before_destroy :invalidate_cached_#{finder}
"
  end
end

.cached_supermodel_decrement_counterObject



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

alias_method :cached_supermodel_decrement_counter, :decrement_counter

.cached_supermodel_deleteObject



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

alias_method :cached_supermodel_delete, :delete

.cached_supermodel_findObject



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

alias_method :cached_supermodel_find, :find

.cached_supermodel_find_by_sqlObject



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

alias_method :cached_supermodel_find_by_sql, :find_by_sql

.cached_supermodel_increment_counterObject



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

alias_method :cached_supermodel_increment_counter, :increment_counter

.decrement_counter(counter_name, id) ⇒ Object



425
426
427
428
429
430
431
# File 'lib/cs_active_record.rb', line 425

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



441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/cs_active_record.rb', line 441

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



240
241
242
243
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
# File 'lib/cs_active_record.rb', line 240

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



307
308
309
310
311
312
313
314
315
# File 'lib/cs_active_record.rb', line 307

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



409
410
411
412
413
414
415
# File 'lib/cs_active_record.rb', line 409

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



221
222
223
224
225
226
227
228
229
230
# File 'lib/cs_active_record.rb', line 221

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



180
181
182
# File 'lib/cs_active_record.rb', line 180

def self.ttl
  @@ttl
end

.ttl=(t) ⇒ Object



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

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

.use_local_cache=(u) ⇒ Object



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

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

.use_local_cache?Boolean

Returns:

  • (Boolean)


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

def self.use_local_cache?
  @@use_local_cache
end

Instance Method Details

#cache_deleteObject

Remove this record from the cache.



355
356
357
358
359
# File 'lib/cs_active_record.rb', line 355

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



368
369
370
# File 'lib/cs_active_record.rb', line 368

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

#cache_key_memcacheObject



379
380
381
# File 'lib/cs_active_record.rb', line 379

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

#cache_localObject

The local object cache.



386
387
388
# File 'lib/cs_active_record.rb', line 386

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.



393
394
395
396
397
398
399
# File 'lib/cs_active_record.rb', line 393

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.



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

alias_method :cached_supermodel_destroy, :destroy

#cached_supermodel_reloadObject

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



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

alias_method :cached_supermodel_reload, :reload

#cached_supermodel_updateObject

Store a new copy of ourselves into the cache.



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

alias_method :cached_supermodel_update, :update

#destroyObject



322
323
324
325
326
# File 'lib/cs_active_record.rb', line 322

def destroy
  return cached_supermodel_destroy
ensure
  cache_delete
end

#reloadObject



333
334
335
336
337
338
# File 'lib/cs_active_record.rb', line 333

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



345
346
347
348
349
# File 'lib/cs_active_record.rb', line 345

def update
  return cached_supermodel_update
ensure
  cache_store
end