Tadas Sasnauskas Tech/Engineering Blog

Extending ActiveRecord associations

Ever found yourself having difficulty to decide if method belongs to parent or child model?

Let’s say we have a Blogger and Article models. And profile section of our “blogging platform” displays list of articles published this current month.

Our option 1 is doing it in Blogger model:

class Blogger < ApplicationRecord
  has_many :articles

  def articles_this_month
    articles.where('created_at > ?', Time.current.beginning_of_month)
  end
end

And somewhere in our controller:

@articles = blogger.articles_this_month

Looks fine, but with domain models getting more complicated we may find our Blogger model polluted by articles_this, articles_that methods.

Option 2 is implement it in Article model using class method (or alternatively scope):

class Article < ApplicationRecord
  belongs_to :blogger

  def self.this_month
    where('created_at > ?', Time.current.beginning_of_month)
  end
end

And somewhere in our controller:

@articles = blogger.articles.this_month

Again, nothing wrong with that. Except maybe the fact that Article.this_month does not make too much sense on its own. In a way it pollutes Article model api with methods which don’t make sense without context of parent Blogger.

It’s almost as if our this_month method does not fit on both of our models and belongs to the relationship between them. Here we come to a somewhat little known ActiveRecord feature and our option 3: extending relationship between the models with custom methods.

has_many relationship documentation describes option :extend:

Specifies a module or array of modules that will be extended into the association object returned. Useful for defining methods on associations, especially when they should be shared between multiple association objects.

Our module then:

module BloggerArticles
  def this_month
    where('created_at > ?', Time.current.beginning_of_month)
  end
end

And the Blogger model:

class Blogger < ApplicationRecord
  has_many :articles, extend: BloggerArticles
end

Code somewhere in controller:

@articles = blogger.articles.this_month

Now neither Blogger nor Article has any out of place “this month” filter related methods. Instead, filter is now part of relationship between the models.

So, to summarise: models are not the only places to put your domain code. Relationship can also be a good option for that. And when used well - can be great way to build natural and well scoped domain APIs.