Ylan Segal

Using ActiveRecord::Base#merge

The API for ActiveRecord::Base is very large, which makes it easy to miss some of the great convinences it affords.

1
2
3
4
5
class Account < ActiveRecord::Base
end

Account.new.methods.count # => 558
Object.new.methods.count # => 123

ActiveRecord::Relation#merge merges the conditions of two relations, allowing the use of scopes on both sides. This allows not repeating where clauses in separate places in the code base. Example, extracted from the Rails documentation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Instead of:

Post.joins(:comments).where(published: true, comments: { spam: false })

# => SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "posts"."published" = ? AND "comments"."spam" = ?  [["published", "t"], ["spam", "f"]]

# Lets take advantage of scopes in Post and Comment:

class Comment < ActiveRecord::Base
  scope :non_spam, -> { where(spam: false) }
end

class Post < ActiveRecord::Base
  scope :published, -> { where(published: true) }
end

Post
  .joins(:comments)
  .published
  .merge(Comment.non_spam)

# => SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "posts"."published" = ? AND "comments"."spam" = ?  [["published", "t"], ["spam", "f"]]

This way, each scope stays with the model it belongs in, removing any danger of duplicating domain logic (like what is consider spam) to a single place in the code. This technique is especially useful when taking advantage of more complicated (possibly parametized) scopes.