Podzapytania w activerecord

81

Dzięki SQL mogę łatwo wykonywać takie pod-zapytania

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

To daje:

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

Jak mogę to zrobić za pomocą 3 activerecord / arel / meta_where railsów?

Potrzebuję / chcę prawdziwych podzapytań, żadnych obejść w Ruby (przy użyciu kilku zapytań).

gucki
źródło

Odpowiedzi:

126

Railsy robią to teraz domyślnie :)

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

wygeneruje następujący kod SQL

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

(numer wersji, do którego odnosi się „teraz”, to najprawdopodobniej 3.2)

Christopher Lindblom
źródło
6
Jak zrobić to samo, jeśli warunek NIE MA?
coorasse
13
@coorasse: Jeśli używasz Rails 4, istnieje teraz notwarunek . Udało mi się to osiągnąć w Railsach 3, dostosowując podejście w tym poście : subquery = Profile.select("user_id").where(gender: 'm')).to_sql; Message.where('user_id NOT IN (#{subquery})) Zasadniczo ActiveRecordmetody są używane do tworzenia kompletnego, odpowiednio cytowanego podzapytania, które jest następnie wstawiane do zewnętrznego zapytania. Głównym minusem jest to, że parametry podzapytania nie są związane.
dwanaście
3
Aby zakończyć uwagę @ twelve17 na temat Rails 4, specyficzna składnia nie jest Message.where.not(user_id: Profile.select("user_id").where(gender: 'm'))taka, że ​​generuje podselekcję "NOT IN". Właśnie rozwiązałem mój problem ...
Steve Midgley,
1
@ChristopherLindblom Kiedy mówisz, że Railsy „teraz” robią to domyślnie, co dokładnie masz na myśli? Od wersji Rails 3.2? Byłoby miło, gdybyśmy mogli zmienić odpowiedź na „Railsy robią to domyślnie od wersji X”.
Jason Swett
@JasonSwett Przykro mi, nie wiem, prawdopodobnie było to 3.2, jak mówisz, ponieważ była to aktualna wersja czasów i działała tylko w wydanych wersjach. Pomyślę o przyszłych odpowiedziach na przyszłość, dziękuję za wskazanie tego.
Christopher Lindblom,
43

W ARel where()metody mogą przyjmować tablice jako argumenty, które wygenerują zapytanie „WHERE id IN ...”. Więc to, co napisałeś, jest w porządku.

Na przykład następujący kod ARel:

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

... co jest równoważne z:

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

... wyświetli następujący kod SQL w bazie danych PostgreSQL:

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

Aktualizacja: w odpowiedzi na komentarze

Okej, więc źle zrozumiałem pytanie. Uważam, że chcesz, aby zapytanie podrzędne wyraźnie wymieniało nazwy kolumn, które mają być wybrane, aby nie trafić do bazy danych dwoma zapytaniami (co w najprostszym przypadku robi ActiveRecord).

Możesz użyć projectdla selectw swoim sub-selekcji:

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

... co dałoby następujący kod SQL:

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

Mam szczerą nadzieję, że tym razem dałem Ci to, czego chciałeś!

Scott
źródło
Tak, ale właśnie tego nie chcę, ponieważ generuje dwa oddzielne zapytania, a nie jedno zawierające jedno podzapytanie.
gucki
Przepraszamy za niezrozumienie pytania. Czy możesz podać przykład tego, jak ma wyglądać Twój SQL?
Scott
Nie ma problemu. Jest to już wspomniane powyżej: SELECT * FROM users WHERE id IN (SELECT user_id FROM accounts WHERE ..)
gucki
1
Ah, dobrze. Rozumiem, co teraz mówisz. Rozumiem, co masz na myśli, jeśli chodzi o generowanie 2 zapytań. Na szczęście wiem, jak rozwiązać Twój problem! (patrz poprawiona odpowiedź)
Scott,
23

Sam szukałem odpowiedzi na to pytanie i wpadłem na alternatywne podejście. Pomyślałem, że się tym podzielę - mam nadzieję, że to komuś pomoże! :)

# 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!

Działa z Railsami 3.1

Jan
źródło
4
wykonuje pierwsze zapytanie dwukrotnie. lepiej zrobić subquery = Account.where(...).select(:id).to_sql query = User.where("users.account_id IN (#{subquery})")
coorasse
9
Wykonałoby to pierwsze zapytanie tylko dwukrotnie w Twojej REPL, ponieważ wywołuje to_s w zapytaniu, aby je wyświetlić. Wykonywałoby to tylko raz w Twojej aplikacji.
Ritchie
Co jeśli chcemy mieć wiele kolumn z tabel kont?
Ahmad hamza
0

Inna alternatywa:

Message.where(user: User.joins(:profile).where(profile: { gender: 'm' })
lobati
źródło
0

To jest przykład zagnieżdżonego podzapytania używającego rails ActiveRecord i używających JOINs, gdzie możesz dodać klauzule do każdego zapytania, a także wynik:

Możesz dodać zagnieżdżone zakresy inner_query i external_query w pliku modelu i użyj ...

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

Przykład zakresu:

   scope :inner_query , -> (ids) {
    select("...")
    .joins("left join users on users.id = accounts.id")
    .where("users.account_id IN (?)", ids)
    .group("...")
   }
aabiro
źródło