Подзапросы в ActiveRecord

С 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?

Мне нужны/хочу настоящие подзапросы, никаких рубиновых обходных путей (используя несколько запросов).


person gucki    schedule 30.03.2011    source источник


Ответы (5)


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)

person Christopher Lindblom    schedule 31.05.2012
comment
Как сделать то же самое, если условие НЕ В? - person coorasse; 23.02.2013
comment
@coorasse: если вы используете Rails 4, теперь есть not условие. Я смог добиться этого в Rails 3, изменив подход в этом посте: subquery = Profile.select("user_id").where(gender: 'm')).to_sql; Message.where('user_id NOT IN (#{subquery})) В основном, ActiveRecord методы используются для создания завершенного, правильно цитируемого подзапроса, который затем встраивается во внешний запрос. Основным недостатком является то, что параметры подзапроса не привязаны. - person twelve17; 24.07.2013
comment
Просто чтобы закончить замечание @twelve17 о Rails 4, особый синтаксис not - Message.where.not(user_id: Profile.select("user_id").where(gender: 'm')), который генерирует подзапрос NOT IN. Только что решил свою проблему.. - person Steve Midgley; 01.10.2014
comment
@ChristopherLindblom Когда вы говорите, что Rails теперь делает это по умолчанию, что именно вы имеете в виду? Начиная с Rails 3.2? Было бы неплохо, если бы мы могли изменить ответ, сказав, что Rails делает это по умолчанию, начиная с версии X. - person Jason Swett; 03.08.2017
comment
@JasonSwett Извините, я не знаю, вероятно, это была версия 3.2, как вы говорите, поскольку это была текущая версия того времени, и она запускала только выпущенные версии. Буду думать о будущих ответах на проверку в будущем, спасибо, что указали на это. - person Christopher Lindblom; 14.06.2018
comment
есть ли способ поместить подзапрос как value в запрос на обновление. Как 2_? Я задал вопрос здесь - person CamiloVA; 12.05.2020

В ARel методы 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)

Я искренне надеюсь, что на этот раз я дал вам то, что вы хотели!

person Scott    schedule 18.04.2011
comment
Да, но это именно то, чего я не хочу, потому что он генерирует два отдельных запроса, а не один, содержащий один подзапрос. - person gucki; 19.04.2011
comment
Приносим извинения за непонимание вашего вопроса. Не могли бы вы привести пример того, как должен выглядеть ваш SQL? - person Scott; 19.04.2011
comment
Без проблем. Выше уже упоминалось: SELECT * FROM users WHERE id IN (SELECT user_id FROM account WHERE..) - person gucki; 19.04.2011
comment
Ах хорошо. Я понимаю, что вы сейчас говорите. Я понимаю, что вы имеете в виду о двух генерируемых запросах. К счастью, я знаю, как решить вашу проблему! (см. пересмотренный ответ) - person Scott; 19.04.2011

Я сам искал ответ на этот вопрос и придумал альтернативный подход. Я просто подумал, что поделюсь этим - надеюсь, это кому-то поможет! :)

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

Бинго! Банго!

Работает с рельсами 3.1

person John    schedule 03.01.2012
comment
он выполняет первый запрос дважды. лучше сделать subquery = Account.where(...).select(:id).to_sql query = User.where("users.account_id IN (#{subquery})") - person coorasse; 23.02.2013
comment
Он будет выполнять первый запрос только дважды в вашем REPL, потому что он вызывает to_s для запроса, чтобы отобразить его. Он будет выполнять его только один раз в вашем приложении. - person Ritchie; 14.01.2015
comment
Что, если нам нужно несколько столбцов из таблиц учетных записей? - person Ahmad hamza; 01.06.2018

Другая альтернатива:

Message.where(user: User.joins(:profile).where(profile: { gender: 'm' })
person lobati    schedule 26.09.2018

Это пример вложенного подзапроса с использованием rails ActiveRecord и JOIN, где вы можете добавлять предложения в каждый запрос, а также в результат:

Вы можете добавить вложенные области inner_query и external_query в свой файл модели и использовать...

  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("...")
   }
person aabiro    schedule 15.02.2019