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

81

С SQL я легко могу выполнять такие подзапросы

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

Это производит:

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

Как я могу это сделать с помощью rails '3 activerecord / arel / meta_where?

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

Gucki
источник

Ответы:

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условие now . Мне удалось добиться этого в Rails 3, изменив подход в этом посте : в subquery = Profile.select("user_id").where(gender: 'm')).to_sql; Message.where('user_id NOT IN (#{subquery})) основном, ActiveRecordметоды используются для создания завершенного, правильно процитированного подзапроса, который затем встроен во внешний запрос. Основным недостатком является то, что параметры подзапроса не связаны.
twelve17
3
Чтобы завершить замечание @ twelve17 о Rails 4, конкретный синтаксис not Message.where.not(user_id: Profile.select("user_id").where(gender: 'm'))- генерирует подвыбор "NOT IN". Только что решил мою проблему ..
Стив Мидгли
1
@ChristopherLindblom Когда вы говорите Rails «сейчас», это происходит по умолчанию, что именно вы имеете в виду? Что касается Rails 3.2? Было бы неплохо, если бы мы могли изменить ответ так, чтобы он говорил: «Rails делает это по умолчанию, начиная с версии X».
Джейсон Светт 02
@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
Ах хорошо. Я понимаю, о чем вы сейчас говорите. Я понимаю, что вы имеете в виду, говоря о создании двух запросов. К счастью, я знаю, как решить вашу проблему! (см. измененный ответ)
Скотт
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

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

Вы можете добавить вложенные области inner_query и outer_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("...")
   }
Аабиро
источник