redis-memo 0.1.1 → 1.1.0
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 +4 -4
- data/lib/redis_memo.rb +48 -36
- data/lib/redis_memo/after_commit.rb +2 -2
- data/lib/redis_memo/batch.rb +36 -11
- data/lib/redis_memo/cache.rb +36 -19
- data/lib/redis_memo/connection_pool.rb +4 -3
- data/lib/redis_memo/errors.rb +9 -0
- data/lib/redis_memo/future.rb +22 -13
- data/lib/redis_memo/memoizable.rb +109 -72
- data/lib/redis_memo/memoizable/bump_version.lua +39 -0
- data/lib/redis_memo/memoizable/dependency.rb +10 -11
- data/lib/redis_memo/memoizable/invalidation.rb +68 -66
- data/lib/redis_memo/memoize_method.rb +169 -131
- data/lib/redis_memo/memoize_query.rb +135 -92
- data/lib/redis_memo/memoize_query/cached_select.rb +73 -62
- data/lib/redis_memo/memoize_query/cached_select/bind_params.rb +202 -70
- data/lib/redis_memo/memoize_query/cached_select/connection_adapter.rb +19 -10
- data/lib/redis_memo/memoize_query/invalidation.rb +22 -20
- data/lib/redis_memo/memoize_query/memoize_table_column.rb +1 -0
- data/lib/redis_memo/middleware.rb +3 -1
- data/lib/redis_memo/options.rb +111 -5
- data/lib/redis_memo/railtie.rb +11 -0
- data/lib/redis_memo/redis.rb +15 -5
- data/lib/redis_memo/testing.rb +49 -0
- data/lib/redis_memo/thread_local_var.rb +16 -0
- data/lib/redis_memo/tracer.rb +1 -0
- data/lib/redis_memo/util.rb +25 -0
- metadata +80 -4
@@ -1,23 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require_relative 'batch'
|
3
4
|
require_relative 'future'
|
4
5
|
require_relative 'memoizable'
|
5
6
|
require_relative 'middleware'
|
6
7
|
require_relative 'options'
|
8
|
+
require_relative 'util'
|
7
9
|
|
8
10
|
module RedisMemo::MemoizeMethod
|
11
|
+
# Core entry method for using RedisMemo to cache a method's results. When a method is memoized, all
|
12
|
+
# calls to the method will first check if the results exist in the RedisMemo cache before calling
|
13
|
+
# the original method.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# class Post < ApplicationRecord
|
17
|
+
# extend RedisMemo::MemoizeMethod
|
18
|
+
# def display_title
|
19
|
+
# "#{title} by #{author.display_name}"
|
20
|
+
# end
|
21
|
+
# memoize_method :display_title do |post|
|
22
|
+
# depends_on Post.where(id: post.id)
|
23
|
+
# depends_on User.where(id: post.author_id)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @param method_name [String] The name of the method to memoize
|
28
|
+
# @param method_id [String] Optionally, a method_id that's used to tag APM traces of RedisMemo calls.
|
29
|
+
# @param options [Hash] Cache options to pass to RedisMemo. These values will override the global
|
30
|
+
# cache options.
|
31
|
+
# @option options [Integer] :expires_in The TTL for this method's cached result.
|
32
|
+
# @option options [Hash] :redis_options Other valid options are ones which are passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html].
|
33
|
+
# @param depends_on [block] The method's dependency block.
|
34
|
+
# - The first parameter of the block is a reference to the object whose method is being memoized.
|
35
|
+
# - The rest of the block parameters are the memoized method's arguments.
|
36
|
+
# - Within this block, you can declare the method's dependencies as individual +RedisMemo::Memoizable+'s,
|
37
|
+
# using the +RedisMemo::Dependency.depends_on+ method. RedisMemo will automatically extract dependencies
|
38
|
+
# from this block and use them to compute a method's versioned cache key.
|
9
39
|
def memoize_method(method_name, method_id: nil, **options, &depends_on)
|
10
|
-
|
40
|
+
method_name_without_memoization = :"_redis_memo_#{method_name}_without_memoization"
|
11
41
|
method_name_with_memo = :"_redis_memo_#{method_name}_with_memo"
|
12
42
|
|
13
|
-
alias_method
|
43
|
+
alias_method method_name_without_memoization, method_name
|
14
44
|
|
15
45
|
define_method method_name_with_memo do |*args|
|
16
|
-
return
|
46
|
+
return __send__(method_name_without_memoization, *args) if RedisMemo.without_memoization?
|
17
47
|
|
18
48
|
dependent_memos = nil
|
19
49
|
if depends_on
|
20
|
-
dependency = RedisMemo::MemoizeMethod.get_or_extract_dependencies
|
50
|
+
dependency = RedisMemo::MemoizeMethod.__send__(:get_or_extract_dependencies, self, *args, &depends_on)
|
21
51
|
dependent_memos = dependency.memos
|
22
52
|
end
|
23
53
|
|
@@ -25,7 +55,7 @@ module RedisMemo::MemoizeMethod
|
|
25
55
|
self,
|
26
56
|
case method_id
|
27
57
|
when NilClass
|
28
|
-
RedisMemo::MemoizeMethod.method_id
|
58
|
+
RedisMemo::MemoizeMethod.__send__(:method_id, self, method_name)
|
29
59
|
when String, Symbol
|
30
60
|
method_id
|
31
61
|
else
|
@@ -34,7 +64,7 @@ module RedisMemo::MemoizeMethod
|
|
34
64
|
args,
|
35
65
|
dependent_memos,
|
36
66
|
options,
|
37
|
-
|
67
|
+
method_name_without_memoization,
|
38
68
|
)
|
39
69
|
|
40
70
|
if RedisMemo::Batch.current
|
@@ -44,164 +74,172 @@ module RedisMemo::MemoizeMethod
|
|
44
74
|
|
45
75
|
future.execute
|
46
76
|
rescue RedisMemo::WithoutMemoization
|
47
|
-
|
77
|
+
__send__(method_name_without_memoization, *args)
|
48
78
|
end
|
49
79
|
|
80
|
+
ruby2_keywords method_name_with_memo
|
50
81
|
alias_method method_name, method_name_with_memo
|
51
82
|
|
52
83
|
@__redis_memo_method_dependencies ||= Hash.new
|
53
84
|
@__redis_memo_method_dependencies[method_name] = depends_on
|
54
85
|
|
55
|
-
define_method :dependency_of do |
|
56
|
-
method_depends_on = self.class.instance_variable_get(:@__redis_memo_method_dependencies)[
|
86
|
+
define_method :dependency_of do |other_method_name, *method_args|
|
87
|
+
method_depends_on = self.class.instance_variable_get(:@__redis_memo_method_dependencies)[other_method_name]
|
57
88
|
unless method_depends_on
|
58
|
-
raise(
|
59
|
-
|
60
|
-
"#{method_name} is not a memoized method"
|
89
|
+
raise RedisMemo::ArgumentError.new(
|
90
|
+
"#{other_method_name} is not a memoized method",
|
61
91
|
)
|
62
92
|
end
|
63
|
-
|
93
|
+
|
94
|
+
RedisMemo::MemoizeMethod.__send__(:get_or_extract_dependencies, self, *method_args, &method_depends_on)
|
64
95
|
end
|
96
|
+
ruby2_keywords :dependency_of
|
65
97
|
end
|
66
98
|
|
67
|
-
|
68
|
-
|
69
|
-
class_name = is_class_method ? ref.name : ref.class.name
|
99
|
+
class << self
|
100
|
+
private
|
70
101
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def self.extract_dependencies(ref, *method_args, &depends_on)
|
75
|
-
dependency = RedisMemo::Memoizable::Dependency.new
|
102
|
+
def method_id(ref, method_name)
|
103
|
+
is_class_method = ref.class == Class
|
104
|
+
class_name = is_class_method ? ref.name : ref.class.name
|
76
105
|
|
77
|
-
|
78
|
-
|
79
|
-
dependency
|
80
|
-
end
|
106
|
+
"#{class_name}#{is_class_method ? '::' : '#'}#{method_name}"
|
107
|
+
end
|
81
108
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
109
|
+
ruby2_keywords def get_or_extract_dependencies(ref, *method_args, &depends_on)
|
110
|
+
if RedisMemo::Cache.local_dependency_cache
|
111
|
+
RedisMemo::Cache.local_dependency_cache[ref.class] ||= {}
|
112
|
+
RedisMemo::Cache.local_dependency_cache[ref.class][depends_on] ||= {}
|
113
|
+
named_args = exclude_anonymous_args(depends_on, ref, method_args)
|
114
|
+
RedisMemo::Cache.local_dependency_cache[ref.class][depends_on][named_args] ||= extract_dependencies(ref, *method_args, &depends_on)
|
115
|
+
else
|
116
|
+
extract_dependencies(ref, *method_args, &depends_on)
|
117
|
+
end
|
90
118
|
end
|
91
|
-
end
|
92
119
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
return [] if depends_on.parameters.empty? or args.empty?
|
107
|
-
|
108
|
-
positional_args = []
|
109
|
-
kwargs = {}
|
110
|
-
depends_on_args = [ref] + args
|
111
|
-
options = depends_on_args.extract_options!
|
112
|
-
|
113
|
-
# Keep track of the splat start index, and the number of positional args before and after the splat,
|
114
|
-
# so we can map which args belong to positional args and which args belong to the splat.
|
115
|
-
named_splat = false
|
116
|
-
splat_index = nil
|
117
|
-
num_positional_args_after_splat = 0
|
118
|
-
num_positional_args_before_splat = 0
|
119
|
-
|
120
|
-
depends_on.parameters.each_with_index do |param, i|
|
121
|
-
# Defined by https://github.com/ruby/ruby/blob/22b8ddfd1049c3fd1e368684c4fd03bceb041b3a/proc.c#L3048-L3059
|
122
|
-
case param.first
|
123
|
-
when :opt, :req
|
124
|
-
if splat_index
|
125
|
-
num_positional_args_after_splat += 1
|
120
|
+
def method_cache_keys(future_contexts)
|
121
|
+
memos = Array.new(future_contexts.size)
|
122
|
+
future_contexts.each_with_index do |(_, _, dependent_memos), i|
|
123
|
+
memos[i] = dependent_memos
|
124
|
+
end
|
125
|
+
|
126
|
+
j = 0
|
127
|
+
memo_checksums = RedisMemo::Memoizable.__send__(:checksums, memos.compact)
|
128
|
+
method_cache_key_versions = Array.new(future_contexts.size)
|
129
|
+
future_contexts.each_with_index do |(method_id, method_args, _), i|
|
130
|
+
if memos[i]
|
131
|
+
method_cache_key_versions[i] = [method_id, memo_checksums[j]]
|
132
|
+
j += 1
|
126
133
|
else
|
127
|
-
|
134
|
+
ordered_method_args = method_args.map do |arg|
|
135
|
+
arg.is_a?(Hash) ? RedisMemo::Util.deep_sort_hash(arg) : arg
|
136
|
+
end
|
137
|
+
|
138
|
+
method_cache_key_versions[i] = [
|
139
|
+
method_id,
|
140
|
+
RedisMemo::Util.checksum(ordered_method_args.to_json),
|
141
|
+
]
|
128
142
|
end
|
129
|
-
when :rest
|
130
|
-
named_splat = is_named?(param)
|
131
|
-
splat_index = i
|
132
|
-
when :key, :keyreq
|
133
|
-
kwargs[param.last] = options[param.last] if is_named?(param)
|
134
|
-
when :keyrest
|
135
|
-
kwargs.merge!(options) if is_named?(param)
|
136
|
-
else
|
137
|
-
raise(RedisMemo::ArgumentError, "#{param.first} argument isn't supported in the dependency block")
|
138
143
|
end
|
139
|
-
end
|
140
144
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
145
|
+
method_cache_key_versions.map do |method_id, method_cache_key_version|
|
146
|
+
# Example:
|
147
|
+
#
|
148
|
+
# RedisMemo:MyModel#slow_calculation:<global cache version>:<local
|
149
|
+
# cache version>
|
150
|
+
#
|
151
|
+
[
|
152
|
+
RedisMemo.name,
|
153
|
+
method_id,
|
154
|
+
RedisMemo::DefaultOptions.global_cache_key_version,
|
155
|
+
method_cache_key_version,
|
156
|
+
].join(':')
|
150
157
|
end
|
158
|
+
rescue RedisMemo::Cache::Rescuable
|
159
|
+
nil
|
151
160
|
end
|
152
161
|
|
153
|
-
|
154
|
-
|
155
|
-
elsif named_splat && !options.empty?
|
156
|
-
positional_args + [options]
|
157
|
-
else
|
158
|
-
positional_args
|
159
|
-
end
|
160
|
-
end
|
161
|
-
private
|
162
|
-
def self.is_named?(param)
|
163
|
-
param.size == 2 && param.last != :_
|
164
|
-
end
|
162
|
+
ruby2_keywords def extract_dependencies(ref, *method_args, &depends_on)
|
163
|
+
dependency = RedisMemo::Memoizable::Dependency.new
|
165
164
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
memos[i] = dependent_memos
|
165
|
+
# Resolve the dependency recursively
|
166
|
+
dependency.instance_exec(ref, *method_args, &depends_on)
|
167
|
+
dependency
|
170
168
|
end
|
171
169
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
170
|
+
# We only look at named method parameters in the dependency block in order
|
171
|
+
# to define its dependent memos and ignore anonymous parameters, following
|
172
|
+
# the convention that nil or :_ is an anonymous parameter.
|
173
|
+
#
|
174
|
+
# Example:
|
175
|
+
# ```
|
176
|
+
# def method(param1, param2)
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# memoize_method :method do |_, _, param2|`
|
180
|
+
# depends_on RedisMemo::Memoizable.new(param2: param2)
|
181
|
+
# end
|
182
|
+
# ```
|
183
|
+
# `exclude_anonymous_args(depends_on, ref, [1, 2])` returns [2]
|
184
|
+
def exclude_anonymous_args(depends_on, ref, args)
|
185
|
+
return [] if depends_on.parameters.empty? || args.empty?
|
186
|
+
|
187
|
+
positional_args = []
|
188
|
+
kwargs = {}
|
189
|
+
depends_on_args = [ref] + args
|
190
|
+
options = depends_on_args.extract_options!
|
191
|
+
|
192
|
+
# Keep track of the splat start index, and the number of positional args before and after the splat,
|
193
|
+
# so we can map which args belong to positional args and which args belong to the splat.
|
194
|
+
named_splat = false
|
195
|
+
splat_index = nil
|
196
|
+
num_positional_args_after_splat = 0
|
197
|
+
num_positional_args_before_splat = 0
|
198
|
+
|
199
|
+
depends_on.parameters.each_with_index do |param, i|
|
200
|
+
# Defined by https://github.com/ruby/ruby/blob/22b8ddfd1049c3fd1e368684c4fd03bceb041b3a/proc.c#L3048-L3059
|
201
|
+
case param.first
|
202
|
+
when :opt, :req
|
203
|
+
if splat_index
|
204
|
+
num_positional_args_after_splat += 1
|
205
|
+
else
|
206
|
+
num_positional_args_before_splat += 1
|
207
|
+
end
|
208
|
+
when :rest
|
209
|
+
named_splat = is_named?(param)
|
210
|
+
splat_index = i
|
211
|
+
when :key, :keyreq
|
212
|
+
kwargs[param.last] = options[param.last] if is_named?(param)
|
213
|
+
when :keyrest
|
214
|
+
kwargs.merge!(options) if is_named?(param)
|
215
|
+
else
|
216
|
+
raise RedisMemo::ArgumentError.new("#{param.first} argument isn't supported in the dependency block")
|
182
217
|
end
|
218
|
+
end
|
183
219
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
220
|
+
# Determine the named positional and splat arguments after we know the # of pos. arguments before and after splat
|
221
|
+
after_splat_index = depends_on_args.size - num_positional_args_after_splat
|
222
|
+
depends_on_args.each_with_index do |arg, i|
|
223
|
+
# if the index is within the splat
|
224
|
+
if i >= num_positional_args_before_splat && i < after_splat_index
|
225
|
+
positional_args << arg if named_splat
|
226
|
+
else
|
227
|
+
j = i < num_positional_args_before_splat ? i : i - (after_splat_index - splat_index) - 1
|
228
|
+
positional_args << arg if is_named?(depends_on.parameters[j])
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
if !kwargs.empty?
|
233
|
+
positional_args + [kwargs]
|
234
|
+
elsif named_splat && !options.empty?
|
235
|
+
positional_args + [options]
|
236
|
+
else
|
237
|
+
positional_args
|
188
238
|
end
|
189
239
|
end
|
190
240
|
|
191
|
-
|
192
|
-
|
193
|
-
#
|
194
|
-
# RedisMemo:MyModel#slow_calculation:<global cache version>:<local
|
195
|
-
# cache version>
|
196
|
-
#
|
197
|
-
[
|
198
|
-
RedisMemo.name,
|
199
|
-
method_id,
|
200
|
-
RedisMemo::DefaultOptions.global_cache_key_version,
|
201
|
-
method_cache_key_version,
|
202
|
-
].join(':')
|
241
|
+
def is_named?(param)
|
242
|
+
param.size == 2 && param.last != :_
|
203
243
|
end
|
204
|
-
rescue RedisMemo::Cache::Rescuable
|
205
|
-
nil
|
206
244
|
end
|
207
245
|
end
|
@@ -1,121 +1,164 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require_relative 'memoize_method'
|
3
2
|
|
4
|
-
|
5
|
-
# Hook into ActiveRecord to cache SQL queries and perform auto cache
|
6
|
-
# invalidation
|
7
|
-
module RedisMemo::MemoizeQuery
|
8
|
-
require_relative 'memoize_query/cached_select'
|
9
|
-
require_relative 'memoize_query/invalidation'
|
10
|
-
require_relative 'memoize_query/model_callback'
|
3
|
+
require_relative 'memoize_method'
|
11
4
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
5
|
+
# Hook into ActiveRecord to cache SQL queries and perform auto cache
|
6
|
+
# invalidation
|
7
|
+
module RedisMemo::MemoizeQuery
|
8
|
+
require_relative 'memoize_query/cached_select'
|
9
|
+
require_relative 'memoize_query/invalidation'
|
10
|
+
require_relative 'memoize_query/model_callback'
|
11
|
+
|
12
|
+
# Core entry method for using RedisMemo to cache SQL select queries on the given
|
13
|
+
# column names. We intercept any ActiveRecord select queries and extract the
|
14
|
+
# column dependencies from SQL query parameters. From the extracted dependencies
|
15
|
+
# and the memoized columns on the table, we determine whether or not the query
|
16
|
+
# should be cached on RedisMemo. Learn more in +RedisMemo::MemoizeQuery::CachedSelect+.
|
17
|
+
#
|
18
|
+
# class User < ApplicationRecord
|
19
|
+
# extend RedisMemo::MemoizeQuery
|
20
|
+
# memoize_table_column :id
|
21
|
+
# memoize_table_column :first_name
|
22
|
+
# memoize_table_column :first_name, last_name
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# On the User model, queries such as
|
26
|
+
# - record.user
|
27
|
+
# - User.find(user_id)
|
28
|
+
# - User.where(id: user_id).first
|
29
|
+
# - User.where(first_name: first_name).first
|
30
|
+
# - User.where(first_name: first_name, last_name: last_name).first
|
31
|
+
# - User.find_by_first_name(first_name)
|
32
|
+
# will first check the Redis cache for the data before hitting the SQL database;
|
33
|
+
# the cache results are invalidated automatically when user records are changed.
|
34
|
+
#
|
35
|
+
# Note that +memoize_table_column :first_name, last_name+ specifies that only AND queries
|
36
|
+
# that contain both columns will be memoized. The query +User.where(last_name: last_name)+
|
37
|
+
# will NOT be memoized with the given configuration.
|
38
|
+
#
|
39
|
+
# @param raw_columns [Array] A list of columns to memoize.
|
40
|
+
# @param editable [Boolean] Specify if the column is editable. Only editable columns
|
41
|
+
# will be used to create memos that are invalidatable after each record save.
|
42
|
+
def memoize_table_column(*raw_columns, editable: true)
|
43
|
+
RedisMemo::MemoizeQuery.__send__(:using_active_record!, self)
|
44
|
+
return if RedisMemo::DefaultOptions.disable_all
|
45
|
+
return if RedisMemo::DefaultOptions.model_disabled_for_caching?(self)
|
46
|
+
|
47
|
+
columns = raw_columns.map(&:to_sym).sort
|
48
|
+
|
49
|
+
RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: true) << columns if editable
|
50
|
+
RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: false) << columns
|
51
|
+
|
52
|
+
RedisMemo::MemoizeQuery::ModelCallback.install(self)
|
53
|
+
RedisMemo::MemoizeQuery::Invalidation.install(self)
|
54
|
+
|
55
|
+
unless RedisMemo::DefaultOptions.disable_cached_select
|
56
|
+
RedisMemo::MemoizeQuery::CachedSelect.install(ActiveRecord::Base.connection)
|
57
|
+
end
|
17
58
|
|
18
|
-
|
59
|
+
# The code below might fail due to missing DB/table errors
|
60
|
+
columns.each do |column|
|
61
|
+
next if columns_hash.include?(column.to_s)
|
19
62
|
|
20
|
-
RedisMemo::
|
21
|
-
|
63
|
+
raise RedisMemo::ArgumentError.new("'#{name}' does not contain column '#{column}'")
|
64
|
+
end
|
22
65
|
|
23
|
-
|
24
|
-
RedisMemo::MemoizeQuery::
|
66
|
+
unless RedisMemo::DefaultOptions.model_disabled_for_caching?(self)
|
67
|
+
RedisMemo::MemoizeQuery::CachedSelect.enabled_models[table_name] = self
|
68
|
+
end
|
69
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
|
70
|
+
# no-opts: models with memoize_table_column decleared might be loaded in
|
71
|
+
# rake tasks that are used to create databases
|
72
|
+
end
|
25
73
|
|
26
|
-
|
27
|
-
|
28
|
-
|
74
|
+
# Invalidates all memoized SQL queries on the given model.
|
75
|
+
#
|
76
|
+
# @param model_class [Class]
|
77
|
+
def self.invalidate_all(model_class)
|
78
|
+
RedisMemo::Tracer.trace(
|
79
|
+
'redis_memo.memoizable.invalidate_all',
|
80
|
+
model_class.name,
|
81
|
+
) do
|
82
|
+
RedisMemo::Memoizable.invalidate([model_class.redis_memo_class_memoizable])
|
83
|
+
end
|
84
|
+
end
|
29
85
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
86
|
+
# Invalidates all memoized SQL queries which would contain the given records.
|
87
|
+
#
|
88
|
+
# @param records [Array] ActiveRecord models to invalidate
|
89
|
+
def self.invalidate(*records)
|
90
|
+
RedisMemo::Memoizable.invalidate(
|
91
|
+
records.map { |record| to_memos(record) }.flatten,
|
92
|
+
)
|
93
|
+
end
|
39
94
|
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
|
44
|
-
# no-opts: models with memoize_table_column decleared might be loaded in
|
45
|
-
# rake tasks that are used to create databases
|
46
|
-
end
|
95
|
+
# Class variable containing all memoized columns on all ActiveRecord models
|
96
|
+
@@memoized_columns = Hash.new { |h, k| h[k] = [Set.new, Set.new] }
|
47
97
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
98
|
+
# Returns the list of columns currently memoized on the model or table
|
99
|
+
#
|
100
|
+
# @param model_or_table [Class] or [String] The ActiveRecord model class or table name
|
101
|
+
# @param editable [Boolean] Specifies whether to retrieve only editable columns
|
102
|
+
def self.memoized_columns(model_or_table, editable_only: false)
|
103
|
+
table = model_or_table.is_a?(Class) ? model_or_table.table_name : model_or_table
|
104
|
+
@@memoized_columns[table.to_sym][editable_only ? 1 : 0]
|
105
|
+
end
|
53
106
|
|
54
|
-
|
55
|
-
|
107
|
+
# Creates a +RedisMemo::Memoizable+ from the given ActiveRecord model class and column values.
|
108
|
+
#
|
109
|
+
# @param model_class [Class] The ActiveRecord model class
|
110
|
+
# @param extra_props [Hash] Props representing any column values on the model. +extra_props+
|
111
|
+
# are considered as AND conditions on the model class
|
112
|
+
def self.create_memo(model_class, **extra_props)
|
113
|
+
using_active_record!(model_class)
|
114
|
+
|
115
|
+
keys = extra_props.keys.sort
|
116
|
+
if !keys.empty? && !memoized_columns(model_class).include?(keys)
|
117
|
+
raise RedisMemo::ArgumentError.new("'#{model_class.name}' has not memoized columns: #{keys}")
|
56
118
|
end
|
57
119
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
120
|
+
extra_props.each do |key, value|
|
121
|
+
# The data type is ensured by the database, thus we don't need to cast
|
122
|
+
# types here for better performance
|
123
|
+
column_name = key.to_s
|
124
|
+
extra_props[key] =
|
125
|
+
if model_class.defined_enums.include?(column_name)
|
126
|
+
enum_mapping = model_class.defined_enums[column_name]
|
127
|
+
# Assume a value is a converted enum if it does not exist in the
|
128
|
+
# enum mapping
|
129
|
+
(enum_mapping[value.to_s] || value).to_s
|
130
|
+
else
|
131
|
+
value.to_s
|
132
|
+
end
|
63
133
|
end
|
64
134
|
|
65
|
-
|
66
|
-
|
67
|
-
|
135
|
+
RedisMemo::Memoizable.new(
|
136
|
+
__redis_memo_memoize_query_table_name__: model_class.table_name,
|
137
|
+
**extra_props,
|
138
|
+
)
|
139
|
+
end
|
68
140
|
|
69
|
-
|
70
|
-
|
71
|
-
raise(
|
72
|
-
RedisMemo::ArgumentError,
|
73
|
-
"'#{model_class.name}' has not memoized columns: #{keys}",
|
74
|
-
)
|
75
|
-
end
|
141
|
+
class << self
|
142
|
+
private
|
76
143
|
|
77
|
-
|
78
|
-
|
79
|
-
# types here for better performance
|
80
|
-
column_name = key.to_s
|
81
|
-
extra_props[key] =
|
82
|
-
if model_class.defined_enums.include?(column_name)
|
83
|
-
enum_mapping = model_class.defined_enums[column_name]
|
84
|
-
# Assume a value is a converted enum if it does not exist in the
|
85
|
-
# enum mapping
|
86
|
-
(enum_mapping[value.to_s] || value).to_s
|
87
|
-
else
|
88
|
-
value.to_s
|
89
|
-
end
|
90
|
-
end
|
144
|
+
def using_active_record!(model_class)
|
145
|
+
return if using_active_record?(model_class)
|
91
146
|
|
92
|
-
RedisMemo::
|
93
|
-
|
94
|
-
**extra_props,
|
147
|
+
raise RedisMemo::ArgumentError.new(
|
148
|
+
"'#{model_class.name}' does not use ActiveRecord",
|
95
149
|
)
|
96
150
|
end
|
97
151
|
|
98
|
-
def
|
99
|
-
|
100
|
-
'redis_memo.memoizable.invalidate_all',
|
101
|
-
model_class.name,
|
102
|
-
) do
|
103
|
-
RedisMemo::Memoizable.invalidate([model_class.redis_memo_class_memoizable])
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def self.invalidate(*records)
|
108
|
-
RedisMemo::Memoizable.invalidate(
|
109
|
-
records.map { |record| to_memos(record) }.flatten,
|
110
|
-
)
|
152
|
+
def using_active_record?(model_class)
|
153
|
+
model_class.respond_to?(:<) && model_class < ActiveRecord::Base
|
111
154
|
end
|
112
155
|
|
113
|
-
def
|
156
|
+
def to_memos(record)
|
114
157
|
# Invalidate memos with current values
|
115
158
|
memos_to_invalidate = memoized_columns(record.class).map do |columns|
|
116
159
|
props = {}
|
117
160
|
columns.each do |column|
|
118
|
-
props[column] = record.
|
161
|
+
props[column] = record.__send__(column)
|
119
162
|
end
|
120
163
|
|
121
164
|
create_memo(record.class, **props)
|
@@ -138,7 +181,7 @@ if defined?(ActiveRecord)
|
|
138
181
|
columns.each do |column|
|
139
182
|
next if props.include?(column)
|
140
183
|
|
141
|
-
props[column] = record.
|
184
|
+
props[column] = record.__send__(column)
|
142
185
|
end
|
143
186
|
|
144
187
|
memos_to_invalidate << create_memo(record.class, **props)
|