CachedSupermodel 0.1.0
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.
- data/History.txt +5 -0
- data/Manifest.txt +9 -0
- data/README.txt +42 -0
- data/Rakefile +16 -0
- data/bin/cached_supermodel +0 -0
- data/lib/cached_supermodel.rb +7 -0
- data/lib/cs_active_record.rb +392 -0
- data/lib/cs_associations.rb +251 -0
- data/test/test_cached_supermodel.rb +0 -0
- metadata +70 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
CachedSupermodel
|
2
|
+
by Adocca AB
|
3
|
+
http://rubyforge.org/projects/adocca-plugins
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A library that overloads lots of methods in ActiveRecord::Base and ActiveRecord::Associations
|
8
|
+
to automatically cache all descendants of ActiveRecord::Base and their associations and finders
|
9
|
+
etc in memcached using the AdoccaMemcache gem.
|
10
|
+
|
11
|
+
== FEATURES/PROBLEMS:
|
12
|
+
|
13
|
+
* FIX (list of features or problems)
|
14
|
+
|
15
|
+
== SYNOPSYS:
|
16
|
+
|
17
|
+
FIX (code sample of usage)
|
18
|
+
|
19
|
+
== REQUIREMENTS:
|
20
|
+
|
21
|
+
* FIX (list of requirements)
|
22
|
+
|
23
|
+
== INSTALL:
|
24
|
+
|
25
|
+
* FIX (sudo gem install, anything else)
|
26
|
+
|
27
|
+
== LICENSE:
|
28
|
+
|
29
|
+
This program is free software; you can redistribute it and/or
|
30
|
+
modify it under the terms of the GNU General Public License
|
31
|
+
as published by the Free Software Foundation; either version 2
|
32
|
+
of the License, or (at your option) any later version.
|
33
|
+
|
34
|
+
This program is distributed in the hope that it will be useful,
|
35
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
36
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
37
|
+
GNU General Public License for more details.
|
38
|
+
|
39
|
+
You should have received a copy of the GNU General Public License
|
40
|
+
along with this program; if not, write to the Free Software
|
41
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
42
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/cached_supermodel.rb'
|
6
|
+
|
7
|
+
Hoe.new('CachedSupermodel', '0.1.0') do |p|
|
8
|
+
p.rubyforge_name = 'adocca-plugins'
|
9
|
+
p.summary = 'A library that automatically caches all ActiveRecord::Base instances in memcache using the AdoccaMemcache gem.'
|
10
|
+
p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
|
11
|
+
p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
12
|
+
p.extra_deps << ['AdoccaMemcache', '>= 0.1.0']
|
13
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
# vim: syntax=Ruby
|
File without changes
|
@@ -0,0 +1,392 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Some monkeypatches upon ActiveRecord::Base
|
5
|
+
#
|
6
|
+
class ActiveRecord::Base
|
7
|
+
#
|
8
|
+
# Removes all association-created attributes from this instance.
|
9
|
+
#
|
10
|
+
def remove_associations!
|
11
|
+
instance_variable_set(:@attributes, attributes_before_type_cast)
|
12
|
+
instance_variables.collect do |var|
|
13
|
+
var[1..-1]
|
14
|
+
end.each do |var|
|
15
|
+
unless self.class.columns_hash.merge({"new_record" => true, "attributes" => true}).include?(var)
|
16
|
+
instance_variable_set("@#{var}", nil)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
#
|
21
|
+
# Return a copy of this instance with all association-created attributes removed.
|
22
|
+
#
|
23
|
+
def remove_associations
|
24
|
+
obj = dup
|
25
|
+
obj.remove_associations!
|
26
|
+
obj
|
27
|
+
end
|
28
|
+
#
|
29
|
+
# Tell this class to use memcache_memoize to cache certain finders.
|
30
|
+
#
|
31
|
+
# The results of the finders will be automatically cached and reused (unless
|
32
|
+
# you give extra conditions, which will turn off the caching completely!).
|
33
|
+
#
|
34
|
+
# Invalidation will occur on save and destroy.
|
35
|
+
#
|
36
|
+
# Usage:
|
37
|
+
#
|
38
|
+
# class MyModel < ActiveRecord::Base
|
39
|
+
# cached_finders :find_by_id_and_name_and_something_else
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
def self.cached_finders(*args)
|
43
|
+
args.each do |finder|
|
44
|
+
if (match = finder.to_s.match(/^find_(all_)?by_(.*)$/))
|
45
|
+
attributes = match[2].to_s.split(/_and_/)
|
46
|
+
define_method("invalidate_cached_#{finder}") do
|
47
|
+
unless self.new_record?
|
48
|
+
self.class.find(self.id).send("non_recursive_invalidate_cached_#{finder}")
|
49
|
+
end
|
50
|
+
self.send("non_recursive_invalidate_cached_#{finder}")
|
51
|
+
end
|
52
|
+
define_method("non_recursive_invalidate_cached_#{finder}") do
|
53
|
+
attribute_values = attributes.collect do |attribute|
|
54
|
+
self[attribute.to_sym]
|
55
|
+
end
|
56
|
+
expire_cached_namespace(self.class.name + ":#{finder}:" + attribute_values.inspect)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
raise "Unknown finder type #{finder}"
|
60
|
+
end
|
61
|
+
|
62
|
+
class_eval "
|
63
|
+
def self.#{finder}_get_ids(*args)
|
64
|
+
match = '#{finder}'.match(/^find_(all_)?by_(.*)$/)
|
65
|
+
attributes = match[2].to_s.split(/_and_/)
|
66
|
+
normal_arguments = args[0...attributes.size]
|
67
|
+
rest = args[attributes.size..-1]
|
68
|
+
cache_value([self.name + ':#{finder}:' + normal_arguments.inspect, rest.inspect]) do
|
69
|
+
rval = method_missing(:#{finder}, *args)
|
70
|
+
if Array === rval
|
71
|
+
rval.collect(&:id)
|
72
|
+
else
|
73
|
+
rval.nil? ? nil : rval.id
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
def self.#{finder}(*args)
|
78
|
+
match = '#{finder}'.match(/^find_(all_)?by_(.*)$/)
|
79
|
+
attributes = match[2].to_s.split(/_and_/)
|
80
|
+
normal_arguments = args[0...attributes.size]
|
81
|
+
rest = args[attributes.size..-1]
|
82
|
+
extra_conditions = false
|
83
|
+
rest.each do |arg|
|
84
|
+
if Hash === arg
|
85
|
+
extra_conditions = extra_conditions || arg.include?(:conditions)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
if extra_conditions
|
89
|
+
method_missing(:#{finder}, *args)
|
90
|
+
else
|
91
|
+
rval = self.#{finder}_get_ids(*args)
|
92
|
+
begin
|
93
|
+
if Array === rval
|
94
|
+
rval.collect do |oid|
|
95
|
+
self.find(oid)
|
96
|
+
end
|
97
|
+
else
|
98
|
+
rval.nil? ? nil : self.find(rval)
|
99
|
+
end
|
100
|
+
rescue ActiveRecord::RecordNotFound
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
before_save :invalidate_cached_#{finder}
|
106
|
+
before_destroy :invalidate_cached_#{finder}
|
107
|
+
"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
@@cache_local = {}
|
112
|
+
@@use_local_cache = false
|
113
|
+
@@ttl = 60 * 15
|
114
|
+
|
115
|
+
def self.ttl
|
116
|
+
@@ttl
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.ttl=(t)
|
120
|
+
@@ttl = t
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.use_local_cache?
|
124
|
+
@@use_local_cache
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.use_local_cache=(u)
|
128
|
+
@@use_local_cache = u
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Invalidate the cache entry for an record. The update method will
|
133
|
+
# automatically invalidate the cache when updates are made through
|
134
|
+
# ActiveRecord model record. However, several methods update tables with
|
135
|
+
# direct sql queries for effeciency. These methods should call this method
|
136
|
+
# to invalidate the cache after making those changes.
|
137
|
+
#
|
138
|
+
# NOTE - if a SQL query updates multiple rows with one query, there is
|
139
|
+
# currently no way to invalidate the affected entries unless the entire
|
140
|
+
# cache is dumped or until the TTL expires, so try not to do this.
|
141
|
+
|
142
|
+
def self.cache_delete(klass, id)
|
143
|
+
self.cache_local.delete self.cache_key_local(klass, id) if self.use_local_cache?
|
144
|
+
CACHE.delete self.cache_key_memcache(klass, id)
|
145
|
+
logger.debug("deleted #{self.cache_key_memcache(klass, id)}")
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Invalidate the local process cache. This should be called from a before
|
150
|
+
# filter at the beginning of each request.
|
151
|
+
|
152
|
+
def self.cache_reset
|
153
|
+
self.cache_local.clear if self.use_local_cache?
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.ok_primary_key(key)
|
157
|
+
case self.columns_hash[self.primary_key].type
|
158
|
+
when :integer
|
159
|
+
key.is_a?(Fixnum)
|
160
|
+
when :string
|
161
|
+
key.is_a?(String)
|
162
|
+
else
|
163
|
+
raise "I dont know about this column type: #{self.columns_hash[self.primary_key_column].type}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Override the find method to look for values in the cache before going to
|
169
|
+
# the database.
|
170
|
+
|
171
|
+
class << self
|
172
|
+
alias_method :cached_supermodel_find, :find
|
173
|
+
end
|
174
|
+
|
175
|
+
def self.find(*args)
|
176
|
+
args.reject! do |arg|
|
177
|
+
arg.is_a?(Hash) && arg.values.compact.empty?
|
178
|
+
end
|
179
|
+
args[0] = args.first.to_i if args.first =~ /\A\d+\Z/
|
180
|
+
# Only handle simple find requests. If the request was more complicated,
|
181
|
+
# let the base class handle it, but store the retrieved records in the
|
182
|
+
# local cache in case we need them later.
|
183
|
+
if args.length != 1 or !ok_primary_key(args.first) then
|
184
|
+
records = cached_supermodel_find(*args)
|
185
|
+
# Rails requires two levels of indirection to look up a record
|
186
|
+
return records if args.first == :all and @skip_find_hack
|
187
|
+
case records
|
188
|
+
when Array then
|
189
|
+
records.each { |r| r.cache_store }
|
190
|
+
when ActiveRecord then
|
191
|
+
records.cache_store # Model.find 1 gets cached here
|
192
|
+
end
|
193
|
+
return records
|
194
|
+
end
|
195
|
+
|
196
|
+
# Try to find the record in the local cache.
|
197
|
+
id = args.first
|
198
|
+
if self.use_local_cache? then
|
199
|
+
record = self.cache_local[self.cache_key_local(name, id)]
|
200
|
+
return record unless record.nil?
|
201
|
+
end
|
202
|
+
|
203
|
+
# Try to find the record in memcache and add it to the local cache
|
204
|
+
record = CACHE.get self.cache_key_memcache(name, id)
|
205
|
+
unless record == :MemCache_no_such_entry then
|
206
|
+
logger.debug("found #{self.cache_key_memcache(name, id)}")
|
207
|
+
record = nil if record == :MemCache_nil
|
208
|
+
if self.use_local_cache? then
|
209
|
+
self.cache_local[self.cache_key_local(name, id)] = record
|
210
|
+
end
|
211
|
+
return record
|
212
|
+
end
|
213
|
+
|
214
|
+
# Fetch the record from the DB. Inside the multiple levels of indirection
|
215
|
+
# of find it will get cached. (no it wont, so i added a cache_store below //martin)
|
216
|
+
#
|
217
|
+
# We don't want the subsequent find_by_sql to loop back here, so guard
|
218
|
+
# the call.
|
219
|
+
#
|
220
|
+
# NOTE This guard is not thread safe, beware use of cached ActiveRecord where
|
221
|
+
# ActiveRecord's thread safety is disabled.
|
222
|
+
begin
|
223
|
+
@skip_find_hack = true
|
224
|
+
record = cached_supermodel_find(args).first
|
225
|
+
record.cache_store
|
226
|
+
ensure
|
227
|
+
@skip_find_hack = false
|
228
|
+
end
|
229
|
+
|
230
|
+
return record
|
231
|
+
end
|
232
|
+
|
233
|
+
##
|
234
|
+
# Skip the special handling for find by primary key if this method was
|
235
|
+
# called from find. If this is really a lookup for a single row by primary
|
236
|
+
# key, use a simple find call instead.
|
237
|
+
|
238
|
+
class << self
|
239
|
+
alias_method :cached_supermodel_find_by_sql, :find_by_sql
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.find_by_sql(*args)
|
243
|
+
unless @skip_find_hack
|
244
|
+
if args.first =~ /SELECT \* FROM #{table_name} WHERE \(#{table_name}\.#{primary_key} = '?(\d+)'?\) LIMIT 1/ then
|
245
|
+
return [find($1.to_i)]
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
return cached_supermodel_find_by_sql(*args)
|
250
|
+
end
|
251
|
+
|
252
|
+
##
|
253
|
+
# Delete the entry from the cache now that it isn't in the DB.
|
254
|
+
|
255
|
+
alias_method :cached_supermodel_destroy, :destroy
|
256
|
+
|
257
|
+
def destroy
|
258
|
+
return cached_supermodel_destroy
|
259
|
+
ensure
|
260
|
+
cache_delete
|
261
|
+
end
|
262
|
+
|
263
|
+
##
|
264
|
+
# Invalidate the cache for this record before reloading from the DB.
|
265
|
+
|
266
|
+
alias_method :cached_supermodel_reload, :reload
|
267
|
+
|
268
|
+
def reload
|
269
|
+
cache_delete
|
270
|
+
return cached_supermodel_reload
|
271
|
+
ensure
|
272
|
+
cache_store
|
273
|
+
end
|
274
|
+
|
275
|
+
##
|
276
|
+
# Store a new copy of ourselves into the cache.
|
277
|
+
|
278
|
+
alias_method :cached_supermodel_update, :update
|
279
|
+
|
280
|
+
def update
|
281
|
+
return cached_supermodel_update
|
282
|
+
ensure
|
283
|
+
cache_store
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
##
|
288
|
+
# Remove this record from the cache.
|
289
|
+
|
290
|
+
def cache_delete
|
291
|
+
cache_local.delete cache_key_local if self.class.use_local_cache?
|
292
|
+
CACHE.delete cache_key_memcache
|
293
|
+
logger.debug("deleted #{cache_key_memcache}")
|
294
|
+
end
|
295
|
+
|
296
|
+
##
|
297
|
+
# The local cache key for this record.
|
298
|
+
|
299
|
+
def self.cache_key_local(klass, id)
|
300
|
+
return "#{klass}:#{id}"
|
301
|
+
end
|
302
|
+
|
303
|
+
def cache_key_local
|
304
|
+
self.class.cache_key_local(self.class, self.id)
|
305
|
+
end
|
306
|
+
|
307
|
+
##
|
308
|
+
# The memcache key for this record.
|
309
|
+
|
310
|
+
def self.cache_key_memcache(klass, id)
|
311
|
+
return "active_record:#{self.cache_key_local(klass, id)}"
|
312
|
+
end
|
313
|
+
|
314
|
+
def cache_key_memcache
|
315
|
+
self.class.cache_key_memcache(self.class, self.id)
|
316
|
+
end
|
317
|
+
|
318
|
+
##
|
319
|
+
# The local object cache.
|
320
|
+
|
321
|
+
def cache_local
|
322
|
+
return self.class.cache_local
|
323
|
+
end
|
324
|
+
|
325
|
+
##
|
326
|
+
# Store this record in the cache without associations. Storing associations
|
327
|
+
# leads to wasted cache space and hard-to-debug problems.
|
328
|
+
def cache_store
|
329
|
+
if self.class.use_local_cache? then
|
330
|
+
cache_local[cache_key_local] = remove_associations
|
331
|
+
end
|
332
|
+
CACHE.set cache_key_memcache, remove_associations, self.class.ttl
|
333
|
+
logger.debug("stored #{cache_key_memcache}")
|
334
|
+
end
|
335
|
+
|
336
|
+
#
|
337
|
+
# Make increment_counter invalidate the cache
|
338
|
+
#
|
339
|
+
|
340
|
+
class << self
|
341
|
+
alias_method :cached_supermodel_increment_counter, :increment_counter
|
342
|
+
end
|
343
|
+
|
344
|
+
def self.increment_counter(counter_name, id)
|
345
|
+
begin
|
346
|
+
return cached_supermodel_increment_counter(counter_name, id)
|
347
|
+
ensure
|
348
|
+
cache_delete(self, id)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
#
|
353
|
+
# Make decrement_counter invalidate the cache
|
354
|
+
#
|
355
|
+
|
356
|
+
class << self
|
357
|
+
alias_method :cached_supermodel_decrement_counter, :decrement_counter
|
358
|
+
end
|
359
|
+
|
360
|
+
def self.decrement_counter(counter_name, id)
|
361
|
+
begin
|
362
|
+
return cached_supermodel_decrement_counter(counter_name, id)
|
363
|
+
ensure
|
364
|
+
cache_delete(self, id)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
#
|
369
|
+
# Make delete invalidate the cache
|
370
|
+
#
|
371
|
+
|
372
|
+
class << self
|
373
|
+
alias_method :cached_supermodel_delete, :delete
|
374
|
+
end
|
375
|
+
|
376
|
+
def self.delete(id)
|
377
|
+
begin
|
378
|
+
return cached_supermodel_delete(id)
|
379
|
+
ensure
|
380
|
+
if id.respond_to?(:each)
|
381
|
+
id.each do |i|
|
382
|
+
cache_delete(self.class, i)
|
383
|
+
end
|
384
|
+
else
|
385
|
+
cache_delete(self.class, id)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
|
391
|
+
end
|
392
|
+
|
@@ -0,0 +1,251 @@
|
|
1
|
+
#
|
2
|
+
# Some monkeypatches upon ActiveRecord::Associations
|
3
|
+
#
|
4
|
+
ActiveRecord::Associations::ClassMethods.class_eval do
|
5
|
+
|
6
|
+
#
|
7
|
+
# The method that is used to define has_one and belongs_to relationships.
|
8
|
+
#
|
9
|
+
def association_accessor_methods(reflection, association_proxy_class)
|
10
|
+
#
|
11
|
+
# We only handle has_one relations
|
12
|
+
#
|
13
|
+
if reflection.macro == :has_one
|
14
|
+
#
|
15
|
+
# Add invalidations
|
16
|
+
#
|
17
|
+
our_name = name
|
18
|
+
reflection.klass.class_eval do
|
19
|
+
if reflection.options[:as]
|
20
|
+
#
|
21
|
+
# Add invalidations for poly associations.
|
22
|
+
#
|
23
|
+
type_field = reflection.primary_key_name.gsub(/_id$/, "_type")
|
24
|
+
expire_proc = Proc.new do |instance|
|
25
|
+
if instance.send(type_field) == our_name
|
26
|
+
key = "#{our_name}:#{instance.send(reflection.primary_key_name)}:#{reflection.name}"
|
27
|
+
expire_cached_value(key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
else
|
31
|
+
#
|
32
|
+
# Add invalidations for normal associations.
|
33
|
+
#
|
34
|
+
expire_proc = Proc.new do |instance|
|
35
|
+
key = "#{our_name}:#{instance.send(reflection.primary_key_name)}:#{reflection.name}"
|
36
|
+
expire_cached_value(key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
before_destroy expire_proc
|
41
|
+
before_save expire_proc
|
42
|
+
|
43
|
+
var_name = "@@association_invalidations"
|
44
|
+
class_variable_set(var_name, []) unless class_variables.include?(var_name)
|
45
|
+
current_invalidations = class_variable_get(var_name)
|
46
|
+
current_invalidations << expire_proc
|
47
|
+
class_variable_set(var_name, current_invalidations)
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Add a cached getter for the association.
|
52
|
+
#
|
53
|
+
define_method(reflection.name) do |*params|
|
54
|
+
force_reload = params.first unless params.empty?
|
55
|
+
association = instance_variable_get("@#{reflection.name}")
|
56
|
+
|
57
|
+
if association.nil? || force_reload
|
58
|
+
if new_record?
|
59
|
+
assoc = association_proxy_class.new(self, reflection)
|
60
|
+
retval = assoc.reload
|
61
|
+
association = retval.nil? ? nil : assoc
|
62
|
+
else
|
63
|
+
key = "#{self.class.name}:#{self.id}:#{reflection.name}"
|
64
|
+
expire_cached_value(key) if force_reload
|
65
|
+
association = cache_value(key) do
|
66
|
+
assoc = association_proxy_class.new(self.remove_associations, reflection)
|
67
|
+
retval = assoc.reload
|
68
|
+
retval.nil? ? nil : assoc
|
69
|
+
end
|
70
|
+
end
|
71
|
+
instance_variable_set("@#{reflection.name}", association)
|
72
|
+
end
|
73
|
+
|
74
|
+
association
|
75
|
+
end
|
76
|
+
else
|
77
|
+
#
|
78
|
+
# If this wasnt has_one, do the normal stuff.
|
79
|
+
#
|
80
|
+
|
81
|
+
define_method(reflection.name) do |*params|
|
82
|
+
force_reload = params.first unless params.empty?
|
83
|
+
association = instance_variable_get("@#{reflection.name}")
|
84
|
+
|
85
|
+
if association.nil? || force_reload
|
86
|
+
association = association_proxy_class.new(self, reflection)
|
87
|
+
retval = association.reload
|
88
|
+
unless retval.nil?
|
89
|
+
instance_variable_set("@#{reflection.name}", association)
|
90
|
+
else
|
91
|
+
instance_variable_set("@#{reflection.name}", nil)
|
92
|
+
return nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
association
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Here ends our customization, the rest is just copy and paste except for a few invalidations.
|
102
|
+
#
|
103
|
+
|
104
|
+
define_method("#{reflection.name}=") do |new_value|
|
105
|
+
|
106
|
+
# Here is the magic: Invalidate the current owner before we change it.
|
107
|
+
me = self
|
108
|
+
self.class.class_eval do
|
109
|
+
class_variable_get("@@association_invalidations").each do |invalidation|
|
110
|
+
invalidation.call(me)
|
111
|
+
end if class_variables.include?("@@association_invalidations")
|
112
|
+
end
|
113
|
+
|
114
|
+
association = instance_variable_get("@#{reflection.name}")
|
115
|
+
if association.nil?
|
116
|
+
association = association_proxy_class.new(self, reflection)
|
117
|
+
end
|
118
|
+
|
119
|
+
association.replace(new_value)
|
120
|
+
|
121
|
+
unless new_value.nil?
|
122
|
+
instance_variable_set("@#{reflection.name}", association)
|
123
|
+
else
|
124
|
+
instance_variable_set("@#{reflection.name}", nil)
|
125
|
+
return nil
|
126
|
+
end
|
127
|
+
|
128
|
+
association
|
129
|
+
end
|
130
|
+
|
131
|
+
define_method("set_#{reflection.name}_target") do |target|
|
132
|
+
return if target.nil?
|
133
|
+
association = association_proxy_class.new(self, reflection)
|
134
|
+
association.target = target
|
135
|
+
instance_variable_set("@#{reflection.name}", association)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# The method that is used to define has_many relationships.
|
141
|
+
#
|
142
|
+
# Wraps the original collection_reader_method within some memcache magic
|
143
|
+
#
|
144
|
+
def collection_reader_method(reflection, association_proxy_class)
|
145
|
+
|
146
|
+
#
|
147
|
+
# If this is a through reflection, we will invalidate on changes in the
|
148
|
+
# source reflection instead of the reflection itself.
|
149
|
+
#
|
150
|
+
invalidating_reflection = reflection
|
151
|
+
invalidating_reflection = reflection.through_reflection if (is_through_reflection = reflection.through_reflection)
|
152
|
+
|
153
|
+
#
|
154
|
+
# The reflection we invalidate on will expire our cache on destroy and save.
|
155
|
+
#
|
156
|
+
our_name = name
|
157
|
+
invalidating_reflection.klass.class_eval do
|
158
|
+
if invalidating_reflection.options[:as]
|
159
|
+
#
|
160
|
+
# Polymorphic associations should only invalidate their actual object.
|
161
|
+
#
|
162
|
+
# (only if the _type field of the association is of the right class)
|
163
|
+
#
|
164
|
+
type_field = invalidating_reflection.primary_key_name.gsub(/_id$/, '_type')
|
165
|
+
expire_proc = Proc.new do |instance|
|
166
|
+
if instance.send(type_field) == our_name
|
167
|
+
key = "#{our_name}:#{instance.send(invalidating_reflection.primary_key_name)}:#{reflection.name}"
|
168
|
+
expire_cached_value(key)
|
169
|
+
key << ":cached_count"
|
170
|
+
expire_cached_value(key)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
else
|
174
|
+
#
|
175
|
+
# Non-polymorphic associations just invalidate their object regardless.
|
176
|
+
#
|
177
|
+
expire_proc = Proc.new do |instance|
|
178
|
+
key = "#{our_name}:#{instance.send(invalidating_reflection.primary_key_name)}:#{reflection.name}"
|
179
|
+
expire_cached_value(key)
|
180
|
+
key << ":cached_count"
|
181
|
+
expire_cached_value(key)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
before_destroy expire_proc
|
185
|
+
before_save expire_proc
|
186
|
+
|
187
|
+
var_name = "@@association_invalidations"
|
188
|
+
class_variable_set(var_name, []) unless class_variables.include?(var_name)
|
189
|
+
current_invalidations = class_variable_get(var_name)
|
190
|
+
current_invalidations << expire_proc
|
191
|
+
class_variable_set(var_name, current_invalidations)
|
192
|
+
end
|
193
|
+
|
194
|
+
#
|
195
|
+
# We also have to add before_remove callbacks to the owner class
|
196
|
+
#
|
197
|
+
write_inheritable_array("before_remove_for_#{reflection.name}".to_sym,
|
198
|
+
[Proc.new do |owner, record|
|
199
|
+
key = "#{owner.class.name}:#{owner.id}:#{reflection.name}"
|
200
|
+
expire_cached_value(key)
|
201
|
+
key << ":cached_count"
|
202
|
+
expire_cached_value(key)
|
203
|
+
end])
|
204
|
+
|
205
|
+
class_eval "
|
206
|
+
def self.#{reflection.name}_cached_count_for(oid)
|
207
|
+
key = self.name + ':' + oid.to_s + ':#{reflection.name}:cached_count'
|
208
|
+
cache_value(key) do
|
209
|
+
self.find(oid).#{reflection.name}_cached_count
|
210
|
+
end
|
211
|
+
end
|
212
|
+
"
|
213
|
+
|
214
|
+
#
|
215
|
+
# The getter for the cached count of this association.
|
216
|
+
#
|
217
|
+
define_method("#{reflection.name}_cached_count") do |*params|
|
218
|
+
key = "#{self.class.name}:#{self.id}:#{reflection.name}:cached_count"
|
219
|
+
cache_value(key) do
|
220
|
+
association_proxy_class.new(self.remove_associations, reflection).count
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
#
|
225
|
+
# The getter for the association will cache its value.
|
226
|
+
#
|
227
|
+
define_method(reflection.name) do |*params|
|
228
|
+
force_reload = params.first unless params.empty?
|
229
|
+
association = instance_variable_get("@#{reflection.name}")
|
230
|
+
|
231
|
+
if !association.respond_to?(:loaded?) || force_reload
|
232
|
+
if new_record?
|
233
|
+
association = association_proxy_class.new(self, reflection)
|
234
|
+
else
|
235
|
+
key = "#{self.class.name}:#{self.id}:#{reflection.name}"
|
236
|
+
expire_cached_value(key) if force_reload
|
237
|
+
association = cache_value(key) do
|
238
|
+
a = association_proxy_class.new(self.remove_associations, reflection)
|
239
|
+
a.reload
|
240
|
+
a
|
241
|
+
end
|
242
|
+
end
|
243
|
+
instance_variable_set("@#{reflection.name}", association)
|
244
|
+
end
|
245
|
+
|
246
|
+
association
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: CachedSupermodel
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2006-11-13 00:00:00 +01:00
|
8
|
+
summary: A library that automatically caches all ActiveRecord::Base instances in memcache using the AdoccaMemcache gem.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: ryand-ruby@zenspider.com
|
12
|
+
homepage: " by Adocca AB"
|
13
|
+
rubyforge_project: adocca-plugins
|
14
|
+
description: "== FEATURES/PROBLEMS: * FIX (list of features or problems) == SYNOPSYS: FIX (code sample of usage) == REQUIREMENTS:"
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- Ryan Davis
|
30
|
+
files:
|
31
|
+
- History.txt
|
32
|
+
- Manifest.txt
|
33
|
+
- README.txt
|
34
|
+
- Rakefile
|
35
|
+
- bin/cached_supermodel
|
36
|
+
- lib/cached_supermodel.rb
|
37
|
+
- lib/cs_active_record.rb
|
38
|
+
- lib/cs_associations.rb
|
39
|
+
- test/test_cached_supermodel.rb
|
40
|
+
test_files:
|
41
|
+
- test/test_cached_supermodel.rb
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
executables:
|
47
|
+
- cached_supermodel
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
dependencies:
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: hoe
|
55
|
+
version_requirement:
|
56
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.1.4
|
61
|
+
version:
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: AdoccaMemcache
|
64
|
+
version_requirement:
|
65
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.1.0
|
70
|
+
version:
|