web-dev-qa-db-fra.com

Sous-requêtes dans activerecord

Avec SQL, je peux facilement faire des sous-requêtes comme celle-ci

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

Cela produit:

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

Comment puis-je faire cela en utilisant Rails '3 activerecord/arel/meta_where? 

J'ai besoin/je veux de vraies sous-requêtes, pas de solutions de rechange Ruby (utilisation de plusieurs requêtes).

76
gucki

Rails le fait maintenant par défaut :)

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

produira le code SQL suivant

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

(le numéro de version auquel "maintenant" fait référence est probablement 3.2)

116

Dans ARel, les méthodes where() peuvent prendre des tableaux comme arguments qui généreront une requête "WHERE id IN ...". Donc, ce que vous avez écrit va dans la bonne direction.

Par exemple, le code ARel suivant:

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

... qui équivaut à:

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

... générerait le code SQL suivant sur une base de données PostgreSQL:

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

Mise à jour: en réponse aux commentaires

Ok, alors j'ai mal compris la question. Je pense que vous souhaitez que la sous-requête répertorie explicitement les noms de colonne à sélectionner afin de ne pas frapper la base de données avec deux requêtes (c'est ce qu'ActiveRecord fait dans le cas le plus simple).

Vous pouvez utiliser project pour la select dans votre sous-sélection:

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

... qui produirait le code SQL suivant:

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

J'espère sincèrement que je vous ai donné ce que vous vouliez cette fois!

40
Scott

Je cherchais moi-même la réponse à cette question et j'ai proposé une approche alternative. Je pensais juste que je le partagerais - espérons que cela aidera quelqu'un! :)

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

Fonctionne avec Rails 3.1

23
John

Une autre alternative:

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

Voici un exemple de sous-requête imbriquée utilisant Rails ActiveRecord et JOIN, dans laquelle vous pouvez ajouter des clauses sur chaque requête ainsi que le résultat:

Vous pouvez ajouter les étendues imbriquées inner_query et outer_query dans votre fichier de modèle et utiliser ...

  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 exemple de la portée:

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