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).

Gucki
quelle

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)

Christopher Lindblom
quelle
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!

Scott
quelle
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

John
quelle
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' })
Lobati
quelle
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("...")
   }
aabiro
quelle