Subconsultas en activerecord

82

Con SQL puedo hacer fácilmente subconsultas como esta

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

Esto produce:

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

¿Cómo puedo hacer esto usando 3 activerecord / arel / meta_where de rails?

Necesito / quiero subconsultas reales, sin soluciones ruby ​​(usando varias consultas).

gucki
fuente

Respuestas:

125

Rails ahora hace esto de forma predeterminada :)

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

producirá el siguiente SQL

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

(el número de versión al que se refiere "ahora" es probablemente 3.2)

Christopher Lindblom
fuente
6
¿Cómo hacer lo mismo si la condición NO ESTÁ EN?
coorasse
13
@coorasse: Si estás usando Rails 4, ahora hay una notcondición . Pude lograrlo en Rails 3 ajustando el enfoque en esta publicación : subquery = Profile.select("user_id").where(gender: 'm')).to_sql; Message.where('user_id NOT IN (#{subquery})) Básicamente, los ActiveRecordmétodos se utilizan para crear la subconsulta completada y citada correctamente, que luego se incluye en la consulta externa. El principal inconveniente es que los parámetros de las subconsultas no están vinculados.
doce17
3
Solo para terminar el punto de @ doce17 sobre Rails 4, la sintaxis específica de not es Message.where.not(user_id: Profile.select("user_id").where(gender: 'm'))- que genera una subselección "NOT IN". Acabo de resolver mi problema ..
Steve Midgley
1
@ChristopherLindblom Cuando dices Rails "ahora" hace esto por defecto, ¿qué quieres decir exactamente? ¿A partir de Rails 3.2? Sería bueno si pudiéramos cambiar la respuesta para decir, "Rails hace esto por defecto a partir de la versión X".
Jason Swett
@JasonSwett Lo siento, no lo sé, probablemente fue 3.2 como dices, ya que era la versión actual de los tiempos y solo ejecutaba las versiones publicadas. Pensará en respuestas a prueba de futuro en el futuro, gracias por señalar esto.
Christopher Lindblom
43

En ARel, los where()métodos pueden tomar matrices como argumentos que generarán una consulta "WHERE id IN ...". Entonces, lo que ha escrito está en la línea correcta.

Por ejemplo, el siguiente código ARel:

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

... que es equivalente a:

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

... generaría el siguiente SQL en una base de datos PostgreSQL:

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

Actualización: en respuesta a comentarios

Bien, entendí mal la pregunta. Creo que desea que la subconsulta enumere explícitamente los nombres de columna que se seleccionarán para no llegar a la base de datos con dos consultas (que es lo que hace ActiveRecord en el caso más simple).

Puede utilizar projectpara selecten su sub-selección:

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

... que produciría el siguiente SQL:

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

¡Espero sinceramente haberte dado lo que querías esta vez!

Scott
fuente
Sí, pero esto es exactamente lo que no quiero porque genera dos consultas separadas y no una sola que contenga una subconsulta.
gucki
Disculpas por malinterpretar tu pregunta. ¿Podría dar un ejemplo de cómo desea que se vea su SQL?
Scott
No hay problema. Ya se mencionó anteriormente: SELECT * FROM users WHERE id IN (SELECT user_id FROM accounts WHERE ..)
gucki
1
Ah bien. Entiendo lo que estás diciendo ahora. Veo lo que quiere decir con la generación de 2 consultas. ¡Afortunadamente, sé cómo solucionar tu problema! (ver respuesta revisada)
Scott
23

Yo mismo estaba buscando la respuesta a esta pregunta y se me ocurrió un enfoque alternativo. Pensé en compartirlo, ¡espero que ayude a alguien! :)

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

Funciona con Rails 3.1

Juan
fuente
4
ejecuta la primera consulta dos veces. es mejor hacerlo subquery = Account.where(...).select(:id).to_sql query = User.where("users.account_id IN (#{subquery})")
coorasse
9
Solo ejecutaría la primera consulta dos veces en su REPL porque llama a to_s en la consulta para mostrarla. Solo lo ejecutaría una vez en su aplicación.
Ritchie
¿Qué pasa si queremos varias columnas de las tablas de cuentas?
Ahmad hamza
0

Otra alternativa:

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

Este es un ejemplo de una subconsulta anidada que usa rails ActiveRecord y usa JOINs, donde puede agregar cláusulas en cada consulta, así como el resultado:

Puede agregar los ámbitos anidado inner_query y outer_query en su archivo de modelo y usar ...

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

Un ejemplo del alcance:

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