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