Using ActiveRecord's merge to access scopes in different models

One of the things that I love about ActiveRecord is that it allows me to build sql by using composition. By chaining together scopes, I have reusable snippets of sql code that can be combined to build a complex query. Anyone that has used ActiveRecord has seen this in action:

class Child < ActiveRecord::Base
  def self.active
    where(active: true)
  end

  def self.recent
    where("created_at > ? ", Date.today - 5.days)
  end
end

Child.active.recent

This generates a sql statement by composing the chained scopes.

SELECT "children".* FROM "children" WHERE "children"."active" = 't' AND (created_at > '2016-05-06')"

Ok, that's great, but what if you want to limit the children by some attribute of its parent? For example, what if instead of active children, we only want recent Child records where the parent is active?

First, we need to add the relationship to Child.

class Child < ActiveRecord::Base

  belongs_to :parent

  def self.active
    where(active: true)
  end

  def self.recent
    where("created_at > ? ", Date.today - 5.days)
  end
end

Next, we create the parent class.

class Parent < ActiveRecord::Base

  has_many :children

  def self.active
    where(active: true)
  end
end

If you've run into this before, you probably joined to (or included) the parent in the query and added a where clause for the active=true condition like this ...

Child.recent.
  joins(:parent).
  where(parents: { active: true })

This works and generates the sql

SELECT "children".* FROM "children" WHERE "children"."active" = 't' AND (created_at > '2016-05-06')" AND
"parents"."active" = 't'

But, besides being a little verbose, it doesn't reuse the existing active scope on the Parent class. What happens if the definition of the parent's active scope changes? How would we know that this code needs to be changed as well?

ActiveRecord#merge to the rescue. Using merge, you can just merge in the scope from the other class.

Child.recent.
  joins(:parent).
  merge(Parent.active)

This generates the exact same sql as above, but provides a number of advantages to the readability and maintainability of our code. By reusing the existing scope on the Parent class, we have DRYed up our code and made it more intention-revealing. Also, we have now abstracted the implementation detail of the parents table's schema.

One important thing to remember when using merge is that you need to either join or include the parent table explicitly. Without it, you'll get an error like no such column: parents.active.

ActiveRecord is a huge framework and there are a ton of great methods in it. I hope you find this one useful. I'm going to try to introduce more in future posts. Until next time ...

Posted by Craig Israel on May 12, 2016

comments powered by Disqus