Unterabfragen in Activerecord


81

Mit SQL kann ich einfach solche Unterabfragen durchführen

User.where(:id => Account.where(..).select(:user_id))

Dies erzeugt:

SELECT * FROM users WHERE id IN (SELECT user_id FROM accounts WHERE ..)

Wie kann ich dies mit Rails '3 activerecord / arel / meta_where tun?

Ich brauche / will echte Unterabfragen, keine Ruby-Problemumgehungen (mit mehreren Abfragen).

Antworten:


124

Rails macht das jetzt standardmäßig :)

Message.where(user_id: Profile.select("user_id").where(gender: 'm'))

erzeugt das folgende SQL

SELECT "messages".* FROM "messages" WHERE "messages"."user_id" IN (SELECT user_id FROM "profiles" WHERE "profiles"."gender" = 'm')

(Die Versionsnummer, auf die sich "jetzt" bezieht, ist höchstwahrscheinlich 3.2)


6
Wie mache ich dasselbe, wenn die Bedingung NICHT IN ist?
Coorasse

12
@coorasse: Wenn Sie Rails 4 verwenden, gibt es jetzt eine notBedingung . Ich konnte dies in Rails 3 erreichen, indem ich den Ansatz in diesem Beitrag anpasste : subquery = Profile.select("user_id").where(gender: 'm')).to_sql; Message.where('user_id NOT IN (#{subquery})) Grundsätzlich werden ActiveRecordMethoden verwendet, um die abgeschlossene, ordnungsgemäß zitierte Unterabfrage zu erstellen, die dann in die äußere Abfrage eingefügt wird. Der Hauptnachteil ist, dass Unterabfrageparameter nicht gebunden sind.
12.17

3
Um den Punkt von @ zwölf17 zu Rails 4 zu beenden, lautet die spezifische Nicht-Syntax Message.where.not(user_id: Profile.select("user_id").where(gender: 'm'))- das erzeugt eine Unterauswahl "NOT IN". Gerade mein Problem gelöst ..
Steve Midgley

1
@ChristopherLindblom Wenn Sie sagen, dass Rails "jetzt" standardmäßig funktioniert, was genau meinen Sie damit? Ab Schienen 3.2? Es wäre schön, wenn wir die Antwort so ändern könnten, dass "Rails dies standardmäßig ab Version X tut".
Jason Swett

@ JasonSwett Es tut mir leid, ich weiß nicht, es war wahrscheinlich 3.2, wie Sie sagen, da es die aktuelle Version der Zeit war und nur die veröffentlichten Versionen lief. Ich werde über zukünftige Zukunftssicherungen nachdenken. Vielen Dank, dass Sie darauf hingewiesen haben.
Christopher Lindblom

43

In ARel können die where()Methoden Arrays als Argumente verwenden, die eine Abfrage "WHERE id IN ..." generieren. Was Sie geschrieben haben, ist also in die richtige Richtung.

Zum Beispiel der folgende ARel-Code:

User.where(:id => Order.where(:user_id => 5)).to_sql

... was entspricht:

User.where(:id => [5, 1, 2, 3]).to_sql

... würde das folgende SQL in einer PostgreSQL-Datenbank ausgeben:

SELECT "users".* FROM "users" WHERE "users"."id" IN (5, 1, 2, 3)" 

Update: als Antwort auf Kommentare

Okay, also habe ich die Frage falsch verstanden. Ich glaube, Sie möchten, dass die Unterabfrage die Spaltennamen, die ausgewählt werden sollen, explizit auflistet, um die Datenbank nicht mit zwei Abfragen zu treffen (was ActiveRecord im einfachsten Fall tut).

Sie können projectfür die selectin Ihrer Unterauswahl verwenden:

accounts = Account.arel_table
User.where(:id => accounts.project(:user_id).where(accounts[:user_id].not_eq(6)))

... was das folgende SQL erzeugen würde:

SELECT "users".* FROM "users" WHERE "users"."id" IN (SELECT user_id FROM "accounts" WHERE "accounts"."user_id" != 6)

Ich hoffe aufrichtig, dass ich Ihnen diesmal gegeben habe, was Sie wollten!


Ja, aber genau das möchte ich nicht, da es zwei separate Abfragen generiert und keine einzige mit einer Unterabfrage.
Gucki

Entschuldigung für das Missverständnis Ihrer Frage. Können Sie ein Beispiel geben, wie Ihr SQL aussehen soll?
Scott

Kein Problem. Es ist bereits oben erwähnt: SELECT * FROM Benutzer WHERE ID IN (SELECT user_id FROM Konten WHERE ..)
gucki

1
Ah, okay. Ich verstehe, was du jetzt sagst. Ich verstehe, was Sie mit 2 generierten Abfragen meinen. Zum Glück weiß ich, wie ich Ihr Problem beheben kann! (siehe überarbeitete Antwort)
Scott

23

Ich habe selbst nach der Antwort auf diese Frage gesucht und mir einen alternativen Ansatz ausgedacht. Ich dachte nur, ich würde es teilen - hoffe, es hilft jemandem! :) :)

# 1. Build you subquery with AREL.
subquery = Account.where(...).select(:id)
# 2. Use the AREL object in your query by converting it into a SQL string
query = User.where("users.account_id IN (#{subquery.to_sql})")

Bingo! Bango!

Funktioniert mit Schienen 3.1


4
Die erste Abfrage wird zweimal ausgeführt. Es ist besser zu tun subquery = Account.where(...).select(:id).to_sql query = User.where("users.account_id IN (#{subquery})")
Coorasse

9
Die erste Abfrage in Ihrer REPL wird nur zweimal ausgeführt, da sie in der Abfrage to_s aufruft, um sie anzuzeigen. Es würde es nur einmal in Ihrer Anwendung ausführen.
Ritchie

Was ist, wenn wir mehrere Spalten aus Kontotabellen möchten?
Ahmad Hamza

0

Eine andere Alternative:

Message.where(user: User.joins(:profile).where(profile: { gender: 'm' })

0

Dies ist ein Beispiel für eine verschachtelte Unterabfrage mit Rails ActiveRecord und JOINs, in der Sie Klauseln zu jeder Abfrage sowie das Ergebnis hinzufügen können:

Sie können die verschachtelten Bereiche inner_query und Outer_query zu Ihrer Modelldatei hinzufügen und ...

  inner_query = Account.inner_query(params)
  result = User.outer_query(params).joins("(#{inner_query.to_sql}) alias ON users.id=accounts.id")
   .group("alias.grouping_var, alias.grouping_var2 ...")
   .order("...")

Ein Beispiel für den Umfang:

   scope :inner_query , -> (ids) {
    select("...")
    .joins("left join users on users.id = accounts.id")
    .where("users.account_id IN (?)", ids)
    .group("...")
   }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.