Skip to main content

← Back to the blog

Using Named Scopes Across Models with ActiveRecord#Merge

This post was written by Chris Oliver on May 15, 2014.

 


After years of working with ActiveRecord and watching it change so much, it is exciting to find new features you didn't know about. The one I discovered this week is ActiveRecord#merge. It is one of the most underused methods in ActiveRecord, due in part, to the name. It isn't necessarily clear what they mean by "merge" but it's simply a way of using a named scope on a joined model.

Say we have two models that are associated and one of them has a scope:

class Author < ActiveRecord::Base
  has_many :books
end
class Book < ActiveRecord::Base
  belongs_to :author

  scope :available, ->{ where(available: true) }
end

Let's say we want to join the tables to find all Authors who have books that are available. Without ActiveRecord#merge, we have to make this query:

Author.joins(:books).where("books.available = ?", true)
SELECT "authors".* FROM "authors" INNER JOIN "books" ON "books"."author_id" = "authors"."id" WHERE "books"."available" = 't'

But with ActiveRecord#merge, this becomes a whole lot cleaner and we don't duplicate the available scope:

Author.joins(:books).merge(Book.available)
SELECT "authors".* FROM "authors" INNER JOIN "books" ON "books"."author_id" = "authors"."id" WHERE "books"."available" = 't'

As you can see, the resulting SQL queries are exactly the same. ActiveRecord#merge is a great way to reduce the duplication in your code to continue relying on the named scopes you define in your models. I really want to see more people using this so please share this around!

Want to learn more about Rails and become a great programmer?

Check out the Ruby on Rails Screencasts for more awesome learning material like this.

P.S. You might enjoy following me on Twitter.


Discussion


Fallback

You have to fix your latest blog Post excerpt on the home page.
Great site btw.

Fallback

Doh! I'll get on that. My markdown parser sure didn't like that video.


Fallback

I am doing it with subquery
Like

Author.where(id: Book.available.select(:author_id))

I agree that your version is cleaner and easier to remember,
but you might want to add .distinct after joins to avoid duplicated authors.

Fallback

Ah yes, great points! I like that as a subquery and definitely need the distinct for my example as well.

Fallback

this query will generate a long SQL query. it's not best practice.


Fallback

Well, this is useful, but ugly... Thinking about querying, im not supposed to "merge" anything. IMHO

My wish was Author.books.scope(:available), lol

Fallback

Well you can do Author.books.available because that's just querying Book.where(author_id: X).available. But obviously this is for the more complex joining tables case, so at least you're working with the models like you normally would. Not the best, but it saves you from duplication.

Fallback

The difference is that Author.books.available will return books, but Author.joins(:books).merge(Book.available) will return authors. Using merge is useful when your desired record set has conditions that are dependant on another table. As stated in the video, you're looking for **Authors** with available books, not available **books**.

Fallback

Nice guys.


Fallback

Minor nitpick: but I really think you mean `ActiveRecord::Relation#merge`, not `ActiveRecord#merge`


Fallback

Fallback

Fallback

great! is ugly, but useful, thanks!


Fallback

Big thanks! Had a default scope on my Model but rails 4.2.5 didn't unscope it when I was doing "each do" so using the .joins(:model).merge(Model.scope) you suggested instead


Fallback

Could do something like:

scope :has_available_books, -> { joins(:books).merge(Book.available) }

Then you could just do:

Author.has_available_books


Fallback
Konstantin Ivanov

Need to note that merges on the same attribute overrides each other.

class Book < ActiveRecord::Base
...
scope :red, ->{ where(color: "red") }
scope :green, ->{ where(color: "green") }
end

Author.joins(:books).merge(Book.red).merge(Book.green)
^ would apply only green, because overwrite previous merge


Login or create an account to join the conversation.