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.
@@ -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
- method_name_without_memo = :"_redis_memo_#{method_name}_without_memo"
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 method_name_without_memo, method_name
43
+ alias_method method_name_without_memoization, method_name
14
44
 
15
45
  define_method method_name_with_memo do |*args|
16
- return send(method_name_without_memo, *args) if RedisMemo.without_memo?
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(self, *args, &depends_on)
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(self, method_name)
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
- method_name_without_memo,
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
- send(method_name_without_memo, *args)
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 |method_name, *method_args|
56
- method_depends_on = self.class.instance_variable_get(:@__redis_memo_method_dependencies)[method_name]
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
- RedisMemo::ArgumentError,
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
- RedisMemo::MemoizeMethod.get_or_extract_dependencies(self, *method_args, &method_depends_on)
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
- def self.method_id(ref, method_name)
68
- is_class_method = ref.class == Class
69
- class_name = is_class_method ? ref.name : ref.class.name
99
+ class << self
100
+ private
70
101
 
71
- "#{class_name}#{is_class_method ? '::' : '#'}#{method_name}"
72
- end
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
- # Resolve the dependency recursively
78
- dependency.instance_exec(ref, *method_args, &depends_on)
79
- dependency
80
- end
106
+ "#{class_name}#{is_class_method ? '::' : '#'}#{method_name}"
107
+ end
81
108
 
82
- def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
83
- if RedisMemo::Cache.local_dependency_cache
84
- RedisMemo::Cache.local_dependency_cache[ref.class] ||= {}
85
- RedisMemo::Cache.local_dependency_cache[ref.class][depends_on] ||= {}
86
- named_args = exclude_anonymous_args(depends_on, ref, method_args)
87
- RedisMemo::Cache.local_dependency_cache[ref.class][depends_on][named_args] ||= extract_dependencies(ref, *method_args, &depends_on)
88
- else
89
- extract_dependencies(ref, *method_args, &depends_on)
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
- # We only look at named method parameters in the dependency block in order to define its dependent
94
- # memos and ignore anonymous parameters, following the convention that nil or :_ is an anonymous parameter.
95
- # Example:
96
- # ```
97
- # def method(param1, param2)
98
- # end
99
- #
100
- # memoize_method :method do |_, _, param2|`
101
- # depends_on RedisMemo::Memoizable.new(param2: param2)
102
- # end
103
- # ```
104
- # `exclude_anonymous_args(depends_on, ref, [1, 2])` returns [2]
105
- def self.exclude_anonymous_args(depends_on, ref, args)
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
- num_positional_args_before_splat += 1
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
- # Determine the named positional and splat arguments after we know the # of pos. arguments before and after splat
142
- after_splat_index = depends_on_args.size - num_positional_args_after_splat
143
- depends_on_args.each_with_index do |arg, i|
144
- # if the index is within the splat
145
- if i >= num_positional_args_before_splat && i < after_splat_index
146
- positional_args << arg if named_splat
147
- else
148
- j = i < num_positional_args_before_splat ? i : i - (after_splat_index - splat_index) - 1
149
- positional_args << arg if is_named?(depends_on.parameters[j])
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
- if !kwargs.empty?
154
- positional_args + [kwargs]
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
- def self.method_cache_keys(future_contexts)
167
- memos = Array.new(future_contexts.size)
168
- future_contexts.each_with_index do |(_, _, dependent_memos), i|
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
- j = 0
173
- memo_checksums = RedisMemo::Memoizable.checksums(memos.compact)
174
- method_cache_key_versions = Array.new(future_contexts.size)
175
- future_contexts.each_with_index do |(method_id, method_args, _), i|
176
- if memos[i]
177
- method_cache_key_versions[i] = [method_id, memo_checksums[j]]
178
- j += 1
179
- else
180
- ordered_method_args = method_args.map do |arg|
181
- arg.is_a?(Hash) ? RedisMemo.deep_sort_hash(arg) : arg
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
- method_cache_key_versions[i] = [
185
- method_id,
186
- RedisMemo.checksum(ordered_method_args.to_json),
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
- method_cache_key_versions.map do |method_id, method_cache_key_version|
192
- # Example:
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
- if defined?(ActiveRecord)
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
- # Only editable columns will be used to create memos that are invalidatable
13
- # after each record save
14
- def memoize_table_column(*raw_columns, editable: true)
15
- RedisMemo::MemoizeQuery.using_active_record!(self)
16
- return if ENV["REDIS_MEMO_DISABLE_#{self.table_name.upcase}"] == 'true'
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
- columns = raw_columns.map(&:to_sym).sort
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::MemoizeQuery.memoized_columns(self, editable_only: true) << columns if editable
21
- RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: false) << columns
63
+ raise RedisMemo::ArgumentError.new("'#{name}' does not contain column '#{column}'")
64
+ end
22
65
 
23
- RedisMemo::MemoizeQuery::ModelCallback.install(self)
24
- RedisMemo::MemoizeQuery::Invalidation.install(self)
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
- if ENV['REDIS_MEMO_DISABLE_CACHED_SELECT'] != 'true'
27
- RedisMemo::MemoizeQuery::CachedSelect.install(ActiveRecord::Base.connection)
28
- end
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
- # The code below might fail due to missing DB/table errors
31
- columns.each do |column|
32
- unless self.columns_hash.include?(column.to_s)
33
- raise(
34
- RedisMemo::ArgumentError,
35
- "'#{self.name}' does not contain column '#{column}'",
36
- )
37
- end
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
- unless ENV["REDIS_MEMO_DISABLE_QUERY_#{self.table_name.upcase}"] == 'true'
41
- RedisMemo::MemoizeQuery::CachedSelect.enabled_models[self.table_name] = self
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
- def self.using_active_record!(model_class)
49
- unless using_active_record?(model_class)
50
- raise RedisMemo::ArgumentError, "'#{model_class.name}' does not use ActiveRecord"
51
- end
52
- end
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
- def self.using_active_record?(model_class)
55
- model_class.respond_to?(:<) && model_class < ActiveRecord::Base
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
- @@memoized_columns = Hash.new { |h, k| h[k] = [Set.new, Set.new] }
59
-
60
- def self.memoized_columns(model_or_table, editable_only: false)
61
- table = model_or_table.is_a?(Class) ? model_or_table.table_name : model_or_table
62
- @@memoized_columns[table.to_sym][editable_only ? 1 : 0]
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
- # extra_props are considered as AND conditions on the model class
66
- def self.create_memo(model_class, **extra_props)
67
- using_active_record!(model_class)
135
+ RedisMemo::Memoizable.new(
136
+ __redis_memo_memoize_query_table_name__: model_class.table_name,
137
+ **extra_props,
138
+ )
139
+ end
68
140
 
69
- keys = extra_props.keys.sort
70
- if !keys.empty? && !memoized_columns(model_class).include?(keys)
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
- extra_props.each do |key, value|
78
- # The data type is ensured by the database, thus we don't need to cast
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::Memoizable.new(
93
- __redis_memo_memoize_query_table_name__: model_class.table_name,
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 self.invalidate_all(model_class)
99
- RedisMemo::Tracer.trace(
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 self.to_memos(record)
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.send(column)
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.send(column)
184
+ props[column] = record.__send__(column)
142
185
  end
143
186
 
144
187
  memos_to_invalidate << create_memo(record.class, **props)