ninjudd-record_cache 0.9.4
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/README.rdoc +35 -0
- data/VERSION.yml +4 -0
- data/lib/record_cache/index.rb +373 -0
- data/lib/record_cache/scope.rb +64 -0
- data/lib/record_cache/set.rb +141 -0
- data/lib/record_cache.rb +265 -0
- data/test/record_cache_test.rb +210 -0
- data/test/test_helper.rb +38 -0
- metadata +62 -0
data/README.rdoc
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
= RecordCache
|
2
|
+
|
3
|
+
RecordCache is a simple yet powerful extension to ActiveRecord that caches indexes
|
4
|
+
and ActiveRecord models using MemCache. If you use it correctly, it will drastically
|
5
|
+
reduce your database load.
|
6
|
+
|
7
|
+
== Usage:
|
8
|
+
|
9
|
+
class Foo < ActiveRecord
|
10
|
+
record_cache :by => :id
|
11
|
+
record_cache :id, :by => :owner_id
|
12
|
+
end
|
13
|
+
|
14
|
+
# These will use the cache now.
|
15
|
+
Foo.find(1)
|
16
|
+
Foo.find_by_id(2)
|
17
|
+
Foo.find_all_by_owner_id(3)
|
18
|
+
|
19
|
+
Invalidation is handled for you using callbacks.
|
20
|
+
|
21
|
+
== Install:
|
22
|
+
|
23
|
+
First, install the after_commit plugin from: http://github.com/ninjudd/after_commit
|
24
|
+
|
25
|
+
Then install the following gems:
|
26
|
+
|
27
|
+
sudo gem install ninjudd-deferrable -s http://gems.github.com
|
28
|
+
sudo gem install ninjudd-ordered_set -s http://gems.github.com
|
29
|
+
sudo gem install ninjudd-memcache -s http://gems.github.com
|
30
|
+
sudo gem install ninjudd-cache_version -s http://gems.github.com
|
31
|
+
sudo gem install ninjudd-record_cache -s http://gems.github.com
|
32
|
+
|
33
|
+
== License:
|
34
|
+
|
35
|
+
Copyright (c) 2009 Justin Balthrop, Geni.com; Published under The MIT License, see LICENSE
|
data/VERSION.yml
ADDED
@@ -0,0 +1,373 @@
|
|
1
|
+
module RecordCache
|
2
|
+
class Index
|
3
|
+
include Deferrable
|
4
|
+
|
5
|
+
attr_reader :model_class, :index_field, :fields, :order_by, :limit, :cache, :expiry, :name, :prefix
|
6
|
+
|
7
|
+
NULL = 'NULL'
|
8
|
+
|
9
|
+
def initialize(opts)
|
10
|
+
raise ':by => index_field required for cache' if opts[:by].nil?
|
11
|
+
raise 'explicit name or prefix required with scope' if opts[:scope] and opts[:name].nil? and opts[:prefix].nil?
|
12
|
+
|
13
|
+
@auto_name = opts[:name].nil?
|
14
|
+
@write_ahead = opts[:write_ahead]
|
15
|
+
@cache = opts[:cache] || CACHE
|
16
|
+
@expiry = opts[:expiry]
|
17
|
+
@model_class = opts[:class]
|
18
|
+
@set_class = opts[:set_class] || "#{@model_class}Set"
|
19
|
+
@index_field = opts[:by].to_s
|
20
|
+
@fields = opts[:fields].collect {|field| field.to_s}
|
21
|
+
@prefix = opts[:prefix]
|
22
|
+
@name = ( opts[:name] || [prefix, 'by', index_field].compact.join('_') ).to_s
|
23
|
+
@order_by = opts[:order_by]
|
24
|
+
@limit = opts[:limit]
|
25
|
+
@disallow_null = opts[:null] == false
|
26
|
+
@scope_query = opts[:scope] || {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def auto_name?; @auto_name; end
|
30
|
+
def write_ahead?; @write_ahead; end
|
31
|
+
def disallow_null?; @disallow_null; end
|
32
|
+
|
33
|
+
def full_record?
|
34
|
+
fields.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def includes_id?
|
38
|
+
full_record? or fields.include?('id')
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_class
|
42
|
+
@set_class.constantize
|
43
|
+
end
|
44
|
+
|
45
|
+
def namespace
|
46
|
+
"#{model_class.name}_#{model_class.version}_#{RecordCache.version}:#{name}" << ( full_record? ? '' : ":#{fields.join(',')}" )
|
47
|
+
end
|
48
|
+
|
49
|
+
def fields_hash
|
50
|
+
if @fields_hash.nil?
|
51
|
+
if full_record?
|
52
|
+
@fields_hash ||= model_class.column_names.hash
|
53
|
+
else
|
54
|
+
@fields_hash ||= fields.collect {|field| field.to_s}.hash
|
55
|
+
end
|
56
|
+
end
|
57
|
+
@fields_hash
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_by_ids(ids, model_class)
|
61
|
+
expects_array = ids.first.kind_of?(Array)
|
62
|
+
ids = ids.flatten.compact.collect {|id| id.to_i}
|
63
|
+
ids = stringify(ids)
|
64
|
+
|
65
|
+
if ids.empty?
|
66
|
+
return [] if expects_array
|
67
|
+
raise ActiveRecord::RecordNotFound, "Couldn't find #{model_class} without an ID"
|
68
|
+
end
|
69
|
+
|
70
|
+
records_by_id = get_records(ids)
|
71
|
+
|
72
|
+
models = ids.collect do |id|
|
73
|
+
records = records_by_id[id]
|
74
|
+
model = records.instantiate_first(model_class, full_record?) if records
|
75
|
+
|
76
|
+
# try to get record from db again before we throw an exception
|
77
|
+
if model.nil?
|
78
|
+
invalidate(id)
|
79
|
+
records = get_records([id])[id]
|
80
|
+
model = records.instantiate_first(model_class, full_record?) if records
|
81
|
+
end
|
82
|
+
|
83
|
+
raise ActiveRecord::RecordNotFound, "Couldn't find #{model_class} with ID #{id}" unless model
|
84
|
+
model
|
85
|
+
end
|
86
|
+
|
87
|
+
if models.size == 1 and not expects_array
|
88
|
+
models.first
|
89
|
+
else
|
90
|
+
models
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def find_by_field(keys, model_class, type)
|
95
|
+
keys = [keys] if not keys.kind_of?(Array)
|
96
|
+
keys = stringify(keys)
|
97
|
+
records_by_key = get_records(keys)
|
98
|
+
|
99
|
+
case type
|
100
|
+
when :first
|
101
|
+
keys.each do |key|
|
102
|
+
model = records_by_key[key].instantiate_first(model_class, full_record?)
|
103
|
+
return model if model
|
104
|
+
end
|
105
|
+
return nil
|
106
|
+
when :all
|
107
|
+
models = []
|
108
|
+
keys.each do |key|
|
109
|
+
models.concat( records_by_key[key].instantiate(model_class, full_record?) )
|
110
|
+
end
|
111
|
+
models
|
112
|
+
when :set, :ids
|
113
|
+
ids = []
|
114
|
+
keys.each do |key|
|
115
|
+
ids.concat( records_by_key[key].ids(model_class) )
|
116
|
+
end
|
117
|
+
type == :set ? set_class.new(ids) : ids
|
118
|
+
when :raw
|
119
|
+
raw_records = []
|
120
|
+
keys.each do |key|
|
121
|
+
raw_records.concat( records_by_key[key].records(model_class) )
|
122
|
+
end
|
123
|
+
raw_records
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def field_lookup(keys, model_class, field, flag = nil)
|
128
|
+
keys = [*keys]
|
129
|
+
keys = stringify(keys)
|
130
|
+
field = field.to_s if field
|
131
|
+
records_by_key = get_records(keys)
|
132
|
+
|
133
|
+
field_by_index = {}
|
134
|
+
all_fields = [].to_ordered_set
|
135
|
+
keys.each do |key|
|
136
|
+
records = records_by_key[key]
|
137
|
+
fields = field ? records.fields(field, model_class) : records.all_fields(model_class, :except => index_field)
|
138
|
+
if flag == :all
|
139
|
+
all_fields.concat(fields)
|
140
|
+
elsif flag == :first
|
141
|
+
next if fields.empty?
|
142
|
+
field_by_index[index_column.type_cast(key)] = fields.first
|
143
|
+
else
|
144
|
+
field_by_index[index_column.type_cast(key)] = fields
|
145
|
+
end
|
146
|
+
end
|
147
|
+
if flag == :all
|
148
|
+
all_fields.to_a
|
149
|
+
else
|
150
|
+
field_by_index
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def invalidate(*keys)
|
155
|
+
keys = stringify(keys)
|
156
|
+
cache.in_namespace(namespace) do
|
157
|
+
keys.each do |key|
|
158
|
+
cache.delete(key)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def invalidate_from_conditions_lambda(conditions)
|
164
|
+
sql = "SELECT #{index_field} FROM #{table_name} "
|
165
|
+
model_class.send(:add_conditions!, sql, conditions, model_class.send(:scope, :find))
|
166
|
+
ids = db.select_values(sql)
|
167
|
+
lambda { invalidate(*ids) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def invalidate_from_conditions(conditions)
|
171
|
+
invalidate_from_conditions_lambda(conditions).call
|
172
|
+
end
|
173
|
+
|
174
|
+
def invalidate_model(model)
|
175
|
+
attribute = model.send(index_field)
|
176
|
+
attribute_was = model.attr_was(index_field)
|
177
|
+
if scope.match_previous?(model)
|
178
|
+
if write_ahead?
|
179
|
+
remove_from_cache(model)
|
180
|
+
else
|
181
|
+
now_and_later do
|
182
|
+
invalidate(attribute_was)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
if scope.match_current?(model)
|
188
|
+
if write_ahead?
|
189
|
+
add_to_cache(model)
|
190
|
+
elsif not (scope.match_previous?(model) and attribute_was == attribute)
|
191
|
+
now_and_later do
|
192
|
+
invalidate(attribute)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def scope_query
|
199
|
+
@scope_query[:type] ||= model_class.to_s if sub_class?
|
200
|
+
@scope_query
|
201
|
+
end
|
202
|
+
|
203
|
+
def scope
|
204
|
+
@scope ||= Scope.new(model_class, scope_query)
|
205
|
+
end
|
206
|
+
|
207
|
+
@@disable_db = false
|
208
|
+
def self.disable_db
|
209
|
+
@@disable_db = true
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.enable_db
|
213
|
+
@@disable_db = false
|
214
|
+
end
|
215
|
+
|
216
|
+
def find_method_name(type)
|
217
|
+
if name =~ /(^|_)by_/
|
218
|
+
if type == :first
|
219
|
+
"find_#{name}"
|
220
|
+
else
|
221
|
+
"find_#{type}_#{name}"
|
222
|
+
end
|
223
|
+
else
|
224
|
+
case type
|
225
|
+
when :all
|
226
|
+
"find_#{name}"
|
227
|
+
when :first
|
228
|
+
"find_#{type}_#{name.singularize}"
|
229
|
+
else
|
230
|
+
"find_#{name.singularize}_#{type}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def cached_set(id)
|
236
|
+
# Used for debugging. Gives you the RecordCache::Set that is currently in the cache.
|
237
|
+
id = stringify([id]).first
|
238
|
+
cache.in_namespace(namespace) do
|
239
|
+
cache.get(id)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
|
245
|
+
MAX_FETCH = 1000
|
246
|
+
def get_records(keys)
|
247
|
+
cache.in_namespace(namespace) do
|
248
|
+
opts = {
|
249
|
+
:expiry => expiry,
|
250
|
+
:disable_write => model_class.record_cache_config[:disable_write],
|
251
|
+
:validation => lambda {|key, record_set| record_set.fields_hash == fields_hash},
|
252
|
+
}
|
253
|
+
cache.get_some(keys, opts) do |keys_to_fetch|
|
254
|
+
raise 'db access is disabled' if @@disable_db
|
255
|
+
fetched_records = {}
|
256
|
+
keys_to_fetch.each do |key|
|
257
|
+
fetched_records[key] = RecordCache::Set.new(model_class, fields_hash)
|
258
|
+
end
|
259
|
+
|
260
|
+
keys_to_fetch.each_slice(MAX_FETCH) do |keys_batch|
|
261
|
+
sql = "SELECT #{select_fields} FROM #{table_name} WHERE (#{in_clause(keys_batch)})"
|
262
|
+
sql << " AND #{scope.conditions}" if not scope.empty?
|
263
|
+
sql << " ORDER BY #{order_by}" if order_by
|
264
|
+
sql << " LIMIT #{limit}" if limit
|
265
|
+
|
266
|
+
db.select_all(sql).each do |record|
|
267
|
+
key = record[index_field] || NULL
|
268
|
+
fetched_records[key] << record
|
269
|
+
end
|
270
|
+
end
|
271
|
+
fetched_records
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def model_to_record(model)
|
277
|
+
sql = "SELECT #{select_fields} FROM #{table_name} WHERE id = #{model.id}"
|
278
|
+
db.select_all(sql).first
|
279
|
+
end
|
280
|
+
|
281
|
+
def in_clause(keys)
|
282
|
+
conditions = []
|
283
|
+
conditions << "#{index_field} IS NULL" if keys.delete(NULL)
|
284
|
+
|
285
|
+
if keys.any?
|
286
|
+
values = keys.collect {|value| quote_index_value(value)}.join(',')
|
287
|
+
conditions << "#{index_field} IN (#{values})"
|
288
|
+
end
|
289
|
+
conditions.join(' OR ')
|
290
|
+
end
|
291
|
+
|
292
|
+
def remove_from_cache(model)
|
293
|
+
record = model.attributes
|
294
|
+
key = model.attr_was(index_field)
|
295
|
+
|
296
|
+
cache.in_namespace(namespace) do
|
297
|
+
cache.with_lock(key) do
|
298
|
+
now_and_later do
|
299
|
+
if records = cache.get(key)
|
300
|
+
records.delete(record)
|
301
|
+
cache.set(key, records)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def add_to_cache(model)
|
309
|
+
record = model_to_record(model)
|
310
|
+
key = record[index_field].to_s
|
311
|
+
|
312
|
+
cache.in_namespace(namespace) do
|
313
|
+
cache.with_lock(key) do
|
314
|
+
now_and_later do
|
315
|
+
if records = cache.get(key)
|
316
|
+
records.delete(record)
|
317
|
+
records << record
|
318
|
+
records.sort!(order_by) if order_by
|
319
|
+
records.limit!(limit) if limit
|
320
|
+
cache.set(key, records)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def select_fields
|
328
|
+
if @select_fields.nil?
|
329
|
+
if full_record?
|
330
|
+
@select_fields = model_class.respond_to?(:default_select, true) ? model_class.send(:default_select, nil) : '*'
|
331
|
+
else
|
332
|
+
@select_fields = [index_field, 'id'] + fields
|
333
|
+
@select_fields << 'type' if base_class?
|
334
|
+
@select_fields = @select_fields.uniq.join(', ')
|
335
|
+
end
|
336
|
+
end
|
337
|
+
@select_fields
|
338
|
+
end
|
339
|
+
|
340
|
+
def base_class?
|
341
|
+
@base_class ||= single_table_inheritance? and model_class == model_class.base_class
|
342
|
+
end
|
343
|
+
|
344
|
+
def sub_class?
|
345
|
+
@sub_class ||= single_table_inheritance? and model_class != model_class.base_class
|
346
|
+
end
|
347
|
+
|
348
|
+
def single_table_inheritance?
|
349
|
+
@single_table_inheritance ||= model_class.columns_hash.has_key?(model_class.inheritance_column)
|
350
|
+
end
|
351
|
+
|
352
|
+
def quote_index_value(value)
|
353
|
+
model_class.quote_value(value, index_column)
|
354
|
+
end
|
355
|
+
|
356
|
+
def index_column
|
357
|
+
@index_column ||= model_class.columns_hash[index_field]
|
358
|
+
end
|
359
|
+
|
360
|
+
def table_name
|
361
|
+
model_class.table_name
|
362
|
+
end
|
363
|
+
|
364
|
+
def stringify(keys)
|
365
|
+
keys.compact! if disallow_null?
|
366
|
+
keys.collect {|key| key.nil? ? NULL : key.to_s}.uniq
|
367
|
+
end
|
368
|
+
|
369
|
+
def db
|
370
|
+
RecordCache.db(model_class)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module RecordCache
|
2
|
+
class Scope
|
3
|
+
attr_reader :model_class, :query
|
4
|
+
|
5
|
+
def initialize(model_class, query)
|
6
|
+
@model_class = model_class
|
7
|
+
@query = query
|
8
|
+
end
|
9
|
+
|
10
|
+
def empty?
|
11
|
+
query.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
def fields
|
15
|
+
query.keys
|
16
|
+
end
|
17
|
+
|
18
|
+
def match_current?(model)
|
19
|
+
fields.all? do |field|
|
20
|
+
match?( field, model.send(field) )
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def match_previous?(model)
|
25
|
+
fields.all? do |field|
|
26
|
+
match?( field, model.attr_was(field) )
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def match?(field, value)
|
31
|
+
scope = query[field]
|
32
|
+
if defined?(AntiObject) and scope.kind_of?(AntiObject)
|
33
|
+
scope = ~scope
|
34
|
+
invert = true
|
35
|
+
end
|
36
|
+
|
37
|
+
match = [*scope].include?(value)
|
38
|
+
invert ? !match : match
|
39
|
+
end
|
40
|
+
|
41
|
+
def conditions
|
42
|
+
@conditions ||= begin
|
43
|
+
query.collect do |field, scope|
|
44
|
+
if defined?(AntiObject) and scope.kind_of?(AntiObject)
|
45
|
+
scope = ~scope
|
46
|
+
invert = true
|
47
|
+
end
|
48
|
+
|
49
|
+
if scope.nil?
|
50
|
+
op = invert ? 'IS NOT' : 'IS'
|
51
|
+
"#{field} #{op} NULL"
|
52
|
+
elsif scope.is_a?(Array)
|
53
|
+
op = invert ? 'NOT IN' : 'IN'
|
54
|
+
model_class.send(:sanitize_sql, ["#{field} #{op} (?)", scope])
|
55
|
+
else
|
56
|
+
op = invert ? '!=' : '='
|
57
|
+
model_class.send(:sanitize_sql, ["#{field} #{op} ?", scope])
|
58
|
+
end
|
59
|
+
end.join(' AND ')
|
60
|
+
end
|
61
|
+
@conditions
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module RecordCache
|
2
|
+
class Set
|
3
|
+
attr_reader :model_class, :fields_hash, :created_at, :updated_at, :hostname, :dbhost
|
4
|
+
|
5
|
+
def self.source_tracking?
|
6
|
+
@source_tracking
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.source_tracking=(value)
|
10
|
+
@source_tracking = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.hostname
|
14
|
+
@hostname ||= Socket.gethostname
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(model_class, fields_hash = nil)
|
18
|
+
raise 'valid model class required' unless model_class
|
19
|
+
@model_class = model_class
|
20
|
+
@fields_hash = fields_hash
|
21
|
+
@records_by_type = {}
|
22
|
+
|
23
|
+
if self.class.source_tracking?
|
24
|
+
@created_at = Time.now
|
25
|
+
@hostname = self.class.hostname
|
26
|
+
@dbhost = RecordCache.db(model_class).instance_variable_get(:@config)[:host]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def sort!(order_by)
|
31
|
+
field, order = order_by.strip.squeeze.split
|
32
|
+
descending = (order == 'DESC')
|
33
|
+
@records_by_type.values.each do |records|
|
34
|
+
sorted_records = records.sort_by do |record|
|
35
|
+
type_cast(field, record[field])
|
36
|
+
end
|
37
|
+
sorted_records.reverse! if descending
|
38
|
+
records.replace(sorted_records)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def limit!(limit)
|
43
|
+
all_records = records
|
44
|
+
if all_records.length > limit
|
45
|
+
removed_records = all_records.slice!(limit..-1)
|
46
|
+
removed_records.each do |record|
|
47
|
+
type = record['type']
|
48
|
+
records_by_type(type).delete(record) if type
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def <<(record)
|
54
|
+
mark_updated!
|
55
|
+
record_type = record['type']
|
56
|
+
record['id'] = record['id'].to_i if record.has_key?('id')
|
57
|
+
|
58
|
+
[record_type, model_class.to_s].uniq.each do |type|
|
59
|
+
records_by_type(type) << record
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete(record)
|
64
|
+
raise 'cannot delete record without id' unless record.has_key?('id')
|
65
|
+
mark_updated!
|
66
|
+
record_type = record['type']
|
67
|
+
id = record['id'].to_i
|
68
|
+
|
69
|
+
[record_type, model_class.to_s].uniq.each do |type|
|
70
|
+
records_by_type(type).reject! {|r| r['id'] == id}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def records_by_type(type)
|
75
|
+
@records_by_type[type.to_s] ||= []
|
76
|
+
end
|
77
|
+
|
78
|
+
def records(type = model_class)
|
79
|
+
records_by_type(type)
|
80
|
+
end
|
81
|
+
|
82
|
+
def size
|
83
|
+
records.size
|
84
|
+
end
|
85
|
+
|
86
|
+
def empty?
|
87
|
+
records.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
def ids(type = model_class)
|
91
|
+
records(type).collect {|r| r['id']}
|
92
|
+
end
|
93
|
+
|
94
|
+
def fields(field, type)
|
95
|
+
records(type).collect {|r| type_cast(field, r[field])}
|
96
|
+
end
|
97
|
+
|
98
|
+
def all_fields(type = model_class, opts = {})
|
99
|
+
records(type).collect do |r|
|
100
|
+
record = {}
|
101
|
+
r.each do |field, value|
|
102
|
+
next if field == opts[:except]
|
103
|
+
record[field.to_sym] = type_cast(field, value)
|
104
|
+
end
|
105
|
+
record
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def instantiate_first(type = model_class, full_record = false)
|
110
|
+
if full_record
|
111
|
+
record = records(type).first
|
112
|
+
type.send(:instantiate, record) if record
|
113
|
+
else
|
114
|
+
id = ids(type).first
|
115
|
+
type.find_by_id(id) if id
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def instantiate(type = model_class, full_record = false)
|
120
|
+
if full_record
|
121
|
+
records(type).collect do |record|
|
122
|
+
type.send(:instantiate, record)
|
123
|
+
end
|
124
|
+
else
|
125
|
+
type.find_all_by_id(ids(type))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def mark_updated!
|
132
|
+
@updated_at = Time.now if self.class.source_tracking?
|
133
|
+
end
|
134
|
+
|
135
|
+
def type_cast(field, value)
|
136
|
+
column = model_class.columns_hash[field.to_s]
|
137
|
+
raise 'column not found in #{model_class} for field #{field}' unless column
|
138
|
+
column.type_cast(value)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/record_cache.rb
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_record'
|
3
|
+
require 'ordered_set'
|
4
|
+
require 'memcache_extended'
|
5
|
+
require 'cache_version'
|
6
|
+
require 'deferrable'
|
7
|
+
|
8
|
+
$:.unshift(File.dirname(__FILE__))
|
9
|
+
require 'record_cache/index'
|
10
|
+
require 'record_cache/set'
|
11
|
+
require 'record_cache/scope'
|
12
|
+
|
13
|
+
module RecordCache
|
14
|
+
def self.config(opts = nil)
|
15
|
+
if opts
|
16
|
+
config.merge!(opts)
|
17
|
+
else
|
18
|
+
@config ||= {}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.db(model_class)
|
23
|
+
# Always use the master connection since we are caching.
|
24
|
+
db = model_class.connection
|
25
|
+
if defined?(DataFabric::ConnectionProxy) and db.kind_of?(DataFabric::ConnectionProxy)
|
26
|
+
model_class.record_cache_config[:use_slave] ? db.send(:connection) : db.send(:master)
|
27
|
+
else
|
28
|
+
db
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
def invalidate_record_cache
|
34
|
+
self.class.each_cached_index do |index|
|
35
|
+
index.invalidate_model(self)
|
36
|
+
index.clear_deferred
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def invalidate_record_cache_deferred
|
41
|
+
self.class.each_cached_index do |index|
|
42
|
+
# Have to invalidate both before and after commit.
|
43
|
+
index.invalidate_model(self)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def complete_deferred_record_cache_invalidations
|
48
|
+
self.class.each_cached_index do |index|
|
49
|
+
index.complete_deferred
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def attr_was(attr)
|
54
|
+
attr = attr.to_s
|
55
|
+
['id', 'type'].include?(attr) ? send(attr) : send(:attribute_was, attr)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module ClassMethods
|
60
|
+
def find_with_caching(*args, &block)
|
61
|
+
if args.last.is_a?(Hash)
|
62
|
+
args.last.delete_if {|k,v| v.nil?}
|
63
|
+
args.pop if args.last.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
if [:all, :first, :last].include?(args.first)
|
67
|
+
opts = args.last
|
68
|
+
if opts.is_a?(Hash) and opts.keys == [:conditions]
|
69
|
+
# Try to match the SQL.
|
70
|
+
if opts[:conditions] =~ /^"?#{table_name}"?.(\w*) = (\d*)$/
|
71
|
+
field, value = $1, $2
|
72
|
+
index = cached_index("by_#{field}")
|
73
|
+
return index.find_by_field([value], self, args.first) if index
|
74
|
+
end
|
75
|
+
end
|
76
|
+
elsif not args.last.is_a?(Hash)
|
77
|
+
# This is a find with just ids.
|
78
|
+
index = cached_index('by_id')
|
79
|
+
return index.find_by_ids(args, self) if index
|
80
|
+
end
|
81
|
+
|
82
|
+
find_without_caching(*args, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def update_all_with_invalidate(updates, conditions = nil)
|
86
|
+
invalidate_from_conditions(conditions, :update) do |conditions|
|
87
|
+
update_all_without_invalidate(updates, conditions)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def delete_all_with_invalidate(conditions = nil)
|
92
|
+
invalidate_from_conditions(conditions) do |conditions|
|
93
|
+
delete_all_without_invalidate(conditions)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def invalidate_from_conditions(conditions, flag = nil)
|
98
|
+
if conditions.nil?
|
99
|
+
# Just invalidate all indexes.
|
100
|
+
result = yield(nil)
|
101
|
+
self.increment_version
|
102
|
+
return result
|
103
|
+
end
|
104
|
+
|
105
|
+
# Freeze ids to avoid race conditions.
|
106
|
+
sql = "SELECT id FROM #{table_name} "
|
107
|
+
self.send(:add_conditions!, sql, conditions, self.send(:scope, :find))
|
108
|
+
ids = RecordCache.db(self).select_values(sql)
|
109
|
+
|
110
|
+
return if ids.empty?
|
111
|
+
conditions = "id IN (#{ids.join(',')})"
|
112
|
+
|
113
|
+
if block_given?
|
114
|
+
# Capture the ids to invalidate in lambdas.
|
115
|
+
lambdas = []
|
116
|
+
each_cached_index do |index|
|
117
|
+
lambdas << index.invalidate_from_conditions_lambda(conditions)
|
118
|
+
end
|
119
|
+
|
120
|
+
result = yield(conditions)
|
121
|
+
|
122
|
+
# Finish invalidating with prior attributes.
|
123
|
+
lambdas.each {|l| l.call}
|
124
|
+
end
|
125
|
+
|
126
|
+
# Invalidate again afterwards if we are updating (or for the first time if no block was given).
|
127
|
+
if flag == :update or not block_given?
|
128
|
+
each_cached_index do |index|
|
129
|
+
index.invalidate_from_conditions(conditions)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
def cached_indexes
|
137
|
+
@cached_indexes ||= {}
|
138
|
+
end
|
139
|
+
|
140
|
+
def cached_index(name)
|
141
|
+
name = name.to_s
|
142
|
+
index = cached_indexes[name]
|
143
|
+
index ||= base_class.cached_index(name) if base_class != self and base_class.respond_to?(:cached_index)
|
144
|
+
index
|
145
|
+
end
|
146
|
+
|
147
|
+
def add_cached_index(index)
|
148
|
+
name = index.name
|
149
|
+
count = nil
|
150
|
+
# Make sure the key is unique.
|
151
|
+
while cached_indexes["#{name}#{count}"]
|
152
|
+
count ||= 0
|
153
|
+
count += 1
|
154
|
+
end
|
155
|
+
cached_indexes["#{name}#{count}"] = index
|
156
|
+
end
|
157
|
+
|
158
|
+
def each_cached_index
|
159
|
+
cached_index_names.each do |index_name|
|
160
|
+
yield cached_index(index_name)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def cached_index_names
|
165
|
+
names = cached_indexes.keys
|
166
|
+
names.concat(base_class.cached_index_names) if base_class != self and base_class.respond_to?(:cached_index_names)
|
167
|
+
names.uniq
|
168
|
+
end
|
169
|
+
|
170
|
+
def record_cache_config(opts = nil)
|
171
|
+
if opts
|
172
|
+
record_cache_config.merge!(opts)
|
173
|
+
else
|
174
|
+
@record_cache_config ||= RecordCache.config.clone
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
module ActiveRecordExtension
|
180
|
+
def self.extended(mod)
|
181
|
+
mod.send(:class_inheritable_accessor, :cached_indexes)
|
182
|
+
end
|
183
|
+
|
184
|
+
def record_cache(*args)
|
185
|
+
extend RecordCache::ClassMethods
|
186
|
+
include RecordCache::InstanceMethods
|
187
|
+
|
188
|
+
opts = args.pop
|
189
|
+
opts[:fields] = args
|
190
|
+
opts[:class] = self
|
191
|
+
field_lookup = opts.delete(:field_lookup) || []
|
192
|
+
|
193
|
+
index = RecordCache::Index.new(opts)
|
194
|
+
add_cached_index(index)
|
195
|
+
first_index = (cached_indexes.size == 1)
|
196
|
+
|
197
|
+
(class << self; self; end).module_eval do
|
198
|
+
if index.includes_id?
|
199
|
+
[:first, :all, :set, :raw, :ids].each do |type|
|
200
|
+
next if type == :ids and index.name == 'by_id'
|
201
|
+
define_method( index.find_method_name(type) ) do |keys|
|
202
|
+
if self.send(:scope,:find) and self.send(:scope,:find).any?
|
203
|
+
self.method_missing(index.find_method_name(type), keys)
|
204
|
+
else
|
205
|
+
index.find_by_field(keys, self, type)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
if not index.auto_name? and not index.full_record?
|
212
|
+
field = index.fields.first if index.fields.size == 1
|
213
|
+
|
214
|
+
define_method( "all_#{index.name.pluralize}_by_#{index.index_field}" ) do |keys|
|
215
|
+
index.field_lookup(keys, self, field, :all)
|
216
|
+
end
|
217
|
+
|
218
|
+
define_method( "#{index.name.pluralize}_by_#{index.index_field}" ) do |keys|
|
219
|
+
index.field_lookup(keys, self, field)
|
220
|
+
end
|
221
|
+
|
222
|
+
define_method( "#{index.name.singularize}_by_#{index.index_field}" ) do |keys|
|
223
|
+
index.field_lookup(keys, self, field, :first)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
if index.auto_name?
|
228
|
+
(field_lookup + index.fields).each do |field|
|
229
|
+
next if field == index.index_field
|
230
|
+
plural_field = field.pluralize
|
231
|
+
prefix = index.prefix
|
232
|
+
prefix = "#{prefix}_" if prefix
|
233
|
+
|
234
|
+
define_method( "all_#{prefix}#{plural_field}_by_#{index.index_field}" ) do |keys|
|
235
|
+
index.field_lookup(keys, self, field, :all)
|
236
|
+
end
|
237
|
+
|
238
|
+
define_method( "#{prefix}#{plural_field}_by_#{index.index_field}" ) do |keys|
|
239
|
+
index.field_lookup(keys, self, field)
|
240
|
+
end
|
241
|
+
|
242
|
+
define_method( "#{prefix}#{field}_by_#{index.index_field}" ) do |keys|
|
243
|
+
index.field_lookup(keys, self, field, :first)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
if first_index
|
249
|
+
alias_method_chain :find, :caching
|
250
|
+
alias_method_chain :update_all, :invalidate
|
251
|
+
alias_method_chain :delete_all, :invalidate
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
if first_index
|
256
|
+
after_save :invalidate_record_cache_deferred
|
257
|
+
after_destroy :invalidate_record_cache_deferred
|
258
|
+
after_commit :complete_deferred_record_cache_invalidations
|
259
|
+
after_rollback :complete_deferred_record_cache_invalidations
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
ActiveRecord::Base.send(:extend, RecordCache::ActiveRecordExtension)
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
RecordCache::Set.source_tracking = true
|
4
|
+
|
5
|
+
class CreateTables < ActiveRecord::Migration
|
6
|
+
def self.up
|
7
|
+
down
|
8
|
+
create_table :pets do |t|
|
9
|
+
t.column :breed_id, :bigint
|
10
|
+
t.column :name, :string
|
11
|
+
t.column :color_id, :bigint
|
12
|
+
t.column :sex, :char
|
13
|
+
t.column :type, :string
|
14
|
+
end
|
15
|
+
|
16
|
+
create_table :breeds do |t|
|
17
|
+
t.column :name, :string
|
18
|
+
end
|
19
|
+
|
20
|
+
create_table :colors do |t|
|
21
|
+
t.column :name, :string
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.down
|
26
|
+
drop_table :pets rescue nil
|
27
|
+
drop_table :breeds rescue nil
|
28
|
+
drop_table :colors rescue nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
CreateTables.up
|
33
|
+
|
34
|
+
class Pet < ActiveRecord::Base
|
35
|
+
belongs_to :breed
|
36
|
+
belongs_to :color
|
37
|
+
|
38
|
+
record_cache :by => :id
|
39
|
+
record_cache :id, :by => :breed_id
|
40
|
+
record_cache :id, :by => :color_id, :write_ahead => true
|
41
|
+
|
42
|
+
record_cache :id, :by => :color_id, :scope => {:sex => 'm'}, :prefix => 'male'
|
43
|
+
record_cache :id, :by => :color_id, :scope => {:sex => 'f'}, :prefix => 'female'
|
44
|
+
record_cache :id, :by => :color_id, :scope => {:sex => ['m','f']}, :name => 'all_colors'
|
45
|
+
end
|
46
|
+
|
47
|
+
class Dog < Pet
|
48
|
+
end
|
49
|
+
|
50
|
+
class Cat < Pet
|
51
|
+
end
|
52
|
+
|
53
|
+
class Breed < ActiveRecord::Base
|
54
|
+
end
|
55
|
+
|
56
|
+
class Color < ActiveRecord::Base
|
57
|
+
end
|
58
|
+
|
59
|
+
class RecordCacheTest < Test::Unit::TestCase
|
60
|
+
context "With a memcache and db connection" do
|
61
|
+
setup do
|
62
|
+
system('memcached -d')
|
63
|
+
CACHE.servers = ["localhost:11211"]
|
64
|
+
|
65
|
+
CreateTables.up
|
66
|
+
CacheVersionMigration.up
|
67
|
+
end
|
68
|
+
|
69
|
+
teardown do
|
70
|
+
system('killall memcached')
|
71
|
+
|
72
|
+
CreateTables.down
|
73
|
+
CacheVersionMigration.down
|
74
|
+
RecordCache::Index.enable_db
|
75
|
+
end
|
76
|
+
|
77
|
+
should 'create field lookup functions' do
|
78
|
+
dog = Breed.new(:name => 'pitbull retriever')
|
79
|
+
cat = Breed.new(:name => 'house cat')
|
80
|
+
willy = Cat.create(:name => 'Willy', :breed => cat)
|
81
|
+
daisy = Dog.create(:name => 'Daisy', :breed => dog)
|
82
|
+
|
83
|
+
expected = {dog.id => daisy.id, cat.id => willy.id}
|
84
|
+
assert_equal expected, Pet.id_by_breed_id([dog.id, cat.id, 100, 101])
|
85
|
+
end
|
86
|
+
|
87
|
+
should 'return cached values without accessing the database' do
|
88
|
+
color = Color.new(:name => 'black & white')
|
89
|
+
dog = Breed.new(:name => 'pitbull retriever')
|
90
|
+
cat = Breed.new(:name => 'house cat')
|
91
|
+
daisy = Dog.create(:name => 'Daisy', :color => color, :breed => dog)
|
92
|
+
willy = Cat.create(:name => 'Willy', :color => color, :breed => cat)
|
93
|
+
|
94
|
+
Pet.find(daisy.id, willy.id)
|
95
|
+
Dog.find_all_by_color_id(color.id)
|
96
|
+
Dog.find_all_by_breed_id(dog.id)
|
97
|
+
|
98
|
+
RecordCache::Index.disable_db
|
99
|
+
|
100
|
+
assert_equal Dog, Dog.find(daisy.id).class
|
101
|
+
assert_equal daisy, Dog.find(daisy.id)
|
102
|
+
assert_equal Cat, Cat.find(willy.id).class
|
103
|
+
assert_equal willy, Cat.find(willy.id)
|
104
|
+
assert_equal [daisy], Dog.find_all_by_color_id(color.id)
|
105
|
+
assert_equal [willy], Cat.find_all_by_color_id(color.id)
|
106
|
+
assert_equal [daisy], Dog.find_all_by_breed_id(dog.id)
|
107
|
+
|
108
|
+
RecordCache::Index.enable_db
|
109
|
+
|
110
|
+
assert_raises(ActiveRecord::RecordNotFound) do
|
111
|
+
Dog.find(willy.id)
|
112
|
+
end
|
113
|
+
|
114
|
+
assert_raises(ActiveRecord::RecordNotFound) do
|
115
|
+
Cat.find(daisy.id)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
should 'return multiple cached values without accessing the database' do
|
120
|
+
color1 = Color.new(:name => 'black & white')
|
121
|
+
color2 = Color.new(:name => 'speckled')
|
122
|
+
breed1 = Breed.new(:name => 'pitbull retriever')
|
123
|
+
breed2 = Breed.new(:name => 'pitbull terrier')
|
124
|
+
daisy = Dog.create(:name => 'Daisy', :color => color1, :breed => breed1)
|
125
|
+
sammy = Dog.create(:name => 'Sammy', :color => color1, :breed => breed2)
|
126
|
+
|
127
|
+
Dog.find(daisy.id, sammy.id)
|
128
|
+
Dog.find_all_by_color_id(color1.id)
|
129
|
+
Dog.find_all_by_breed_id([breed1.id, breed2.id])
|
130
|
+
|
131
|
+
RecordCache::Index.disable_db
|
132
|
+
|
133
|
+
assert_equal [daisy, sammy].to_set, Dog.find(daisy.id, sammy.id).to_set
|
134
|
+
assert_equal [daisy, sammy].to_set, Dog.find_all_by_color_id(color1.id).to_set
|
135
|
+
assert_equal [daisy, sammy].to_set, Dog.find_all_by_breed_id([breed1.id, breed2.id]).to_set
|
136
|
+
assert_equal [sammy, daisy].to_set, Dog.find_all_by_breed_id([breed2.id, breed1.id]).to_set
|
137
|
+
assert_equal [daisy].to_set, Dog.find_all_by_breed_id(breed1.id).to_set
|
138
|
+
|
139
|
+
# Alternate find methods.
|
140
|
+
#assert_equal [sammy.id, daisy.id], Dog.find_set_by_breed_id([breed2.id, breed1.id]).ids
|
141
|
+
assert_equal [sammy.id, daisy.id].to_set, Dog.find_ids_by_breed_id([breed2.id, breed1.id]).to_set
|
142
|
+
|
143
|
+
assert_equal daisy, Dog.find_by_color_id(color1.id)
|
144
|
+
assert_equal daisy, Dog.find_by_breed_id([breed1.id, breed2.id])
|
145
|
+
assert_equal sammy, Dog.find_by_breed_id([breed2.id, breed1.id])
|
146
|
+
|
147
|
+
baseball = Dog.create(:name => 'Baseball', :color => color2, :breed => breed1)
|
148
|
+
|
149
|
+
RecordCache::Index.enable_db
|
150
|
+
|
151
|
+
assert_equal [daisy, baseball], Dog.find_all_by_breed_id(breed1.id)
|
152
|
+
end
|
153
|
+
|
154
|
+
should 'create raw find methods' do
|
155
|
+
daisy = Dog.create(:name => 'Daisy')
|
156
|
+
sammy = Dog.create(:name => 'Sammy')
|
157
|
+
|
158
|
+
Dog.find(daisy.id, sammy.id)
|
159
|
+
RecordCache::Index.disable_db
|
160
|
+
|
161
|
+
raw_records = Dog.find_raw_by_id([sammy.id, daisy.id])
|
162
|
+
assert_equal ['Sammy', 'Daisy'], raw_records.collect {|r| r['name']}
|
163
|
+
end
|
164
|
+
|
165
|
+
should 'cache indexes using scope' do
|
166
|
+
color = Color.new(:name => 'black & white')
|
167
|
+
breed1 = Breed.new(:name => 'pitbull retriever')
|
168
|
+
breed2 = Breed.new(:name => 'pitbull terrier')
|
169
|
+
daisy = Dog.create(:name => 'Daisy', :color => color, :breed => breed1, :sex => 'f')
|
170
|
+
sammy = Dog.create(:name => 'Sammy', :color => color, :breed => breed2, :sex => 'm')
|
171
|
+
|
172
|
+
assert_equal [sammy], Dog.find_all_male_by_color_id(color.id)
|
173
|
+
assert_equal [daisy], Dog.find_all_female_by_color_id(color.id)
|
174
|
+
assert_equal [daisy, sammy], Dog.find_all_colors(color.id)
|
175
|
+
|
176
|
+
cousin = Dog.create(:name => 'Cousin', :color => color, :breed => breed2, :sex => 'm')
|
177
|
+
|
178
|
+
assert_equal [sammy, cousin], Dog.find_all_male_by_color_id(color.id)
|
179
|
+
assert_equal [daisy, sammy, cousin], Dog.find_all_colors(color.id)
|
180
|
+
end
|
181
|
+
|
182
|
+
should 'yield cached indexes' do
|
183
|
+
count = 0
|
184
|
+
Dog.each_cached_index do |index|
|
185
|
+
count += 1
|
186
|
+
end
|
187
|
+
assert_equal 6, count
|
188
|
+
end
|
189
|
+
|
190
|
+
should 'invalidate indexes on save' do
|
191
|
+
b_w = Color.new(:name => 'black & white')
|
192
|
+
brown = Color.new(:name => 'brown')
|
193
|
+
breed = Breed.new(:name => 'mutt')
|
194
|
+
daisy = Dog.create(:name => 'Daisy', :color => b_w, :breed => breed, :sex => 'f')
|
195
|
+
|
196
|
+
assert_equal daisy, Dog.find_by_color_id(b_w.id)
|
197
|
+
|
198
|
+
daisy.name = 'Molly'
|
199
|
+
daisy.color = brown
|
200
|
+
daisy.save
|
201
|
+
|
202
|
+
assert_equal 'Molly', daisy.name
|
203
|
+
assert_equal brown.id, daisy.color_id
|
204
|
+
|
205
|
+
assert_equal daisy, Dog.find_by_color_id(brown.id)
|
206
|
+
assert_equal nil, Dog.find_by_color_id(b_w.id)
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'mocha'
|
5
|
+
|
6
|
+
require 'active_record'
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
|
9
|
+
['deep_clonable', 'ordered_set', 'cache_version', 'model_set', 'memcache', 'deferrable'].each do |dir|
|
10
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../../#{dir}/lib"
|
11
|
+
end
|
12
|
+
|
13
|
+
['lib/after_commit', 'lib/after_commit/active_record', 'lib/after_commit/connection_adapters', 'init'].each do |file|
|
14
|
+
require File.dirname(__FILE__) + "/../../after_commit/#{file}"
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'record_cache'
|
18
|
+
|
19
|
+
class Test::Unit::TestCase
|
20
|
+
end
|
21
|
+
|
22
|
+
CACHE = MemCache.new(
|
23
|
+
:ttl=>1800,
|
24
|
+
:compression=>false,
|
25
|
+
:readonly=>false,
|
26
|
+
:debug=>false,
|
27
|
+
:c_threshold=>10000,
|
28
|
+
:urlencode=>false
|
29
|
+
)
|
30
|
+
ActiveRecord::Base.establish_connection(
|
31
|
+
:adapter => "postgresql",
|
32
|
+
:host => "localhost",
|
33
|
+
:username => "postgres",
|
34
|
+
:password => "",
|
35
|
+
:database => "record_cache_test"
|
36
|
+
)
|
37
|
+
ActiveRecord::Migration.verbose = false
|
38
|
+
ActiveRecord::Base.connection.client_min_messages = 'panic'
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ninjudd-record_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Balthrop
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-15 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Active Record caching and indexing in memcache
|
17
|
+
email: code@justinbalthrop.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- README.rdoc
|
26
|
+
- VERSION.yml
|
27
|
+
- lib/record_cache
|
28
|
+
- lib/record_cache/index.rb
|
29
|
+
- lib/record_cache/scope.rb
|
30
|
+
- lib/record_cache/set.rb
|
31
|
+
- lib/record_cache.rb
|
32
|
+
- test/record_cache_test.rb
|
33
|
+
- test/test_helper.rb
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://github.com/ninjudd/record_cache
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options:
|
38
|
+
- --inline-source
|
39
|
+
- --charset=UTF-8
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
version:
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.2.0
|
58
|
+
signing_key:
|
59
|
+
specification_version: 2
|
60
|
+
summary: Active Record caching and indexing in memcache. An alternative to cache_fu
|
61
|
+
test_files: []
|
62
|
+
|