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

萝らか妹 提交于 2019-12-10 13:52:21

问题


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


回答1:


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.



回答2:


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.




回答3:


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)



来源:https://stackoverflow.com/questions/30535263/activerecord-how-to-find-parents-whose-all-children-match-a-condition

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!