record-cache 0.1.3 → 0.1.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.
- checksums.yaml +8 -8
- data/lib/record_cache/base.rb +4 -4
- data/lib/record_cache/datastore/active_record_30.rb +1 -1
- data/lib/record_cache/datastore/active_record_31.rb +1 -1
- data/lib/record_cache/datastore/active_record_32.rb +2 -2
- data/lib/record_cache/datastore/active_record_40.rb +445 -0
- data/lib/record_cache/datastore/active_record_41.rb +446 -0
- data/lib/record_cache/strategy/full_table_cache.rb +1 -1
- data/lib/record_cache/strategy/util.rb +20 -3
- data/lib/record_cache/version.rb +1 -1
- data/spec/db/create-record-cache-db_and_user.sql +5 -0
- data/spec/db/database.yml +7 -0
- data/spec/db/schema.rb +9 -15
- data/spec/initializers/backward_compatibility.rb +32 -0
- data/spec/lib/active_record/visitor_spec.rb +1 -1
- data/spec/lib/base_spec.rb +2 -2
- data/spec/lib/dispatcher_spec.rb +1 -1
- data/spec/lib/multi_read_spec.rb +1 -1
- data/spec/lib/query_spec.rb +1 -1
- data/spec/lib/statistics_spec.rb +1 -1
- data/spec/lib/strategy/base_spec.rb +39 -39
- data/spec/lib/strategy/full_table_cache_spec.rb +18 -18
- data/spec/lib/strategy/index_cache_spec.rb +58 -52
- data/spec/lib/strategy/query_cache_spec.rb +1 -1
- data/spec/lib/strategy/unique_index_on_id_cache_spec.rb +57 -45
- data/spec/lib/strategy/unique_index_on_string_cache_spec.rb +47 -45
- data/spec/lib/strategy/util_spec.rb +49 -43
- data/spec/lib/version_store_spec.rb +1 -1
- data/spec/models/apple.rb +1 -2
- data/spec/spec_helper.rb +16 -7
- data/spec/support/matchers/hit_cache_matcher.rb +1 -1
- data/spec/support/matchers/miss_cache_matcher.rb +1 -1
- data/spec/support/matchers/use_cache_matcher.rb +1 -1
- metadata +63 -17
- data/spec/support/after_commit.rb +0 -73
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
YzM5MTA3YWI2MTRjMDIwMGFmN2FhYzQxZmNlMWU2ZTNiNTkzODkyMQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MGYzNGU3NzVlZTI4YTMwZTM5NDhkZDVjZGVjY2UyODJlMWU1YzIzMQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
MzQxNDM1ZWQxMmUzYmQyNzZhZjZmNDA2MmQzZWViMWYwZDhlMzI0YTczYjY4
|
10
|
+
ZmE4ZDZkNjBkMjc3YjUxNGM1YjJhMDlkNzkwZDRlNGM0MTBlMTUzMTBmZThm
|
11
|
+
MDg1NWNjMzZhOGE3OWIxMjc0ODg5YzZmYjY1MmY5MWRkNWFhMDc=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
ZDA2OTRjOTZkODFhMTdiNGU3M2IzZDZiYTljNTViZTdjZGQ2ZjRjODNmY2Ew
|
14
|
+
MzA4OTgwYmMyNTE2YzRlZDI4Yjg1MjhiMGFlNTdmYzFmNWZlYWZiYmJlZDc0
|
15
|
+
YzMzODM2Y2UyNDIzYjQyNzAwMWY0NWIxZWI0ZjBkNDJlNDVkMTQ=
|
data/lib/record_cache/base.rb
CHANGED
@@ -9,7 +9,7 @@ module RecordCache
|
|
9
9
|
module Base
|
10
10
|
class << self
|
11
11
|
def included(klass)
|
12
|
-
klass.class_eval do
|
12
|
+
klass.class_eval do
|
13
13
|
extend ClassMethods
|
14
14
|
include InstanceMethods
|
15
15
|
end
|
@@ -113,7 +113,7 @@ module RecordCache
|
|
113
113
|
# :store => the cache store for the instances, e.g. :memory_store, :dalli_store* (default: Rails.cache)
|
114
114
|
# or one of the store ids defined using +RecordCache::Base.register_store+
|
115
115
|
# :key => provide a unique shorter key to limit the cache key length (default: model.name)
|
116
|
-
#
|
116
|
+
#
|
117
117
|
# cache strategy specific options:
|
118
118
|
# :index => one or more attributes (Symbols) for which the ids are cached for the value of the attribute
|
119
119
|
#
|
@@ -124,7 +124,7 @@ module RecordCache
|
|
124
124
|
# - use :index => :person_id for aggregated has_many associations
|
125
125
|
def cache_records(options = {})
|
126
126
|
unless @rc_dispatcher
|
127
|
-
@rc_dispatcher = RecordCache::Dispatcher.new(self)
|
127
|
+
@rc_dispatcher = RecordCache::Dispatcher.new(self)
|
128
128
|
# Callback for Data Store specific initialization
|
129
129
|
record_cache_init
|
130
130
|
|
@@ -174,4 +174,4 @@ module RecordCache
|
|
174
174
|
end
|
175
175
|
|
176
176
|
end
|
177
|
-
end
|
177
|
+
end
|
@@ -173,7 +173,7 @@ module RecordCache
|
|
173
173
|
@cacheable = false unless o.groups.empty?
|
174
174
|
visit o.froms if @cacheable
|
175
175
|
visit o.wheres if @cacheable
|
176
|
-
|
176
|
+
@cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
|
177
177
|
end
|
178
178
|
|
179
179
|
def visit_Arel_Nodes_SelectStatement o
|
@@ -199,7 +199,7 @@ module RecordCache
|
|
199
199
|
visit o.froms if @cacheable
|
200
200
|
visit o.wheres if @cacheable
|
201
201
|
visit o.source if @cacheable
|
202
|
-
|
202
|
+
@cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
|
203
203
|
end
|
204
204
|
|
205
205
|
def visit_Arel_Nodes_SelectStatement o
|
@@ -33,7 +33,7 @@ module RecordCache
|
|
33
33
|
arel = sql.is_a?(String) ? sql.instance_variable_get(:@arel) : sql
|
34
34
|
|
35
35
|
sanitized_sql = sanitize_sql(sql)
|
36
|
-
sanitized_sql = connection.to_sql(sanitized_sql, binds) if sanitized_sql.respond_to?(:ast)
|
36
|
+
sanitized_sql = connection.to_sql(sanitized_sql, binds.dup) if sanitized_sql.respond_to?(:ast)
|
37
37
|
|
38
38
|
records = if connection.query_cache_enabled
|
39
39
|
query_cache = connection.instance_variable_get(:@query_cache)
|
@@ -210,7 +210,7 @@ module RecordCache
|
|
210
210
|
visit o.froms if @cacheable
|
211
211
|
visit o.wheres if @cacheable
|
212
212
|
visit o.source if @cacheable
|
213
|
-
|
213
|
+
@cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
|
214
214
|
end
|
215
215
|
|
216
216
|
def visit_Arel_Nodes_SelectStatement o
|
@@ -0,0 +1,445 @@
|
|
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
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# the tests are always run within a transaction, so the threshold is one higher
|
18
|
+
RC_TRANSACTIONS_THRESHOLD = ENV['RAILS_ENV'] == 'test' ? 1 : 0
|
19
|
+
|
20
|
+
# add cache invalidation hooks on initialization
|
21
|
+
def record_cache_init
|
22
|
+
after_commit :record_cache_create, :on => :create, :prepend => true
|
23
|
+
after_commit :record_cache_update, :on => :update, :prepend => true
|
24
|
+
after_commit :record_cache_destroy, :on => :destroy, :prepend => true
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieve the records, possibly from cache
|
28
|
+
def find_by_sql_with_record_cache(sql, binds = [])
|
29
|
+
# Shortcut, no caching please
|
30
|
+
return find_by_sql_without_record_cache(sql, binds) unless record_cache?
|
31
|
+
|
32
|
+
# check the piggy-back'd ActiveRelation record to see if the query can be retrieved from cache
|
33
|
+
arel = sql.is_a?(String) ? sql.instance_variable_get(:@arel) : sql
|
34
|
+
|
35
|
+
sanitized_sql = sanitize_sql(sql)
|
36
|
+
sanitized_sql = connection.to_sql(sanitized_sql, binds) if sanitized_sql.respond_to?(:ast)
|
37
|
+
|
38
|
+
records = if connection.query_cache_enabled
|
39
|
+
query_cache = connection.instance_variable_get(:@query_cache)
|
40
|
+
query_cache["rc/#{sanitized_sql}"][binds] ||= try_record_cache(arel, sql, binds)
|
41
|
+
|
42
|
+
elsif connection.open_transactions > RC_TRANSACTIONS_THRESHOLD
|
43
|
+
find_by_sql_without_record_cache(sql, binds)
|
44
|
+
|
45
|
+
else
|
46
|
+
try_record_cache(arel, sql, binds)
|
47
|
+
end
|
48
|
+
records
|
49
|
+
end
|
50
|
+
|
51
|
+
def try_record_cache(arel, sql, binds)
|
52
|
+
query = arel && arel.respond_to?(:ast) ? RecordCache::Arel::QueryVisitor.new(binds).accept(arel.ast) : nil
|
53
|
+
record_cache.fetch(query) do
|
54
|
+
find_by_sql_without_record_cache(sql, binds)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module Arel
|
64
|
+
|
65
|
+
# The method <ActiveRecord::Base>.find_by_sql is used to actually
|
66
|
+
# retrieve the data from the DB.
|
67
|
+
# Unfortunately the ActiveRelation record is not accessible from
|
68
|
+
# there, so it is piggy-back'd in the SQL string.
|
69
|
+
module TreeManager
|
70
|
+
def self.included(klass)
|
71
|
+
klass.extend ClassMethods
|
72
|
+
klass.send(:include, InstanceMethods)
|
73
|
+
klass.class_eval do
|
74
|
+
alias_method_chain :to_sql, :record_cache
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module ClassMethods
|
79
|
+
end
|
80
|
+
|
81
|
+
module InstanceMethods
|
82
|
+
def to_sql_with_record_cache
|
83
|
+
sql = to_sql_without_record_cache
|
84
|
+
sql.instance_variable_set(:@arel, self)
|
85
|
+
sql
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Visitor for the ActiveRelation to extract a simple cache query
|
91
|
+
# Only accepts single select queries with equality where statements
|
92
|
+
# Rejects queries with grouping / having / offset / etc.
|
93
|
+
class QueryVisitor < ::Arel::Visitors::Visitor
|
94
|
+
DESC = "DESC".freeze
|
95
|
+
COMMA = ",".freeze
|
96
|
+
|
97
|
+
def initialize(bindings)
|
98
|
+
super()
|
99
|
+
@bindings = (bindings || []).inject({}){ |h, cv| column, value = cv; h[column.name] = value; h}
|
100
|
+
@cacheable = true
|
101
|
+
@query = ::RecordCache::Query.new
|
102
|
+
end
|
103
|
+
|
104
|
+
def accept ast
|
105
|
+
super
|
106
|
+
@cacheable && !ast.lock ? @query : nil
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def not_cacheable o, attribute
|
112
|
+
@cacheable = false
|
113
|
+
end
|
114
|
+
|
115
|
+
def skip o, attribute
|
116
|
+
end
|
117
|
+
|
118
|
+
alias :visit_Arel_Nodes_TableAlias :not_cacheable
|
119
|
+
|
120
|
+
alias :visit_Arel_Nodes_Lock :not_cacheable
|
121
|
+
|
122
|
+
alias :visit_Arel_Nodes_Sum :not_cacheable
|
123
|
+
alias :visit_Arel_Nodes_Max :not_cacheable
|
124
|
+
alias :visit_Arel_Nodes_Min :not_cacheable
|
125
|
+
alias :visit_Arel_Nodes_Avg :not_cacheable
|
126
|
+
alias :visit_Arel_Nodes_Count :not_cacheable
|
127
|
+
alias :visit_Arel_Nodes_Addition :not_cacheable
|
128
|
+
alias :visit_Arel_Nodes_Subtraction :not_cacheable
|
129
|
+
alias :visit_Arel_Nodes_Multiplication :not_cacheable
|
130
|
+
alias :visit_Arel_Nodes_NamedFunction :not_cacheable
|
131
|
+
|
132
|
+
alias :visit_Arel_Nodes_Bin :not_cacheable
|
133
|
+
alias :visit_Arel_Nodes_Distinct :not_cacheable
|
134
|
+
alias :visit_Arel_Nodes_DistinctOn :not_cacheable
|
135
|
+
alias :visit_Arel_Nodes_Division :not_cacheable
|
136
|
+
alias :visit_Arel_Nodes_Except :not_cacheable
|
137
|
+
alias :visit_Arel_Nodes_Exists :not_cacheable
|
138
|
+
alias :visit_Arel_Nodes_InfixOperation :not_cacheable
|
139
|
+
alias :visit_Arel_Nodes_Intersect :not_cacheable
|
140
|
+
alias :visit_Arel_Nodes_Union :not_cacheable
|
141
|
+
alias :visit_Arel_Nodes_UnionAll :not_cacheable
|
142
|
+
alias :visit_Arel_Nodes_With :not_cacheable
|
143
|
+
alias :visit_Arel_Nodes_WithRecursive :not_cacheable
|
144
|
+
|
145
|
+
def visit_Arel_Nodes_JoinSource o, attribute
|
146
|
+
# left and right are array, but using blank as it also works for nil
|
147
|
+
@cacheable = o.left.blank? || o.right.blank?
|
148
|
+
end
|
149
|
+
|
150
|
+
alias :visit_Arel_Nodes_CurrentRow :not_cacheable
|
151
|
+
alias :visit_Arel_Nodes_Extract :not_cacheable
|
152
|
+
alias :visit_Arel_Nodes_Following :not_cacheable
|
153
|
+
alias :visit_Arel_Nodes_NamedWindow :not_cacheable
|
154
|
+
alias :visit_Arel_Nodes_Over :not_cacheable
|
155
|
+
alias :visit_Arel_Nodes_Preceding :not_cacheable
|
156
|
+
alias :visit_Arel_Nodes_Range :not_cacheable
|
157
|
+
alias :visit_Arel_Nodes_Rows :not_cacheable
|
158
|
+
alias :visit_Arel_Nodes_Window :not_cacheable
|
159
|
+
|
160
|
+
alias :visit_Arel_Nodes_As :skip
|
161
|
+
alias :visit_Arel_SelectManager :not_cacheable
|
162
|
+
alias :visit_Arel_Nodes_Ascending :skip
|
163
|
+
alias :visit_Arel_Nodes_Descending :skip
|
164
|
+
alias :visit_Arel_Nodes_False :skip
|
165
|
+
alias :visit_Arel_Nodes_True :skip
|
166
|
+
|
167
|
+
alias :visit_Arel_Nodes_StringJoin :not_cacheable
|
168
|
+
alias :visit_Arel_Nodes_InnerJoin :not_cacheable
|
169
|
+
alias :visit_Arel_Nodes_OuterJoin :not_cacheable
|
170
|
+
|
171
|
+
alias :visit_Arel_Nodes_DeleteStatement :not_cacheable
|
172
|
+
alias :visit_Arel_Nodes_InsertStatement :not_cacheable
|
173
|
+
alias :visit_Arel_Nodes_UpdateStatement :not_cacheable
|
174
|
+
|
175
|
+
|
176
|
+
alias :unary :not_cacheable
|
177
|
+
alias :visit_Arel_Nodes_Group :unary
|
178
|
+
alias :visit_Arel_Nodes_Having :unary
|
179
|
+
alias :visit_Arel_Nodes_Not :unary
|
180
|
+
alias :visit_Arel_Nodes_On :unary
|
181
|
+
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
|
182
|
+
|
183
|
+
def visit_Arel_Nodes_Offset o, attribute
|
184
|
+
@cacheable = false unless o.expr == 0
|
185
|
+
end
|
186
|
+
|
187
|
+
def visit_Arel_Nodes_Values o, attribute
|
188
|
+
visit o.expressions if @cacheable
|
189
|
+
end
|
190
|
+
|
191
|
+
def visit_Arel_Nodes_Limit o, attribute
|
192
|
+
@query.limit = o.expr
|
193
|
+
end
|
194
|
+
alias :visit_Arel_Nodes_Top :visit_Arel_Nodes_Limit
|
195
|
+
|
196
|
+
GROUPING_EQUALS_REGEXP = /^\W?(\w*)\W?\.\W?(\w*)\W?\s*=\s*(\d+)$/ # `calendars`.account_id = 5
|
197
|
+
GROUPING_IN_REGEXP = /^^\W?(\w*)\W?\.\W?(\w*)\W?\s*IN\s*\(([\d\s,]+)\)$/ # `service_instances`.`id` IN (118,80,120,82)
|
198
|
+
def visit_Arel_Nodes_Grouping o, attribute
|
199
|
+
return unless @cacheable
|
200
|
+
if @table_name && o.expr =~ GROUPING_EQUALS_REGEXP && $1 == @table_name
|
201
|
+
@cacheable = @query.where($2, $3.to_i)
|
202
|
+
elsif @table_name && o.expr =~ GROUPING_IN_REGEXP && $1 == @table_name
|
203
|
+
@cacheable = @query.where($2, $3.split(',').map(&:to_i))
|
204
|
+
else
|
205
|
+
@cacheable = false
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def visit_Arel_Nodes_SelectCore o, attribute
|
210
|
+
@cacheable = false unless o.groups.empty?
|
211
|
+
visit o.froms if @cacheable
|
212
|
+
visit o.wheres if @cacheable
|
213
|
+
visit o.source if @cacheable
|
214
|
+
@cacheable = o.projections.none?{ |projection| projection.to_s =~ /distinct/i } unless o.projections.empty? if @cacheable
|
215
|
+
end
|
216
|
+
|
217
|
+
def visit_Arel_Nodes_SelectStatement o, attribute
|
218
|
+
@cacheable = false if o.cores.size > 1
|
219
|
+
if @cacheable
|
220
|
+
visit o.offset
|
221
|
+
o.orders.map { |x| handle_order_by(visit x) } if @cacheable && o.orders.size > 0
|
222
|
+
visit o.limit
|
223
|
+
visit o.cores
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
ORDER_BY_REGEXP = /^\s*([\w\.]*)\s*(|ASC|asc|DESC|desc)\s*$/ # people.id DESC
|
228
|
+
def handle_order_by(order)
|
229
|
+
order.to_s.split(COMMA).each do |o|
|
230
|
+
# simple sort order (+people.id+ can be replaced by +id+, as joins are not allowed anyways)
|
231
|
+
if o.match(ORDER_BY_REGEXP)
|
232
|
+
asc = $2.upcase == DESC ? false : true
|
233
|
+
@query.order_by($1.split('.').last, asc)
|
234
|
+
else
|
235
|
+
@cacheable = false
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def visit_Arel_Table o, attribute
|
241
|
+
@table_name = o.name
|
242
|
+
end
|
243
|
+
|
244
|
+
def visit_Arel_Attributes_Attribute o, attribute
|
245
|
+
o.name.to_sym
|
246
|
+
end
|
247
|
+
alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
|
248
|
+
alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
|
249
|
+
alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
|
250
|
+
alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
|
251
|
+
alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
|
252
|
+
alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
|
253
|
+
|
254
|
+
def visit_Arel_Nodes_Equality o, attribute
|
255
|
+
key, value = visit(o.left), visit(o.right)
|
256
|
+
# several different binding markers exist depending on the db driver used (MySQL, Postgress supported)
|
257
|
+
if value.to_s =~ /^(\?|\u0000|\$\d+)$/
|
258
|
+
# puts "bindings: #{@bindings.inspect}, key = #{key.to_s}"
|
259
|
+
value = @bindings[key.to_s] || value
|
260
|
+
end
|
261
|
+
# puts " =====> equality found: #{key.inspect}@#{key.class.name} => #{value.inspect}@#{value.class.name}"
|
262
|
+
@query.where(key, value)
|
263
|
+
end
|
264
|
+
alias :visit_Arel_Nodes_In :visit_Arel_Nodes_Equality
|
265
|
+
|
266
|
+
def visit_Arel_Nodes_And o, attribute
|
267
|
+
visit(o.children)
|
268
|
+
end
|
269
|
+
|
270
|
+
alias :visit_Arel_Nodes_Or :not_cacheable
|
271
|
+
alias :visit_Arel_Nodes_NotEqual :not_cacheable
|
272
|
+
alias :visit_Arel_Nodes_GreaterThan :not_cacheable
|
273
|
+
alias :visit_Arel_Nodes_GreaterThanOrEqual :not_cacheable
|
274
|
+
alias :visit_Arel_Nodes_Assignment :not_cacheable
|
275
|
+
alias :visit_Arel_Nodes_LessThan :not_cacheable
|
276
|
+
alias :visit_Arel_Nodes_LessThanOrEqual :not_cacheable
|
277
|
+
alias :visit_Arel_Nodes_Between :not_cacheable
|
278
|
+
alias :visit_Arel_Nodes_NotIn :not_cacheable
|
279
|
+
alias :visit_Arel_Nodes_DoesNotMatch :not_cacheable
|
280
|
+
alias :visit_Arel_Nodes_Matches :not_cacheable
|
281
|
+
|
282
|
+
def visit_Fixnum o, attribute
|
283
|
+
o.to_i
|
284
|
+
end
|
285
|
+
alias :visit_Bignum :visit_Fixnum
|
286
|
+
|
287
|
+
def visit_Symbol o, attribute
|
288
|
+
o.to_sym
|
289
|
+
end
|
290
|
+
|
291
|
+
def visit_Object o, attribute
|
292
|
+
o
|
293
|
+
end
|
294
|
+
alias :visit_Arel_Nodes_SqlLiteral :visit_Object
|
295
|
+
alias :visit_Arel_Nodes_BindParam :visit_Object
|
296
|
+
alias :visit_Arel_SqlLiteral :visit_Object # This is deprecated
|
297
|
+
alias :visit_String :visit_Object
|
298
|
+
alias :visit_NilClass :visit_Object
|
299
|
+
alias :visit_TrueClass :visit_Object
|
300
|
+
alias :visit_FalseClass :visit_Object
|
301
|
+
alias :visit_Arel_SqlLiteral :visit_Object
|
302
|
+
alias :visit_BigDecimal :visit_Object
|
303
|
+
alias :visit_Float :visit_Object
|
304
|
+
alias :visit_Time :visit_Object
|
305
|
+
alias :visit_Date :visit_Object
|
306
|
+
alias :visit_DateTime :visit_Object
|
307
|
+
alias :visit_Hash :visit_Object
|
308
|
+
|
309
|
+
def visit_Array o, attribute
|
310
|
+
o.map{ |x| visit x }
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
|
317
|
+
module RecordCache
|
318
|
+
|
319
|
+
# Patch ActiveRecord::Relation to make sure update_all will invalidate all referenced records
|
320
|
+
module ActiveRecord
|
321
|
+
module UpdateAll
|
322
|
+
class << self
|
323
|
+
def included(klass)
|
324
|
+
klass.extend ClassMethods
|
325
|
+
klass.send(:include, InstanceMethods)
|
326
|
+
klass.class_eval do
|
327
|
+
alias_method_chain :update_all, :record_cache
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
module ClassMethods
|
333
|
+
end
|
334
|
+
|
335
|
+
module InstanceMethods
|
336
|
+
def __find_in_clause(sub_select)
|
337
|
+
return nil unless sub_select.arel.constraints.count == 1
|
338
|
+
constraint = sub_select.arel.constraints.first
|
339
|
+
return constraint if constraint.is_a?(::Arel::Nodes::In) # directly an IN clause
|
340
|
+
return nil unless constraint.respond_to?(:children) && constraint.children.count == 1
|
341
|
+
constraint = constraint.children.first
|
342
|
+
return constraint if constraint.is_a?(::Arel::Nodes::In) # AND with IN clause
|
343
|
+
nil
|
344
|
+
end
|
345
|
+
|
346
|
+
def update_all_with_record_cache(updates, conditions = nil, options = {})
|
347
|
+
result = update_all_without_record_cache(updates, conditions, options)
|
348
|
+
|
349
|
+
if record_cache?
|
350
|
+
# when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
|
351
|
+
unless conditions || options.present? || @limit_value.present? != @order_values.present?
|
352
|
+
# get all attributes that contain a unique index for this model
|
353
|
+
unique_index_attributes = RecordCache::Strategy::UniqueIndexCache.attributes(self)
|
354
|
+
# go straight to SQL result (without instantiating records) for optimal performance
|
355
|
+
RecordCache::Base.version_store.multi do
|
356
|
+
sub_select = select(unique_index_attributes.map(&:to_s).join(','))
|
357
|
+
in_clause = __find_in_clause(sub_select)
|
358
|
+
if unique_index_attributes.size == 1 && in_clause &&
|
359
|
+
in_clause.left.try(:name).to_s == unique_index_attributes.first.to_s
|
360
|
+
# common case where the unique index is the (only) constraint on the query: SELECT id FROM people WHERE id in (...)
|
361
|
+
attribute = unique_index_attributes.first
|
362
|
+
in_clause.right.each do |value|
|
363
|
+
record_cache.invalidate(attribute, value)
|
364
|
+
end
|
365
|
+
else
|
366
|
+
connection.execute(sub_select.to_sql).each do |row|
|
367
|
+
# invalidate the unique index for all attributes
|
368
|
+
unique_index_attributes.each_with_index do |attribute, index|
|
369
|
+
record_cache.invalidate(attribute, (row.is_a?(Hash) ? row[attribute.to_s] : row[index]))
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
result
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# Patch ActiveRecord::Associations::HasManyAssociation to make sure the index_cache is updated when records are
|
384
|
+
# deleted from the collection
|
385
|
+
module ActiveRecord
|
386
|
+
module HasMany
|
387
|
+
class << self
|
388
|
+
def included(klass)
|
389
|
+
klass.extend ClassMethods
|
390
|
+
klass.send(:include, InstanceMethods)
|
391
|
+
klass.class_eval do
|
392
|
+
alias_method_chain :delete_records, :record_cache
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
module ClassMethods
|
398
|
+
end
|
399
|
+
|
400
|
+
module InstanceMethods
|
401
|
+
def delete_records_with_record_cache(records, method)
|
402
|
+
# invalidate :id cache for all records
|
403
|
+
records.each{ |record| record.class.record_cache.invalidate(record.id) if record.class.record_cache? unless record.new_record? }
|
404
|
+
# invalidate the referenced class for the attribute/value pair on the index cache
|
405
|
+
@reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
|
406
|
+
delete_records_without_record_cache(records, method)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
module HasOne
|
412
|
+
class << self
|
413
|
+
def included(klass)
|
414
|
+
klass.extend ClassMethods
|
415
|
+
klass.send(:include, InstanceMethods)
|
416
|
+
klass.class_eval do
|
417
|
+
alias_method_chain :delete, :record_cache
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
module ClassMethods
|
423
|
+
end
|
424
|
+
|
425
|
+
module InstanceMethods
|
426
|
+
def delete_with_record_cache(method = options[:dependent])
|
427
|
+
# invalidate :id cache for all record
|
428
|
+
if load_target
|
429
|
+
target.class.record_cache.invalidate(target.id) if target.class.record_cache? unless target.new_record?
|
430
|
+
end
|
431
|
+
# invalidate the referenced class for the attribute/value pair on the index cache
|
432
|
+
@reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
|
433
|
+
delete_without_record_cache(method)
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
end
|
440
|
+
|
441
|
+
ActiveRecord::Base.send(:include, RecordCache::ActiveRecord::Base)
|
442
|
+
Arel::TreeManager.send(:include, RecordCache::Arel::TreeManager)
|
443
|
+
ActiveRecord::Relation.send(:include, RecordCache::ActiveRecord::UpdateAll)
|
444
|
+
ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
|
445
|
+
ActiveRecord::Associations::HasOneAssociation.send(:include, RecordCache::ActiveRecord::HasOne)
|