redis-memo 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 -10
- 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 +59 -44
- data/lib/redis_memo/memoize_query/cached_select/connection_adapter.rb +7 -7
- data/lib/redis_memo/memoize_query/invalidation.rb +24 -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 +106 -5
- data/lib/redis_memo/railtie.rb +11 -0
- data/lib/redis_memo/redis.rb +15 -1
- 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 +19 -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)
|