record-cache 0.1.0 → 0.1.1
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.
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
# basic Record Cache functionality
|
4
|
+
ActiveRecord::Base.send(:include, RecordCache::Base)
|
5
|
+
|
6
|
+
# To be able to fetch records from the cache and invalidate records in the cache
|
7
|
+
# some internal Active Record methods needs to be aliased.
|
8
|
+
# The downside of using internal methods, is that they may change in different releases,
|
9
|
+
# hence the following code:
|
10
|
+
AR_VERSION = ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0 ? 30 : 31
|
11
|
+
require File.dirname(__FILE__) + "/active_record_#{AR_VERSION}.rb"
|
@@ -191,10 +191,9 @@ module RecordCache
|
|
191
191
|
alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
|
192
192
|
|
193
193
|
def visit_Arel_Nodes_Equality o
|
194
|
-
|
195
|
-
# equality
|
196
|
-
|
197
|
-
@query.where(equality.first, equality.last)
|
194
|
+
key, value = visit(o.left), visit(o.right)
|
195
|
+
# p " =====> equality found: #{key.inspect}@#{key.class.name} => #{value.inspect}@#{value.class.name}"
|
196
|
+
@query.where(key, value)
|
198
197
|
end
|
199
198
|
alias :visit_Arel_Nodes_In :visit_Arel_Nodes_Equality
|
200
199
|
|
@@ -247,6 +246,10 @@ module RecordCache
|
|
247
246
|
end
|
248
247
|
end
|
249
248
|
|
249
|
+
end
|
250
|
+
|
251
|
+
module RecordCache
|
252
|
+
|
250
253
|
# Patch ActiveRecord::Relation to make sure update_all will invalidate all referenced records
|
251
254
|
module ActiveRecord
|
252
255
|
module UpdateAll
|
@@ -315,4 +318,4 @@ end
|
|
315
318
|
ActiveRecord::Base.send(:include, RecordCache::ActiveRecord::Base)
|
316
319
|
Arel::TreeManager.send(:include, RecordCache::Arel::TreeManager)
|
317
320
|
ActiveRecord::Relation.send(:include, RecordCache::ActiveRecord::UpdateAll)
|
318
|
-
ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
|
321
|
+
ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
|
@@ -0,0 +1,330 @@
|
|
1
|
+
module RecordCache
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
module Base
|
5
|
+
class << self
|
6
|
+
def included(klass)
|
7
|
+
klass.extend ClassMethods
|
8
|
+
klass.class_eval do
|
9
|
+
class << self
|
10
|
+
alias_method_chain :find_by_sql, :record_cache
|
11
|
+
end
|
12
|
+
end
|
13
|
+
include InstanceMethods
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
# add cache invalidation hooks on initialization
|
20
|
+
def record_cache_init
|
21
|
+
after_commit :record_cache_create, :on => :create
|
22
|
+
after_commit :record_cache_update, :on => :update
|
23
|
+
after_commit :record_cache_destroy, :on => :destroy
|
24
|
+
end
|
25
|
+
|
26
|
+
# Retrieve the records, possibly from cache
|
27
|
+
def find_by_sql_with_record_cache(*args)
|
28
|
+
# no caching please
|
29
|
+
return find_by_sql_without_record_cache(*args) unless record_cache?
|
30
|
+
|
31
|
+
# check the piggy-back'd ActiveRelation record to see if the query can be retrieved from cache
|
32
|
+
arel = args[0]
|
33
|
+
if arel.is_a?(String)
|
34
|
+
arel = arel.instance_variable_get(:@arel)
|
35
|
+
puts "TODO: RECORD_CACHE !!!!!!!! Found String with arel piggyback: #{arel}"
|
36
|
+
end
|
37
|
+
|
38
|
+
query = arel ? RecordCache::Arel::QueryVisitor.new(args[1]).accept(arel.ast) : nil
|
39
|
+
cacheable = query && record_cache.cacheable?(query)
|
40
|
+
# log only in debug mode!
|
41
|
+
RecordCache::Base.logger.debug("#{cacheable ? 'Fetch from cache' : 'Not cacheable'} (#{query}): SQL = #{arel.to_sql}") if RecordCache::Base.logger.debug?
|
42
|
+
# retrieve the records from cache if the query is cacheable otherwise go straight to the DB
|
43
|
+
cacheable ? record_cache.fetch(query) : find_by_sql_without_record_cache(*args)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module InstanceMethods
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module Arel
|
53
|
+
|
54
|
+
# The method <ActiveRecord::Base>.find_by_sql is used to actually
|
55
|
+
# retrieve the data from the DB.
|
56
|
+
# Unfortunately the ActiveRelation record is not accessible from
|
57
|
+
# there, so it is piggy-back'd in the SQL string.
|
58
|
+
module TreeManager
|
59
|
+
def self.included(klass)
|
60
|
+
klass.extend ClassMethods
|
61
|
+
klass.send(:include, InstanceMethods)
|
62
|
+
klass.class_eval do
|
63
|
+
alias_method_chain :to_sql, :record_cache
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module ClassMethods
|
68
|
+
end
|
69
|
+
|
70
|
+
module InstanceMethods
|
71
|
+
def to_sql_with_record_cache
|
72
|
+
sql = to_sql_without_record_cache
|
73
|
+
sql.instance_variable_set(:@arel, self)
|
74
|
+
sql
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Visitor for the ActiveRelation to extract a simple cache query
|
80
|
+
# Only accepts single select queries with equality where statements
|
81
|
+
# Rejects queries with grouping / having / offset / etc.
|
82
|
+
class QueryVisitor < ::Arel::Visitors::Visitor
|
83
|
+
def initialize(bindings)
|
84
|
+
super()
|
85
|
+
@bindings = (bindings || []).inject({}){ |h, cv| column, value = cv; h[column.name] = value; h}
|
86
|
+
@cacheable = true
|
87
|
+
@query = ::RecordCache::Query.new
|
88
|
+
end
|
89
|
+
|
90
|
+
def accept object
|
91
|
+
super
|
92
|
+
@cacheable ? @query : nil
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def not_cacheable o
|
98
|
+
@cacheable = false
|
99
|
+
end
|
100
|
+
|
101
|
+
alias :visit_Arel_Nodes_Ordering :not_cacheable
|
102
|
+
|
103
|
+
alias :visit_Arel_Nodes_TableAlias :not_cacheable
|
104
|
+
|
105
|
+
alias :visit_Arel_Nodes_Sum :not_cacheable
|
106
|
+
alias :visit_Arel_Nodes_Max :not_cacheable
|
107
|
+
alias :visit_Arel_Nodes_Avg :not_cacheable
|
108
|
+
alias :visit_Arel_Nodes_Count :not_cacheable
|
109
|
+
|
110
|
+
alias :visit_Arel_Nodes_StringJoin :not_cacheable
|
111
|
+
alias :visit_Arel_Nodes_InnerJoin :not_cacheable
|
112
|
+
alias :visit_Arel_Nodes_OuterJoin :not_cacheable
|
113
|
+
|
114
|
+
alias :visit_Arel_Nodes_DeleteStatement :not_cacheable
|
115
|
+
alias :visit_Arel_Nodes_InsertStatement :not_cacheable
|
116
|
+
alias :visit_Arel_Nodes_UpdateStatement :not_cacheable
|
117
|
+
|
118
|
+
|
119
|
+
alias :unary :not_cacheable
|
120
|
+
alias :visit_Arel_Nodes_Group :unary
|
121
|
+
alias :visit_Arel_Nodes_Having :unary
|
122
|
+
alias :visit_Arel_Nodes_Not :unary
|
123
|
+
alias :visit_Arel_Nodes_On :unary
|
124
|
+
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
|
125
|
+
|
126
|
+
def visit_Arel_Nodes_Offset o
|
127
|
+
@cacheable = false unless o.expr == 0
|
128
|
+
end
|
129
|
+
|
130
|
+
def visit_Arel_Nodes_Values o
|
131
|
+
visit o.expressions if @cacheable
|
132
|
+
end
|
133
|
+
|
134
|
+
def visit_Arel_Nodes_Limit o
|
135
|
+
@query.limit = o.expr
|
136
|
+
end
|
137
|
+
alias :visit_Arel_Nodes_Top :visit_Arel_Nodes_Limit
|
138
|
+
|
139
|
+
def visit_Arel_Nodes_Grouping o
|
140
|
+
return unless @cacheable
|
141
|
+
# "`calendars`.account_id = 5"
|
142
|
+
if @table_name && o.expr =~ /^`#{@table_name}`\.`?(\w*)`?\s*=\s*(\d+)$/
|
143
|
+
@cacheable = @query.where($1, $2.to_i)
|
144
|
+
# "`service_instances`.`id` IN (118,80,120,82)"
|
145
|
+
elsif o.expr =~ /^`#{@table_name}`\.`?(\w*)`?\s*IN\s*\(([\d\s,]+)\)$/
|
146
|
+
@cacheable = @query.where($1, $2.split(',').map(&:to_i))
|
147
|
+
else
|
148
|
+
@cacheable = false
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def visit_Arel_Nodes_SelectCore o
|
153
|
+
@cacheable = false unless o.groups.empty?
|
154
|
+
visit o.froms if @cacheable
|
155
|
+
visit o.wheres if @cacheable
|
156
|
+
# skip o.projections
|
157
|
+
end
|
158
|
+
|
159
|
+
def visit_Arel_Nodes_SelectStatement o
|
160
|
+
@cacheable = false if o.cores.size > 1
|
161
|
+
if @cacheable
|
162
|
+
visit o.offset
|
163
|
+
o.orders.map { |x| handle_order_by(visit x) } if @cacheable && o.orders.size > 0
|
164
|
+
visit o.limit
|
165
|
+
visit o.cores
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def handle_order_by(order)
|
170
|
+
order.to_s.split(",").each do |o|
|
171
|
+
# simple sort order (+peope.id+ can be replaced by +id+, as joins are not allowed anyways)
|
172
|
+
if o.match(/^\s*([\w\.]*)\s*(|ASC|DESC|)\s*$/)
|
173
|
+
asc = $2 == "DESC" ? false : true
|
174
|
+
@query.order_by($1.split('.').last, asc)
|
175
|
+
else
|
176
|
+
@cacheable = false
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def visit_Arel_Table o
|
182
|
+
@table_name = o.name
|
183
|
+
end
|
184
|
+
|
185
|
+
def visit_Arel_Nodes_Ordering o
|
186
|
+
[visit(o.expr), o.descending]
|
187
|
+
end
|
188
|
+
|
189
|
+
def visit_Arel_Attributes_Attribute o
|
190
|
+
o.name.to_sym
|
191
|
+
end
|
192
|
+
alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
|
193
|
+
alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
|
194
|
+
alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
|
195
|
+
alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
|
196
|
+
alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
|
197
|
+
|
198
|
+
def visit_Arel_Nodes_Equality o
|
199
|
+
key, value = visit(o.left), visit(o.right)
|
200
|
+
if value.to_s == "?"
|
201
|
+
# puts "bindings: #{@bindings.inspect}, key = #{key.to_s}"
|
202
|
+
value = @bindings[key.to_s] || "?"
|
203
|
+
end
|
204
|
+
# puts " =====> equality found: #{key.inspect}@#{key.class.name} => #{value.inspect}@#{value.class.name}"
|
205
|
+
@query.where(key, value)
|
206
|
+
end
|
207
|
+
alias :visit_Arel_Nodes_In :visit_Arel_Nodes_Equality
|
208
|
+
|
209
|
+
def visit_Arel_Nodes_And o
|
210
|
+
visit(o.left)
|
211
|
+
visit(o.right)
|
212
|
+
end
|
213
|
+
|
214
|
+
alias :visit_Arel_Nodes_Or :not_cacheable
|
215
|
+
alias :visit_Arel_Nodes_NotEqual :not_cacheable
|
216
|
+
alias :visit_Arel_Nodes_GreaterThan :not_cacheable
|
217
|
+
alias :visit_Arel_Nodes_GreaterThanOrEqual :not_cacheable
|
218
|
+
alias :visit_Arel_Nodes_Assignment :not_cacheable
|
219
|
+
alias :visit_Arel_Nodes_LessThan :not_cacheable
|
220
|
+
alias :visit_Arel_Nodes_LessThanOrEqual :not_cacheable
|
221
|
+
alias :visit_Arel_Nodes_Between :not_cacheable
|
222
|
+
alias :visit_Arel_Nodes_NotIn :not_cacheable
|
223
|
+
alias :visit_Arel_Nodes_DoesNotMatch :not_cacheable
|
224
|
+
alias :visit_Arel_Nodes_Matches :not_cacheable
|
225
|
+
|
226
|
+
def visit_Fixnum o
|
227
|
+
o.to_i
|
228
|
+
end
|
229
|
+
alias :visit_Bignum :visit_Fixnum
|
230
|
+
|
231
|
+
def visit_Symbol o
|
232
|
+
o.to_sym
|
233
|
+
end
|
234
|
+
|
235
|
+
def visit_Object o
|
236
|
+
o
|
237
|
+
end
|
238
|
+
alias :visit_Arel_Nodes_SqlLiteral :visit_Object
|
239
|
+
alias :visit_Arel_SqlLiteral :visit_Object # This is deprecated
|
240
|
+
alias :visit_String :visit_Object
|
241
|
+
alias :visit_NilClass :visit_Object
|
242
|
+
alias :visit_TrueClass :visit_Object
|
243
|
+
alias :visit_FalseClass :visit_Object
|
244
|
+
alias :visit_Arel_SqlLiteral :visit_Object
|
245
|
+
alias :visit_BigDecimal :visit_Object
|
246
|
+
alias :visit_Float :visit_Object
|
247
|
+
alias :visit_Time :visit_Object
|
248
|
+
alias :visit_Date :visit_Object
|
249
|
+
alias :visit_DateTime :visit_Object
|
250
|
+
alias :visit_Hash :visit_Object
|
251
|
+
|
252
|
+
def visit_Array o
|
253
|
+
o.map{ |x| visit x }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
module RecordCache
|
261
|
+
|
262
|
+
# Patch ActiveRecord::Relation to make sure update_all will invalidate all referenced records
|
263
|
+
module ActiveRecord
|
264
|
+
module UpdateAll
|
265
|
+
class << self
|
266
|
+
def included(klass)
|
267
|
+
klass.extend ClassMethods
|
268
|
+
klass.send(:include, InstanceMethods)
|
269
|
+
klass.class_eval do
|
270
|
+
alias_method_chain :update_all, :record_cache
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
module ClassMethods
|
276
|
+
end
|
277
|
+
|
278
|
+
module InstanceMethods
|
279
|
+
def update_all_with_record_cache(updates, conditions = nil, options = {})
|
280
|
+
result = update_all_without_record_cache(updates, conditions, options)
|
281
|
+
|
282
|
+
if record_cache?
|
283
|
+
# when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
|
284
|
+
unless conditions || options.present? || @limit_value.present? != @order_values.present?
|
285
|
+
# go straight to SQL result (without instantiating records) for optimal performance
|
286
|
+
connection.execute(select('id').to_sql).each{ |row| record_cache.invalidate(:id, (row.is_a?(Hash) ? row['id'] : row.first).to_i ) }
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
result
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Patch ActiveRecord::Associations::HasManyAssociation to make sure the index_cache is updated when records are
|
297
|
+
# deleted from the collection
|
298
|
+
module ActiveRecord
|
299
|
+
module HasMany
|
300
|
+
class << self
|
301
|
+
def included(klass)
|
302
|
+
klass.extend ClassMethods
|
303
|
+
klass.send(:include, InstanceMethods)
|
304
|
+
klass.class_eval do
|
305
|
+
alias_method_chain :delete_records, :record_cache
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
module ClassMethods
|
311
|
+
end
|
312
|
+
|
313
|
+
module InstanceMethods
|
314
|
+
def delete_records_with_record_cache(records, method)
|
315
|
+
# invalidate :id cache for all records
|
316
|
+
records.each{ |record| record.class.record_cache.invalidate(record.id) if record.class.record_cache? unless record.new_record? }
|
317
|
+
# invalidate the referenced class for the attribute/value pair on the index cache
|
318
|
+
@reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
|
319
|
+
delete_records_without_record_cache(records, method)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
end
|
326
|
+
|
327
|
+
ActiveRecord::Base.send(:include, RecordCache::ActiveRecord::Base)
|
328
|
+
Arel::TreeManager.send(:include, RecordCache::Arel::TreeManager)
|
329
|
+
ActiveRecord::Relation.send(:include, RecordCache::ActiveRecord::UpdateAll)
|
330
|
+
ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
|
data/lib/record_cache/version.rb
CHANGED
data/lib/record_cache.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
|
-
# Record Cache
|
1
|
+
# Record Cache files
|
2
2
|
["query", "version_store", "multi_read",
|
3
3
|
"strategy/base", "strategy/id_cache", "strategy/index_cache", "strategy/request_cache",
|
4
4
|
"statistics", "dispatcher", "base"].each do |file|
|
5
5
|
require File.dirname(__FILE__) + "/record_cache/#{file}.rb"
|
6
6
|
end
|
7
7
|
|
8
|
-
#
|
9
|
-
require
|
10
|
-
ActiveRecord::Base.send(:include, RecordCache::Base)
|
11
|
-
require File.dirname(__FILE__) + "/record_cache/active_record.rb"
|
8
|
+
# Load Data Stores (currently only support for Active Record)
|
9
|
+
require File.dirname(__FILE__) + "/record_cache/datastore/active_record.rb"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: record-cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Orslumen
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-10-07 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: rails
|
@@ -23,14 +23,13 @@ dependencies:
|
|
23
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
24
|
none: false
|
25
25
|
requirements:
|
26
|
-
- - "
|
26
|
+
- - ">"
|
27
27
|
- !ruby/object:Gem::Version
|
28
|
-
hash:
|
28
|
+
hash: 7
|
29
29
|
segments:
|
30
30
|
- 3
|
31
31
|
- 0
|
32
|
-
|
33
|
-
version: 3.0.10
|
32
|
+
version: "3.0"
|
34
33
|
type: :runtime
|
35
34
|
version_requirements: *id001
|
36
35
|
- !ruby/object:Gem::Dependency
|
@@ -39,14 +38,13 @@ dependencies:
|
|
39
38
|
requirement: &id002 !ruby/object:Gem::Requirement
|
40
39
|
none: false
|
41
40
|
requirements:
|
42
|
-
- - "
|
41
|
+
- - ">"
|
43
42
|
- !ruby/object:Gem::Version
|
44
|
-
hash:
|
43
|
+
hash: 7
|
45
44
|
segments:
|
46
45
|
- 3
|
47
46
|
- 0
|
48
|
-
|
49
|
-
version: 3.0.10
|
47
|
+
version: "3.0"
|
50
48
|
type: :development
|
51
49
|
version_requirements: *id002
|
52
50
|
- !ruby/object:Gem::Dependency
|
@@ -158,8 +156,10 @@ extra_rdoc_files: []
|
|
158
156
|
files:
|
159
157
|
- lib/record-cache.rb
|
160
158
|
- lib/record_cache.rb
|
161
|
-
- lib/record_cache/active_record.rb
|
162
159
|
- lib/record_cache/base.rb
|
160
|
+
- lib/record_cache/datastore/active_record.rb
|
161
|
+
- lib/record_cache/datastore/active_record_30.rb
|
162
|
+
- lib/record_cache/datastore/active_record_31.rb
|
163
163
|
- lib/record_cache/dispatcher.rb
|
164
164
|
- lib/record_cache/multi_read.rb
|
165
165
|
- lib/record_cache/query.rb
|
@@ -223,10 +223,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
223
223
|
requirements: []
|
224
224
|
|
225
225
|
rubyforge_project:
|
226
|
-
rubygems_version: 1.8.
|
226
|
+
rubygems_version: 1.8.11
|
227
227
|
signing_key:
|
228
228
|
specification_version: 3
|
229
|
-
summary: Record Cache v0.1.
|
229
|
+
summary: Record Cache v0.1.1 transparantly stores Records in a Cache Store and retrieve those Records from the store when queried (by ID) using Active Model.
|
230
230
|
test_files:
|
231
231
|
- spec/db/database.yml
|
232
232
|
- spec/db/schema.rb
|