Karol Galanciak - Ruby on Rails and Ember.js consultant

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:

1
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:

1
2
3
4
5
6
7
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:

1
2
3
4
5
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:

1
2
3
4
5
6
7
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:

1
2
3
4
5
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:

1
2
3
4
5
6
7
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.

Comments