record-cache 0.1.2 → 0.1.3
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 +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)
|