record-cache 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- equality = [visit(o.left), visit(o.right)]
195
- # equality.reverse! if equality.last.is_a?(Symbol) || equality.first.is_a?(Fixnum)
196
- # p " =====> equality found: #{equality.first.inspect}@#{equality.first.class.name} => #{equality.last.inspect}@#{equality.last.class.name}"
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)
@@ -1,5 +1,5 @@
1
1
  module RecordCache # :nodoc:
2
2
  module Version # :nodoc:
3
- STRING = '0.1.0'
3
+ STRING = '0.1.1'
4
4
  end
5
5
  end
data/lib/record_cache.rb CHANGED
@@ -1,11 +1,9 @@
1
- # Record Cache shared files
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
- # Support for Active Record
9
- require 'active_record'
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: 27
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 0
10
- version: 0.1.0
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-09-27 00:00:00 Z
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: 19
28
+ hash: 7
29
29
  segments:
30
30
  - 3
31
31
  - 0
32
- - 10
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: 19
43
+ hash: 7
45
44
  segments:
46
45
  - 3
47
46
  - 0
48
- - 10
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.10
226
+ rubygems_version: 1.8.11
227
227
  signing_key:
228
228
  specification_version: 3
229
- summary: Record Cache v0.1.0 transparantly stores Records in a Cache Store and retrieve those Records from the store when queried (by ID) using Active Model.
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