I am trying to make my life simpler inside of a large production Rails 6.0 website. I have a bunch of data that I serve from Redis as denormalized hashes, because Rails, with all the includes and associations is very very slow.
To keep things DRY, I'd like to use a Concern (or module) that can be included within ApplicationRecord that allows me to dynamically define the collection methods for the data I want to store.
This is what I have so far:
class ApplicationRecord < ActiveRecord::Base
include DenormalizableCollection
# ...
end
# The model
class News < ApplicationRecord
denormalizable_collection :most_popular
# ...
end
# The Concern
module DenormalizableCollection
extend ActiveSupport::Concern
class_methods do
def denormalizable_collection(*actions)
actions.each do |action|
# define News.most_popular
define_singleton_method "#{action}" do
collection = Redis.current.get(send("#{action}_key"))
return [] unless collection.present?
JSON.parse(collection).map { |h| DenormalizedHash.new(h) }
end
# define News.set_most_popular
define_singleton_method "set_#{action}" do
Redis.current.set(send("#{action}_key"), send("#{action}_data").to_json)
end
# define News.most_popular_data, which is a method that returns an array of hashes
define_singleton_method "#{action}_data" do
raise NotImplementedError, "#{action}_data is required"
end
# define News.most_popular_key, the index key to use inside of redis
define_singleton_method "#{action}_key" do
"#{name.underscore}_#{action}".to_sym
end
end
end
end
end
This works, but I doesn't seems right because I cannot also define instance methods, or ActiveRecord after_commit callbacks to update the collection inside of Redis.
I'd like to add something like the following to it:
after_commit :set_#{action}
after_destroy :set_#{action}
But obviously these callbacks require an instance method, and after_commit :"self.class.set_most_popular" causes an error to be thrown. So I had wanted to add an instance method like the following:
class News
# ...
def reset_most_popular
self.class.send("set_most_popular")
end
end
I have been reading as many articles as I can and going through the Rails source to see what I'm missing - as I know I'm defo missing something!
define_methodwill define instance methods in the same manner thatdefine_singleton_methodwill define class methods.includedblock to define callbacks, anddefined_methodto define instance methods api.rubyonrails.org/classes/ActiveSupport/Concern.htmldefine_methodfrom theclass_methodsblock?class_methodsreally does something likeClassMethods = Module.new do ... endunder the covers. So when you calldefine_singleton_methodyou are defining a singleton method of the module and not the class that is extended by the module. Its like callingdef self.fooinside a module.define_singleton_methodbut its the only way i could get it to function in a basic way. I did try withclass_eval <<-CODE, __FILE__, __LINE__ + 1 ... CODEand normaldefine_methodand neither registered the methods when I checkedNews.methods.include?(:most_popular)