Підзапити в активному записі


81

За допомогою SQL я можу легко робити такі запити

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

Це дає:

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

Як я можу це зробити за допомогою рейок 3 activerecord / arel / meta_where?

Мені потрібні / потрібні реальні підзапити, без обхідних шляхів рубіну (за допомогою декількох запитів).

Відповіді:


126

Rails тепер робить це за замовчуванням :)

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

створить наступний SQL

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

(номер версії, на який посилається "зараз", швидше за все, 3.2)


6
Як зробити те ж саме, якщо умова НЕ ВХОДИТЬ?
coorasse

13
@coorasse: Якщо ви використовуєте Rails 4, зараз існує notумова . Я зміг досягти цього в Rails 3, відкоригувавши підхід у цій публікації : subquery = Profile.select("user_id").where(gender: 'm')).to_sql; Message.where('user_id NOT IN (#{subquery})) В основному ActiveRecordвикористовуються методи створення заповненого, правильно вказаного підзапиту, який потім вставляється у зовнішній запит. Основним недоліком є ​​те, що параметри підзапитів не пов’язані.
дванадцять17,

3
Щоб закінчити пункт @ twelve17 про Rails 4, конкретний не синтаксис Message.where.not(user_id: Profile.select("user_id").where(gender: 'm'))- це генерує підвибір "NOT IN". Щойно вирішив мою проблему ..
Стів Мідглі

1
@ChristopherLindblom Коли ви говорите Rails "зараз", це робиться за замовчуванням, що саме ви маєте на увазі? Станом на Rails 3.2? Було б непогано, якби ми могли змінити відповідь, сказавши: "Rails робить це за замовчуванням із версії X".
Джейсон Світт,

@JasonSwett Мені шкода, що я не знаю, це було, мабуть, 3,2, як ви кажете, оскільки це була поточна версія часу і запускала лише випущені версії. Подумаємо про майбутні випробувальні результати, дякую вам, що вказали на це.
Крістофер Ліндблом,

43

В ARel where()методи можуть приймати масиви як аргументи, які генерують запит "WHERE id IN ...". Отже, те, що ви написали, відповідає правильним правилам.

Наприклад, такий код ARel:

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

... що еквівалентно:

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

... виведе наступний SQL для бази даних PostgreSQL:

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

Оновлення: у відповідь на коментарі

Гаразд, я неправильно зрозумів питання. Я вважаю, що ви хочете, щоб підзапит явно перелічував імена стовпців, які слід вибрати, щоб не отримувати базу даних із двома запитами (що ActiveRecord робить у найпростішому випадку).

Ви можете використовувати projectдля selectу своєму підвиборі:

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

... який створить такий SQL:

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

Щиро сподіваюся, що цього разу я дав тобі те, що ти хотів!


Так, але це саме те, чого я не хочу, оскільки воно генерує два окремі запити, а не один із містять один підзапит.
gucki

Вибачення за нерозуміння вашого запитання. Не могли б ви навести приклад того, як ви хочете, щоб виглядав ваш SQL?
Скотт,

Нема проблем. Це вже згадувалося вище: SELECT * FROM users WHERE id IN (SELECT user_id FROM accounts WHERE ..)
gucki

1
Гаразд Я розумію, що ти зараз кажеш. Я розумію, що ви маєте на увазі, коли генеруються 2 запити. На щастя, я знаю, як вирішити вашу проблему! (див. переглянуту відповідь)
Скотт,

23

Я сам шукав відповіді на це питання і придумав альтернативний підхід. Я просто думав поділитися цим - сподіваюся, це допоможе комусь! :)

# 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})")

Бінго! Банго!

Працює з Rails 3.1


4
він виконує перший запит двічі. це краще зробити subquery = Account.where(...).select(:id).to_sql query = User.where("users.account_id IN (#{subquery})")
coorasse

10
Він лише два рази виконує перший запит у вашому REPL, оскільки його виклик to_s у запиті для його відображення. Він виконує його лише один раз у вашому додатку.
Річі

Що робити, якщо нам потрібні кілька стовпців з таблиць рахунків?
Ахмад хамза

0

Інша альтернатива:

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

0

Це приклад вкладеного підзапиту за допомогою рейок ActiveRecord та використання JOIN, де ви можете додати речення до кожного запиту, а також результат:

Ви можете додати вкладені сфери внутрішнього_запиту та зовнішній_запит у свій файл моделі та використовувати ...

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

Приклад області застосування:

   scope :inner_query , -> (ids) {
    select("...")
    .joins("left join users on users.id = accounts.id")
    .where("users.account_id IN (?)", ids)
    .group("...")
   }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.