record-cache 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/lib/record_cache.rb +2 -1
- data/lib/record_cache/base.rb +63 -22
- data/lib/record_cache/datastore/active_record.rb +5 -3
- data/lib/record_cache/datastore/active_record_30.rb +95 -38
- data/lib/record_cache/datastore/active_record_31.rb +157 -54
- data/lib/record_cache/datastore/active_record_32.rb +444 -0
- data/lib/record_cache/dispatcher.rb +47 -47
- data/lib/record_cache/multi_read.rb +14 -1
- data/lib/record_cache/query.rb +36 -25
- data/lib/record_cache/statistics.rb +5 -5
- data/lib/record_cache/strategy/base.rb +49 -19
- data/lib/record_cache/strategy/full_table_cache.rb +81 -0
- data/lib/record_cache/strategy/index_cache.rb +38 -36
- data/lib/record_cache/strategy/unique_index_cache.rb +130 -0
- data/lib/record_cache/strategy/util.rb +12 -12
- data/lib/record_cache/test/resettable_version_store.rb +2 -9
- data/lib/record_cache/version.rb +1 -1
- data/lib/record_cache/version_store.rb +23 -16
- data/spec/db/schema.rb +12 -0
- data/spec/db/seeds.rb +10 -0
- data/spec/lib/active_record/visitor_spec.rb +22 -0
- data/spec/lib/base_spec.rb +21 -0
- data/spec/lib/dispatcher_spec.rb +24 -46
- data/spec/lib/multi_read_spec.rb +6 -6
- data/spec/lib/query_spec.rb +43 -43
- data/spec/lib/statistics_spec.rb +28 -28
- data/spec/lib/strategy/base_spec.rb +98 -87
- data/spec/lib/strategy/full_table_cache_spec.rb +68 -0
- data/spec/lib/strategy/index_cache_spec.rb +112 -69
- data/spec/lib/strategy/query_cache_spec.rb +83 -0
- data/spec/lib/strategy/unique_index_on_id_cache_spec.rb +317 -0
- data/spec/lib/strategy/unique_index_on_string_cache_spec.rb +168 -0
- data/spec/lib/strategy/util_spec.rb +67 -49
- data/spec/lib/version_store_spec.rb +22 -41
- data/spec/models/address.rb +9 -0
- data/spec/models/apple.rb +1 -1
- data/spec/models/banana.rb +21 -2
- data/spec/models/language.rb +5 -0
- data/spec/models/person.rb +1 -1
- data/spec/models/store.rb +2 -1
- data/spec/spec_helper.rb +7 -4
- data/spec/support/after_commit.rb +2 -0
- data/spec/support/matchers/hit_cache_matcher.rb +10 -6
- data/spec/support/matchers/log.rb +45 -0
- data/spec/support/matchers/miss_cache_matcher.rb +10 -6
- data/spec/support/matchers/use_cache_matcher.rb +10 -6
- metadata +156 -161
- data/lib/record_cache/strategy/id_cache.rb +0 -93
- data/lib/record_cache/strategy/request_cache.rb +0 -49
- data/spec/lib/strategy/id_cache_spec.rb +0 -168
- data/spec/lib/strategy/request_cache_spec.rb +0 -85
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NjcyYzNmZjBjM2VjZGU4MzMzMGI1YjBkNDdiNmQ5NWZiODNiZjE3OA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MzU0ZDM3ZjE1YzhmNTY2MTM3M2MwZmU4ZjZhZTI5ZGQ1NWMwNTMwZg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NDE4MTQxYjA2ZDQ2ZTE1ZWVlOTk1MjY5OGU3ZDJjNThjZGQ4MjIxZTE1MWQy
|
10
|
+
MjMyZmViZmUzZWY2MGU5NDUzOWNhOWFiYWY0MTlmYTAzMTJlOTVjZTA2N2Yz
|
11
|
+
ODgyNDFiMWY5YzU1MzRhYzNjZWM5M2UyMjU1OGFmNzJlMjliMGI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NDBiNGYyZjIwNDlkMzg5MDU0YjhhNjBjNThiMDhjMzk1YzE1YjgxODUwYTJj
|
14
|
+
ZTk5YzAzNzY1ZmU4MWI2ZjYyYTU1MjQ4NjNkOTEwZmIxMWMzNzAyYWY5NGJj
|
15
|
+
YTc2ZDVlN2UyOTZhMzBhY2NjMmUwMWQ0NDI5MjliYjY5MGZjM2Y=
|
data/lib/record_cache.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Record Cache files
|
2
|
+
require "record_cache/version"
|
2
3
|
["query", "version_store", "multi_read",
|
3
|
-
"strategy/util", "strategy/base", "strategy/
|
4
|
+
"strategy/util", "strategy/base", "strategy/unique_index_cache", "strategy/full_table_cache", "strategy/index_cache",
|
4
5
|
"statistics", "dispatcher", "base"].each do |file|
|
5
6
|
require File.dirname(__FILE__) + "/record_cache/#{file}.rb"
|
6
7
|
end
|
data/lib/record_cache/base.rb
CHANGED
@@ -17,7 +17,16 @@ module RecordCache
|
|
17
17
|
|
18
18
|
# The logger instance (Rails.logger if present)
|
19
19
|
def logger
|
20
|
-
@logger ||=
|
20
|
+
@logger ||= (rails_logger || ::ActiveRecord::Base.logger)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Provide a different logger for Record Cache related information
|
24
|
+
def logger=(logger)
|
25
|
+
@logger = logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def rails_logger
|
29
|
+
defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
21
30
|
end
|
22
31
|
|
23
32
|
# Set the ActiveSupport::Cache::Store instance that contains the current record(group) versions.
|
@@ -29,16 +38,17 @@ module RecordCache
|
|
29
38
|
# The ActiveSupport::Cache::Store instance that contains the current record(group) versions.
|
30
39
|
# Note that it must point to a single Store shared by all webservers (defaults to Rails.cache)
|
31
40
|
def version_store
|
32
|
-
|
41
|
+
self.version_store = Rails.cache unless @version_store
|
42
|
+
@version_store
|
33
43
|
end
|
34
|
-
|
35
|
-
# Register a store
|
44
|
+
|
45
|
+
# Register a cache store by id for future reference with the :store option for +cache_records+
|
36
46
|
# e.g. RecordCache::Base.register_store(:server, ActiveSupport::Cache.lookup_store(:memory_store))
|
37
47
|
def register_store(id, store)
|
38
48
|
stores[id] = RecordCache::MultiRead.test(store)
|
39
49
|
end
|
40
50
|
|
41
|
-
# The hash of stores (store_id => store)
|
51
|
+
# The hash of registered record stores (store_id => store)
|
42
52
|
def stores
|
43
53
|
@stores ||= {}
|
44
54
|
end
|
@@ -56,6 +66,27 @@ module RecordCache
|
|
56
66
|
@status = RecordCache::ENABLED
|
57
67
|
end
|
58
68
|
|
69
|
+
# Executes the block with caching enabled.
|
70
|
+
# Useful in testing scenarios.
|
71
|
+
#
|
72
|
+
# RecordCache::Base.enabled do
|
73
|
+
# @foo = Article.find(1)
|
74
|
+
# @foo.update_attributes(:time_spent => 45)
|
75
|
+
# @foo = Article.find(1)
|
76
|
+
# @foo.time_spent.should be_nil
|
77
|
+
# TimeSpent.last.amount.should == 45
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
def enabled(&block)
|
81
|
+
previous_status = @status
|
82
|
+
begin
|
83
|
+
@status = RecordCache::ENABLED
|
84
|
+
yield
|
85
|
+
ensure
|
86
|
+
@status = previous_status
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
59
90
|
# Retrieve the current status
|
60
91
|
def status
|
61
92
|
@status ||= RecordCache::ENABLED
|
@@ -78,57 +109,67 @@ module RecordCache
|
|
78
109
|
|
79
110
|
module ClassMethods
|
80
111
|
# Cache the instances of this model
|
81
|
-
# options:
|
112
|
+
# generic options:
|
82
113
|
# :store => the cache store for the instances, e.g. :memory_store, :dalli_store* (default: Rails.cache)
|
83
114
|
# or one of the store ids defined using +RecordCache::Base.register_store+
|
84
115
|
# :key => provide a unique shorter key to limit the cache key length (default: model.name)
|
116
|
+
#
|
117
|
+
# cache strategy specific options:
|
85
118
|
# :index => one or more attributes (Symbols) for which the ids are cached for the value of the attribute
|
86
|
-
# :request_cache => Set to true in case the exact same query is executed more than once during a single request
|
87
|
-
# If set to true somewhere, make sure to add the following to your application controller:
|
88
|
-
# before_filter { |c| RecordCache::Strategy::RequestCache.clear }
|
89
119
|
#
|
90
120
|
# Hints:
|
91
121
|
# - Dalli is a high performance pure Ruby client for accessing memcached servers, see https://github.com/mperham/dalli
|
92
122
|
# - use :store => :memory_store in case all records can easily fit in server memory
|
93
123
|
# - use :index => :account_id in case the records are (almost) always queried as a full set per account
|
94
124
|
# - use :index => :person_id for aggregated has_many associations
|
95
|
-
def cache_records(options)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
125
|
+
def cache_records(options = {})
|
126
|
+
unless @rc_dispatcher
|
127
|
+
@rc_dispatcher = RecordCache::Dispatcher.new(self)
|
128
|
+
# Callback for Data Store specific initialization
|
129
|
+
record_cache_init
|
130
|
+
|
131
|
+
class << self
|
132
|
+
alias_method_chain :inherited, :record_cache
|
133
|
+
end
|
103
134
|
end
|
104
|
-
# parse
|
105
|
-
|
106
|
-
# Callback for Data Store specific initialization
|
107
|
-
record_cache_init
|
135
|
+
# parse the requested strategies from the given options
|
136
|
+
@rc_dispatcher.parse(options)
|
108
137
|
end
|
109
138
|
|
110
139
|
# Returns true if record cache is defined and active for this class
|
111
140
|
def record_cache?
|
112
|
-
record_cache && RecordCache::Base.status == RecordCache::ENABLED
|
141
|
+
record_cache && record_cache.instance_variable_get('@base') == self && RecordCache::Base.status == RecordCache::ENABLED
|
113
142
|
end
|
114
143
|
|
115
144
|
# Returns the RecordCache (class) instance
|
116
145
|
def record_cache
|
117
146
|
@rc_dispatcher
|
118
147
|
end
|
148
|
+
|
149
|
+
def inherited_with_record_cache(subclass)
|
150
|
+
class << subclass
|
151
|
+
def record_cache
|
152
|
+
self.superclass.record_cache
|
153
|
+
end
|
154
|
+
end
|
155
|
+
inherited_without_record_cache(subclass)
|
156
|
+
end
|
119
157
|
end
|
120
158
|
|
121
159
|
module InstanceMethods
|
122
160
|
def record_cache_create
|
123
161
|
self.class.record_cache.record_change(self, :create) unless RecordCache::Base.status == RecordCache::DISABLED
|
162
|
+
true
|
124
163
|
end
|
125
164
|
|
126
165
|
def record_cache_update
|
127
166
|
self.class.record_cache.record_change(self, :update) unless RecordCache::Base.status == RecordCache::DISABLED
|
167
|
+
true
|
128
168
|
end
|
129
169
|
|
130
170
|
def record_cache_destroy
|
131
171
|
self.class.record_cache.record_change(self, :destroy) unless RecordCache::Base.status == RecordCache::DISABLED
|
172
|
+
true
|
132
173
|
end
|
133
174
|
end
|
134
175
|
|
@@ -4,8 +4,10 @@ require 'active_record'
|
|
4
4
|
ActiveRecord::Base.send(:include, RecordCache::Base)
|
5
5
|
|
6
6
|
# To be able to fetch records from the cache and invalidate records in the cache
|
7
|
-
# some internal Active Record methods
|
7
|
+
# some internal Active Record methods need to be aliased.
|
8
8
|
# The downside of using internal methods, is that they may change in different releases,
|
9
9
|
# hence the following code:
|
10
|
-
AR_VERSION = ActiveRecord::VERSION::MAJOR
|
11
|
-
|
10
|
+
AR_VERSION = "#{ActiveRecord::VERSION::MAJOR}#{ActiveRecord::VERSION::MINOR}"
|
11
|
+
filename = "#{File.dirname(__FILE__)}/active_record_#{AR_VERSION}.rb"
|
12
|
+
abort("No support for Active Record version #{AR_VERSION}") unless File.exists?(filename)
|
13
|
+
require filename
|
@@ -10,38 +10,49 @@ module RecordCache
|
|
10
10
|
alias_method_chain :find_by_sql, :record_cache
|
11
11
|
end
|
12
12
|
end
|
13
|
-
include InstanceMethods
|
14
13
|
end
|
15
14
|
end
|
16
15
|
|
17
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
|
18
19
|
|
19
20
|
# add cache invalidation hooks on initialization
|
20
21
|
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
|
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
|
24
25
|
end
|
25
|
-
|
26
|
+
|
26
27
|
# Retrieve the records, possibly from cache
|
27
|
-
def find_by_sql_with_record_cache(
|
28
|
-
|
29
|
-
return find_by_sql_without_record_cache(
|
28
|
+
def find_by_sql_with_record_cache(sql)
|
29
|
+
# shortcut, no caching please
|
30
|
+
return find_by_sql_without_record_cache(sql) unless record_cache?
|
30
31
|
|
31
|
-
# check the piggy-back'd ActiveRelation record to see if the query can be retrieved from cache
|
32
|
-
sql = args[0]
|
33
32
|
arel = sql.instance_variable_get(:@arel)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
33
|
+
sanitized_sql = sanitize_sql(sql)
|
34
|
+
|
35
|
+
records = if connection.instance_variable_get(:@query_cache_enabled)
|
36
|
+
query_cache = connection.instance_variable_get(:@query_cache)
|
37
|
+
query_cache["rc/#{sanitized_sql}"] ||= try_record_cache(sanitized_sql, arel)
|
38
|
+
elsif connection.open_transactions > RC_TRANSACTIONS_THRESHOLD
|
39
|
+
connection.send(:select, sanitized_sql, "#{name} Load")
|
40
|
+
else
|
41
|
+
try_record_cache(sanitized_sql, arel)
|
42
|
+
end
|
43
|
+
records.collect! { |record| instantiate(record) } if records[0].is_a?(Hash)
|
44
|
+
records
|
45
|
+
end
|
46
|
+
|
47
|
+
def try_record_cache(sql, arel)
|
48
|
+
query = arel && arel.respond_to?(:ast) ? RecordCache::Arel::QueryVisitor.new.accept(arel.ast) : nil
|
49
|
+
record_cache.fetch(query) do
|
50
|
+
connection.send(:select, sql, "#{name} Load")
|
51
|
+
end
|
40
52
|
end
|
41
|
-
end
|
42
53
|
|
43
|
-
module InstanceMethods
|
44
54
|
end
|
55
|
+
|
45
56
|
end
|
46
57
|
end
|
47
58
|
|
@@ -62,7 +73,7 @@ module RecordCache
|
|
62
73
|
|
63
74
|
module ClassMethods
|
64
75
|
end
|
65
|
-
|
76
|
+
|
66
77
|
module InstanceMethods
|
67
78
|
def to_sql_with_record_cache
|
68
79
|
sql = to_sql_without_record_cache
|
@@ -71,20 +82,23 @@ module RecordCache
|
|
71
82
|
end
|
72
83
|
end
|
73
84
|
end
|
74
|
-
|
85
|
+
|
75
86
|
# Visitor for the ActiveRelation to extract a simple cache query
|
76
87
|
# Only accepts single select queries with equality where statements
|
77
88
|
# Rejects queries with grouping / having / offset / etc.
|
78
89
|
class QueryVisitor < ::Arel::Visitors::Visitor
|
90
|
+
DESC = "DESC".freeze
|
91
|
+
COMMA = ",".freeze
|
92
|
+
|
79
93
|
def initialize
|
80
94
|
super()
|
81
95
|
@cacheable = true
|
82
96
|
@query = ::RecordCache::Query.new
|
83
97
|
end
|
84
98
|
|
85
|
-
def accept
|
99
|
+
def accept ast
|
86
100
|
super
|
87
|
-
@cacheable ? @query : nil
|
101
|
+
@cacheable && !ast.lock ? @query : nil
|
88
102
|
end
|
89
103
|
|
90
104
|
private
|
@@ -93,12 +107,16 @@ module RecordCache
|
|
93
107
|
@cacheable = false
|
94
108
|
end
|
95
109
|
|
96
|
-
|
97
|
-
|
110
|
+
def skip o
|
111
|
+
end
|
112
|
+
|
98
113
|
alias :visit_Arel_Nodes_TableAlias :not_cacheable
|
99
114
|
|
115
|
+
alias :visit_Arel_Nodes_Lock :not_cacheable
|
116
|
+
|
100
117
|
alias :visit_Arel_Nodes_Sum :not_cacheable
|
101
118
|
alias :visit_Arel_Nodes_Max :not_cacheable
|
119
|
+
alias :visit_Arel_Nodes_Min :not_cacheable
|
102
120
|
alias :visit_Arel_Nodes_Avg :not_cacheable
|
103
121
|
alias :visit_Arel_Nodes_Count :not_cacheable
|
104
122
|
|
@@ -110,6 +128,13 @@ module RecordCache
|
|
110
128
|
alias :visit_Arel_Nodes_InsertStatement :not_cacheable
|
111
129
|
alias :visit_Arel_Nodes_UpdateStatement :not_cacheable
|
112
130
|
|
131
|
+
alias :visit_Arel_Nodes_Except :not_cacheable
|
132
|
+
alias :visit_Arel_Nodes_Exists :not_cacheable
|
133
|
+
alias :visit_Arel_Nodes_Intersect :not_cacheable
|
134
|
+
alias :visit_Arel_Nodes_Union :not_cacheable
|
135
|
+
alias :visit_Arel_Nodes_UnionAll :not_cacheable
|
136
|
+
|
137
|
+
alias :visit_Arel_Nodes_As :skip
|
113
138
|
|
114
139
|
alias :unary :not_cacheable
|
115
140
|
alias :visit_Arel_Nodes_Group :unary
|
@@ -131,14 +156,14 @@ module RecordCache
|
|
131
156
|
end
|
132
157
|
alias :visit_Arel_Nodes_Top :visit_Arel_Nodes_Limit
|
133
158
|
|
159
|
+
GROUPING_EQUALS_REGEXP = /^\W?(\w*)\W?\.\W?(\w*)\W?\s*=\s*(\d+)$/ # `calendars`.account_id = 5
|
160
|
+
GROUPING_IN_REGEXP = /^^\W?(\w*)\W?\.\W?(\w*)\W?\s*IN\s*\(([\d\s,]+)\)$/ # `service_instances`.`id` IN (118,80,120,82)
|
134
161
|
def visit_Arel_Nodes_Grouping o
|
135
162
|
return unless @cacheable
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
elsif o.expr =~ /^`#{@table_name}`\.`?(\w*)`?\s*IN\s*\(([\d\s,]+)\)$/
|
141
|
-
@cacheable = @query.where($1, $2.split(',').map(&:to_i))
|
163
|
+
if @table_name && o.expr =~ GROUPING_EQUALS_REGEXP && $1 == @table_name
|
164
|
+
@cacheable = @query.where($2, $3.to_i)
|
165
|
+
elsif @table_name && o.expr =~ GROUPING_IN_REGEXP && $1 == @table_name
|
166
|
+
@cacheable = @query.where($2, $3.split(',').map(&:to_i))
|
142
167
|
else
|
143
168
|
@cacheable = false
|
144
169
|
end
|
@@ -160,12 +185,13 @@ module RecordCache
|
|
160
185
|
visit o.cores
|
161
186
|
end
|
162
187
|
end
|
163
|
-
|
188
|
+
|
189
|
+
ORDER_BY_REGEXP = /^\s*([\w\.]*)\s*(|ASC|asc|DESC|desc)\s*$/ # people.id DESC
|
164
190
|
def handle_order_by(order)
|
165
|
-
order.to_s.split(
|
166
|
-
# simple sort order (+
|
167
|
-
if o.match(
|
168
|
-
asc = $2 ==
|
191
|
+
order.to_s.split(COMMA).each do |o|
|
192
|
+
# simple sort order (+people.id+ can be replaced by +id+, as joins are not allowed anyways)
|
193
|
+
if o.match(ORDER_BY_REGEXP)
|
194
|
+
asc = $2.upcase == DESC ? false : true
|
169
195
|
@query.order_by($1.split('.').last, asc)
|
170
196
|
else
|
171
197
|
@cacheable = false
|
@@ -189,6 +215,7 @@ module RecordCache
|
|
189
215
|
alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
|
190
216
|
alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
|
191
217
|
alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
|
218
|
+
alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
|
192
219
|
|
193
220
|
def visit_Arel_Nodes_Equality o
|
194
221
|
key, value = visit(o.left), visit(o.right)
|
@@ -249,7 +276,7 @@ module RecordCache
|
|
249
276
|
end
|
250
277
|
|
251
278
|
module RecordCache
|
252
|
-
|
279
|
+
|
253
280
|
# Patch ActiveRecord::Relation to make sure update_all will invalidate all referenced records
|
254
281
|
module ActiveRecord
|
255
282
|
module UpdateAll
|
@@ -262,19 +289,49 @@ module RecordCache
|
|
262
289
|
end
|
263
290
|
end
|
264
291
|
end
|
265
|
-
|
292
|
+
|
266
293
|
module ClassMethods
|
267
294
|
end
|
268
295
|
|
269
296
|
module InstanceMethods
|
297
|
+
def __find_in_clause(sub_select)
|
298
|
+
return nil unless sub_select.arel.constraints.count == 1
|
299
|
+
constraint = sub_select.arel.constraints.first
|
300
|
+
return constraint if constraint.is_a?(::Arel::Nodes::In) # directly an IN clause
|
301
|
+
return nil unless constraint.respond_to?(:children) && constraint.children.count == 1
|
302
|
+
constraint = constraint.children.first
|
303
|
+
return constraint if constraint.is_a?(::Arel::Nodes::In) # AND with IN clause
|
304
|
+
nil
|
305
|
+
end
|
306
|
+
|
270
307
|
def update_all_with_record_cache(updates, conditions = nil, options = {})
|
271
308
|
result = update_all_without_record_cache(updates, conditions, options)
|
272
309
|
|
273
310
|
if record_cache?
|
274
311
|
# when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
|
275
312
|
unless conditions || options.present? || @limit_value.present? != @order_values.present?
|
313
|
+
# get all attributes that contain a unique index for this model
|
314
|
+
unique_index_attributes = RecordCache::Strategy::UniqueIndexCache.attributes(self)
|
276
315
|
# go straight to SQL result (without instantiating records) for optimal performance
|
277
|
-
|
316
|
+
RecordCache::Base.version_store.multi do
|
317
|
+
sub_select = select(unique_index_attributes.map(&:to_s).join(','))
|
318
|
+
in_clause = __find_in_clause(sub_select)
|
319
|
+
if unique_index_attributes.size == 1 && in_clause &&
|
320
|
+
in_clause.left.try(:name).to_s == unique_index_attributes.first.to_s
|
321
|
+
# common case where the unique index is the (only) constraint on the query: SELECT id FROM people WHERE id in (...)
|
322
|
+
attribute = unique_index_attributes.first
|
323
|
+
in_clause.right.each do |value|
|
324
|
+
record_cache.invalidate(attribute, value)
|
325
|
+
end
|
326
|
+
else
|
327
|
+
connection.execute(sub_select.to_sql).each do |row|
|
328
|
+
# invalidate the unique index for all attributes
|
329
|
+
unique_index_attributes.each_with_index do |attribute, index|
|
330
|
+
record_cache.invalidate(attribute, (row.is_a?(Hash) ? row[attribute.to_s] : row[index]))
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
278
335
|
end
|
279
336
|
end
|
280
337
|
|
@@ -10,42 +10,52 @@ module RecordCache
|
|
10
10
|
alias_method_chain :find_by_sql, :record_cache
|
11
11
|
end
|
12
12
|
end
|
13
|
-
include InstanceMethods
|
14
13
|
end
|
15
14
|
end
|
16
15
|
|
17
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
|
18
19
|
|
19
20
|
# add cache invalidation hooks on initialization
|
20
21
|
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
|
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
|
24
25
|
end
|
25
|
-
|
26
|
+
|
26
27
|
# Retrieve the records, possibly from cache
|
27
|
-
def find_by_sql_with_record_cache(
|
28
|
-
|
29
|
-
return find_by_sql_without_record_cache(
|
30
|
-
|
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
|
+
|
31
32
|
# check the piggy-back'd ActiveRelation record to see if the query can be retrieved from cache
|
32
|
-
arel =
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
33
|
+
arel = sql.is_a?(String) ? sql.instance_variable_get(:@arel) : sql
|
34
|
+
|
35
|
+
sanitized_sql = sanitize_sql(sql)
|
36
|
+
sanitized_sql = connection.visitor.accept(sanitized_sql.ast) if sanitized_sql.respond_to?(:ast)
|
37
|
+
|
38
|
+
records = if connection.instance_variable_get(:@query_cache_enabled)
|
39
|
+
query_cache = connection.instance_variable_get(:@query_cache)
|
40
|
+
query_cache["rc/#{sanitized_sql}"][binds] ||= try_record_cache(arel, sanitized_sql, binds)
|
41
|
+
elsif connection.open_transactions > RC_TRANSACTIONS_THRESHOLD
|
42
|
+
connection.send(:select, sanitized_sql, "#{name} Load", binds)
|
43
|
+
else
|
44
|
+
try_record_cache(arel, sanitized_sql, binds)
|
45
|
+
end
|
46
|
+
records.collect! { |record| instantiate(record) } if records[0].is_a?(Hash)
|
47
|
+
records
|
48
|
+
end
|
37
49
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
cacheable ? record_cache.fetch(query) : find_by_sql_without_record_cache(*args)
|
50
|
+
def try_record_cache(arel, sql, binds)
|
51
|
+
query = arel && arel.respond_to?(:ast) ? RecordCache::Arel::QueryVisitor.new(binds).accept(arel.ast) : nil
|
52
|
+
record_cache.fetch(query) do
|
53
|
+
connection.send(:select, sql, "#{name} Load", binds)
|
54
|
+
end
|
44
55
|
end
|
45
|
-
end
|
46
56
|
|
47
|
-
module InstanceMethods
|
48
57
|
end
|
58
|
+
|
49
59
|
end
|
50
60
|
end
|
51
61
|
|
@@ -66,7 +76,7 @@ module RecordCache
|
|
66
76
|
|
67
77
|
module ClassMethods
|
68
78
|
end
|
69
|
-
|
79
|
+
|
70
80
|
module InstanceMethods
|
71
81
|
def to_sql_with_record_cache
|
72
82
|
sql = to_sql_without_record_cache
|
@@ -75,11 +85,14 @@ module RecordCache
|
|
75
85
|
end
|
76
86
|
end
|
77
87
|
end
|
78
|
-
|
88
|
+
|
79
89
|
# Visitor for the ActiveRelation to extract a simple cache query
|
80
90
|
# Only accepts single select queries with equality where statements
|
81
91
|
# Rejects queries with grouping / having / offset / etc.
|
82
92
|
class QueryVisitor < ::Arel::Visitors::Visitor
|
93
|
+
DESC = "DESC".freeze
|
94
|
+
COMMA = ",".freeze
|
95
|
+
|
83
96
|
def initialize(bindings)
|
84
97
|
super()
|
85
98
|
@bindings = (bindings || []).inject({}){ |h, cv| column, value = cv; h[column.name] = value; h}
|
@@ -87,9 +100,9 @@ module RecordCache
|
|
87
100
|
@query = ::RecordCache::Query.new
|
88
101
|
end
|
89
102
|
|
90
|
-
def accept
|
103
|
+
def accept ast
|
91
104
|
super
|
92
|
-
@cacheable ? @query : nil
|
105
|
+
@cacheable && !ast.lock ? @query : nil
|
93
106
|
end
|
94
107
|
|
95
108
|
private
|
@@ -98,14 +111,46 @@ module RecordCache
|
|
98
111
|
@cacheable = false
|
99
112
|
end
|
100
113
|
|
101
|
-
|
102
|
-
|
114
|
+
def skip o
|
115
|
+
end
|
116
|
+
|
103
117
|
alias :visit_Arel_Nodes_TableAlias :not_cacheable
|
104
118
|
|
105
|
-
alias :
|
106
|
-
|
107
|
-
alias :
|
108
|
-
alias :
|
119
|
+
alias :visit_Arel_Nodes_Lock :not_cacheable
|
120
|
+
|
121
|
+
alias :visit_Arel_Nodes_Sum :not_cacheable
|
122
|
+
alias :visit_Arel_Nodes_Max :not_cacheable
|
123
|
+
alias :visit_Arel_Nodes_Min :not_cacheable
|
124
|
+
alias :visit_Arel_Nodes_Avg :not_cacheable
|
125
|
+
alias :visit_Arel_Nodes_Count :not_cacheable
|
126
|
+
alias :visit_Arel_Nodes_Addition :not_cacheable
|
127
|
+
alias :visit_Arel_Nodes_Subtraction :not_cacheable
|
128
|
+
alias :visit_Arel_Nodes_Multiplication :not_cacheable
|
129
|
+
alias :visit_Arel_Nodes_NamedFunction :not_cacheable
|
130
|
+
|
131
|
+
alias :visit_Arel_Nodes_Bin :not_cacheable
|
132
|
+
alias :visit_Arel_Nodes_Distinct :not_cacheable
|
133
|
+
alias :visit_Arel_Nodes_DistinctOn :not_cacheable
|
134
|
+
alias :visit_Arel_Nodes_Division :not_cacheable
|
135
|
+
alias :visit_Arel_Nodes_Except :not_cacheable
|
136
|
+
alias :visit_Arel_Nodes_Exists :not_cacheable
|
137
|
+
alias :visit_Arel_Nodes_InfixOperation :not_cacheable
|
138
|
+
alias :visit_Arel_Nodes_Intersect :not_cacheable
|
139
|
+
alias :visit_Arel_Nodes_Union :not_cacheable
|
140
|
+
alias :visit_Arel_Nodes_UnionAll :not_cacheable
|
141
|
+
alias :visit_Arel_Nodes_With :not_cacheable
|
142
|
+
alias :visit_Arel_Nodes_WithRecursive :not_cacheable
|
143
|
+
|
144
|
+
def visit_Arel_Nodes_JoinSource o
|
145
|
+
# left and right are array, but using blank as it also works for nil
|
146
|
+
@cacheable = o.left.blank? || o.right.blank?
|
147
|
+
end
|
148
|
+
|
149
|
+
alias :visit_Arel_Nodes_As :skip
|
150
|
+
alias :visit_Arel_Nodes_Ascending :skip
|
151
|
+
alias :visit_Arel_Nodes_Descending :skip
|
152
|
+
alias :visit_Arel_Nodes_False :skip
|
153
|
+
alias :visit_Arel_Nodes_True :skip
|
109
154
|
|
110
155
|
alias :visit_Arel_Nodes_StringJoin :not_cacheable
|
111
156
|
alias :visit_Arel_Nodes_InnerJoin :not_cacheable
|
@@ -136,14 +181,14 @@ module RecordCache
|
|
136
181
|
end
|
137
182
|
alias :visit_Arel_Nodes_Top :visit_Arel_Nodes_Limit
|
138
183
|
|
184
|
+
GROUPING_EQUALS_REGEXP = /^\W?(\w*)\W?\.\W?(\w*)\W?\s*=\s*(\d+)$/ # `calendars`.account_id = 5
|
185
|
+
GROUPING_IN_REGEXP = /^^\W?(\w*)\W?\.\W?(\w*)\W?\s*IN\s*\(([\d\s,]+)\)$/ # `service_instances`.`id` IN (118,80,120,82)
|
139
186
|
def visit_Arel_Nodes_Grouping o
|
140
187
|
return unless @cacheable
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
elsif o.expr =~ /^`#{@table_name}`\.`?(\w*)`?\s*IN\s*\(([\d\s,]+)\)$/
|
146
|
-
@cacheable = @query.where($1, $2.split(',').map(&:to_i))
|
188
|
+
if @table_name && o.expr =~ GROUPING_EQUALS_REGEXP && $1 == @table_name
|
189
|
+
@cacheable = @query.where($2, $3.to_i)
|
190
|
+
elsif @table_name && o.expr =~ GROUPING_IN_REGEXP && $1 == @table_name
|
191
|
+
@cacheable = @query.where($2, $3.split(',').map(&:to_i))
|
147
192
|
else
|
148
193
|
@cacheable = false
|
149
194
|
end
|
@@ -153,6 +198,7 @@ module RecordCache
|
|
153
198
|
@cacheable = false unless o.groups.empty?
|
154
199
|
visit o.froms if @cacheable
|
155
200
|
visit o.wheres if @cacheable
|
201
|
+
visit o.source if @cacheable
|
156
202
|
# skip o.projections
|
157
203
|
end
|
158
204
|
|
@@ -165,12 +211,13 @@ module RecordCache
|
|
165
211
|
visit o.cores
|
166
212
|
end
|
167
213
|
end
|
168
|
-
|
214
|
+
|
215
|
+
ORDER_BY_REGEXP = /^\s*([\w\.]*)\s*(|ASC|asc|DESC|desc)\s*$/ # people.id DESC
|
169
216
|
def handle_order_by(order)
|
170
|
-
order.to_s.split(
|
171
|
-
# simple sort order (+
|
172
|
-
if o.match(
|
173
|
-
asc = $2 ==
|
217
|
+
order.to_s.split(COMMA).each do |o|
|
218
|
+
# simple sort order (+people.id+ can be replaced by +id+, as joins are not allowed anyways)
|
219
|
+
if o.match(ORDER_BY_REGEXP)
|
220
|
+
asc = $2.upcase == DESC ? false : true
|
174
221
|
@query.order_by($1.split('.').last, asc)
|
175
222
|
else
|
176
223
|
@cacheable = false
|
@@ -182,10 +229,6 @@ module RecordCache
|
|
182
229
|
@table_name = o.name
|
183
230
|
end
|
184
231
|
|
185
|
-
def visit_Arel_Nodes_Ordering o
|
186
|
-
[visit(o.expr), o.descending]
|
187
|
-
end
|
188
|
-
|
189
232
|
def visit_Arel_Attributes_Attribute o
|
190
233
|
o.name.to_sym
|
191
234
|
end
|
@@ -194,12 +237,14 @@ module RecordCache
|
|
194
237
|
alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
|
195
238
|
alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
|
196
239
|
alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
|
240
|
+
alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
|
197
241
|
|
198
242
|
def visit_Arel_Nodes_Equality o
|
199
243
|
key, value = visit(o.left), visit(o.right)
|
200
|
-
|
244
|
+
# several different binding markers exist depending on the db driver used (MySQL, Postgress supported)
|
245
|
+
if value.to_s =~ /^(\?|\u0000|\$\d+)$/
|
201
246
|
# puts "bindings: #{@bindings.inspect}, key = #{key.to_s}"
|
202
|
-
value = @bindings[key.to_s] ||
|
247
|
+
value = @bindings[key.to_s] || value
|
203
248
|
end
|
204
249
|
# puts " =====> equality found: #{key.inspect}@#{key.class.name} => #{value.inspect}@#{value.class.name}"
|
205
250
|
@query.where(key, value)
|
@@ -207,8 +252,7 @@ module RecordCache
|
|
207
252
|
alias :visit_Arel_Nodes_In :visit_Arel_Nodes_Equality
|
208
253
|
|
209
254
|
def visit_Arel_Nodes_And o
|
210
|
-
visit(o.
|
211
|
-
visit(o.right)
|
255
|
+
visit(o.children)
|
212
256
|
end
|
213
257
|
|
214
258
|
alias :visit_Arel_Nodes_Or :not_cacheable
|
@@ -236,6 +280,7 @@ module RecordCache
|
|
236
280
|
o
|
237
281
|
end
|
238
282
|
alias :visit_Arel_Nodes_SqlLiteral :visit_Object
|
283
|
+
alias :visit_Arel_Nodes_BindParam :visit_Object
|
239
284
|
alias :visit_Arel_SqlLiteral :visit_Object # This is deprecated
|
240
285
|
alias :visit_String :visit_Object
|
241
286
|
alias :visit_NilClass :visit_Object
|
@@ -258,7 +303,7 @@ module RecordCache
|
|
258
303
|
end
|
259
304
|
|
260
305
|
module RecordCache
|
261
|
-
|
306
|
+
|
262
307
|
# Patch ActiveRecord::Relation to make sure update_all will invalidate all referenced records
|
263
308
|
module ActiveRecord
|
264
309
|
module UpdateAll
|
@@ -271,19 +316,49 @@ module RecordCache
|
|
271
316
|
end
|
272
317
|
end
|
273
318
|
end
|
274
|
-
|
319
|
+
|
275
320
|
module ClassMethods
|
276
321
|
end
|
277
322
|
|
278
323
|
module InstanceMethods
|
324
|
+
def __find_in_clause(sub_select)
|
325
|
+
return nil unless sub_select.arel.constraints.count == 1
|
326
|
+
constraint = sub_select.arel.constraints.first
|
327
|
+
return constraint if constraint.is_a?(::Arel::Nodes::In) # directly an IN clause
|
328
|
+
return nil unless constraint.respond_to?(:children) && constraint.children.count == 1
|
329
|
+
constraint = constraint.children.first
|
330
|
+
return constraint if constraint.is_a?(::Arel::Nodes::In) # AND with IN clause
|
331
|
+
nil
|
332
|
+
end
|
333
|
+
|
279
334
|
def update_all_with_record_cache(updates, conditions = nil, options = {})
|
280
335
|
result = update_all_without_record_cache(updates, conditions, options)
|
281
336
|
|
282
337
|
if record_cache?
|
283
338
|
# when this condition is met, the arel.update method will be called on the current scope, see ActiveRecord::Relation#update_all
|
284
339
|
unless conditions || options.present? || @limit_value.present? != @order_values.present?
|
340
|
+
# get all attributes that contain a unique index for this model
|
341
|
+
unique_index_attributes = RecordCache::Strategy::UniqueIndexCache.attributes(self)
|
285
342
|
# go straight to SQL result (without instantiating records) for optimal performance
|
286
|
-
|
343
|
+
RecordCache::Base.version_store.multi do
|
344
|
+
sub_select = select(unique_index_attributes.map(&:to_s).join(','))
|
345
|
+
in_clause = __find_in_clause(sub_select)
|
346
|
+
if unique_index_attributes.size == 1 && in_clause &&
|
347
|
+
in_clause.left.try(:name).to_s == unique_index_attributes.first.to_s
|
348
|
+
# common case where the unique index is the (only) constraint on the query: SELECT id FROM people WHERE id in (...)
|
349
|
+
attribute = unique_index_attributes.first
|
350
|
+
in_clause.right.each do |value|
|
351
|
+
record_cache.invalidate(attribute, value)
|
352
|
+
end
|
353
|
+
else
|
354
|
+
connection.execute(sub_select.to_sql).each do |row|
|
355
|
+
# invalidate the unique index for all attributes
|
356
|
+
unique_index_attributes.each_with_index do |attribute, index|
|
357
|
+
record_cache.invalidate(attribute, (row.is_a?(Hash) ? row[attribute.to_s] : row[index]))
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
287
362
|
end
|
288
363
|
end
|
289
364
|
|
@@ -320,6 +395,33 @@ module RecordCache
|
|
320
395
|
end
|
321
396
|
end
|
322
397
|
end
|
398
|
+
|
399
|
+
module HasOne
|
400
|
+
class << self
|
401
|
+
def included(klass)
|
402
|
+
klass.extend ClassMethods
|
403
|
+
klass.send(:include, InstanceMethods)
|
404
|
+
klass.class_eval do
|
405
|
+
alias_method_chain :delete, :record_cache
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
module ClassMethods
|
411
|
+
end
|
412
|
+
|
413
|
+
module InstanceMethods
|
414
|
+
def delete_with_record_cache(method = options[:dependent])
|
415
|
+
# invalidate :id cache for all record
|
416
|
+
if load_target
|
417
|
+
target.class.record_cache.invalidate(target.id) if target.class.record_cache? unless target.new_record?
|
418
|
+
end
|
419
|
+
# invalidate the referenced class for the attribute/value pair on the index cache
|
420
|
+
@reflection.klass.record_cache.invalidate(@reflection.foreign_key.to_sym, @owner.id) if @reflection.klass.record_cache?
|
421
|
+
delete_without_record_cache(method)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
323
425
|
end
|
324
426
|
|
325
427
|
end
|
@@ -328,3 +430,4 @@ ActiveRecord::Base.send(:include, RecordCache::ActiveRecord::Base)
|
|
328
430
|
Arel::TreeManager.send(:include, RecordCache::Arel::TreeManager)
|
329
431
|
ActiveRecord::Relation.send(:include, RecordCache::ActiveRecord::UpdateAll)
|
330
432
|
ActiveRecord::Associations::HasManyAssociation.send(:include, RecordCache::ActiveRecord::HasMany)
|
433
|
+
ActiveRecord::Associations::HasOneAssociation.send(:include, RecordCache::ActiveRecord::HasOne)
|