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.
@@ -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)