Little-known but useful Rails features: ActiveRecord.extending
Every now and then I discover some features in Rails that are not that (arguably) commonly used, but there are some use cases when they turn out to be super useful and the best tool for the job. One of them would definitely be a nice addition to ActiveRecord::QueryMethods
- extending
method. Let’s see how it could be used in the Rails apps.
ActiveRecord::QueryMethods.extending - a great tool for managing common scopes
Imagine you are developing an API in your Rails application from where you will be fetching data periodically. To avoid getting all the records every time (which may end up with tons of unnecessary requests) and returning only the changed records since the last time they were fetched, you may want to implement some scope that will be returning records updated from given date that may look like this:
scope :updated_from, ->(datetime) { where("updated_at >= ?", datetime) }
To handle this logic in API, we could implement a generic method returning either all records or records updated from given date, depending on the presence of updated_from
param:
def fetch_records(model_class, params)
records = model_class.all
if updated_from = ActiveRecord::Type::DateTime.new.type_cast_from_user(params[:updated_from])
records = records.updated_from(updated_from)
end
records
end
If that was the case only for one or two models, we could just add updated_from
scope to them and that would be all. What if we needed it in plenty of other models as well?
One way to solve this problem would be defining updated_from
scope in ApplicationRecord
and letting the models inherit it from this base class:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
scope :updated_from, ->(datetime) { where("updated_at >= ?", datetime) }
end
The problem with this solution is that updated_from
scope would be available for all the models, even for the ones that won’t really need it. Another way would be extracting updated_from
to HasUpdatedFrom
models' concern:
module HasUpdatedFrom
extend ActiveSupport::Concern
included do
scope :updated_from, ->(datetime) { where("updated_at >= ?", datetime) }
end
end
and including it in all the models that will be using that scope, but it’s a bit cumbersome. Fortunately, there’a a perfect solution for such problem in Rails: ActiveRecord::QueryMethods.extending, which lets you extend a collection with additional methods. In this case, we could simply define updated_from
method in HasUpdatedFrom
module:
module HasUpdatedFrom
def updated_from(datetime)
where("updated_at >= ?", datetime)
end
end
and use ActiveRecord::QueryMethods.extending
in our fetch_records
method just like this:
def fetch_records(model_class, params)
records = model_class.all
if updated_from = ActiveRecord::Type::DateTime.new.type_cast_from_user(params[:updated_from])
records = records.extending(HasUpdatedFrom).updated_from(updated_from)
end
records
end
and that’s it! You won’t need to remember about including proper concern in every model used in such API or defining any scopes in ApplicationRecord
and inheriting them in models that won’t ever use them, just use ActiveRecord::QueryMethods.extending
and extend your collection with extra methods only when you need them.
Wrapping up
ActiveRecord::QueryMethods.extending
is not that commonly used Rails feature, but it’s definitely a useful one for managing common scopes in your models.