Felipe Zavan
Felipe Zavan

Reputation: 1793

ActiveRecord: How to find parents whose ALL children match a condition?

Suppose I have a Parent model that has many Child, and that Child also belongs to OtherParent.

How can i find all Parent where ALL of its Child belongs to any OtherParent?

In pure SQL I could do

Parent.find_by_sql(<<SQL)
  SELECT *
  FROM parents p
  WHERE NOT EXISTS (
    SELECT *
    FROM children
    WHERE parent_id = p.id
      AND other_parent_id IS NULL
  )
SQL

(from here), but I'd prefer to do it by taking advantage of ActiveRecord if possible.

Thanks!


I'm using Rails 4.2.1 and PostgreSQL 9.3

Upvotes: 6

Views: 2786

Answers (3)

D-side
D-side

Reputation: 9485

Using arel can get you pretty far. The tricky part is how do you not write your entire query using arel's own query syntax?

Here's a trick: when building your query using where, if you use arel conditions, you get some extra methods for free. For instance, you can tail the subquery you have there with .exists.not, which will get you a (NOT ( EXISTS (subquery))) Toss that into parent's where-clause and you're set.

The question is, how do you reference the tables involved? You need Arel for that. You could use Arel's where with its ugly conditions like a.eq b. But why? Since it's an equality condition, you can use Rails' conditions instead! You can reference the table you're quering with a hash key, but for the other table (in the outer query) you can use its arel_table. Watch this:

parents = Parent.arel_table
Parent.where(
  Child.where(other_parent_id: nil, parent_id: parents[:id]).exists.not
)

You can even reduce Arel usage by resorting to strings a little and relying on the fact that you can feed in subqueries as parameters to Rails' where. There is not much use to it, but it doesn't force you to dig into Arel's methods too much, so you can use that trick or other SQL operators that take a subquery (are there even any others?):

parents = Parent.arel_table
Parent.where('NOT EXISTS (?)',
  Child.where(parent_id: parents[:id], other_parent_id: nil)
)

The two main points here are:

  • You can build subqueries just the same way you are used to building regular queries, referencing the outer query's table with Arel. It may not even be a real table, it may be an alias! Crazy stuff.
  • You can use subqueries as parameters for Rails' where method just fine.

Upvotes: 3

ABMagil
ABMagil

Reputation: 5595

Using the exceptional scuttle, you can translate arbitrary SQL into ruby (ActiveRecord and ARel queries)

From that tool, your query converts to

Parent.select(Arel.star).where(
  Child.select(Arel.star).where(
    Child.arel_table[:parent_id].eq(Parent.arel_table[:id]).and(Child.arel_table[:other_parent_id].eq(nil))
  ).ast
)

Splitting up the query-

  • Parent.select(Arel.star) will query for all columns in the Parent table.
  • Child.arel_table brings you into Arel-world, allowing you a little bit more power in generating your query from ruby. Specifically, Child.arel_table[:parent_id] gives you a handle onto an Arel::Attributes::Attribute that you can continue to use while building a query.
  • the .eq and .and methods do exactly what you would expect, letting you build a query of arbitrary depth and complexity.

Not necessarily "cleaner", but entirely within ruby, which is nice.

Upvotes: 2

joseramonc
joseramonc

Reputation: 1921

Given Parent and Child, and child is in a belongs_to relationship with OtherParent (Rails defaults assumed):

Parent.joins(:childs).where('other_parent_id = ?', other_parent_id)

Upvotes: -1

Related Questions