2010-05-24 7 views
19

Je tente d'imbriquer des requêtes SELECT dans Arel et/ou Active Record dans Rails 3 pour générer l'instruction SQL suivante.Requêtes imbriquées dans Arel

SELECT sorted.* FROM (SELECT * FROM points ORDER BY points.timestamp DESC) AS sorted GROUP BY sorted.client_id 

Un alias pour la sous-requête peut être créée en faisant

points = Table(:points) 
sorted = points.order('timestamp DESC').alias 

mais je suis coincé que la façon de le transmettre dans la requête parent (courte d'appeler #to_sql, qui sonne assez laid) . Comment utiliser une instruction SELECT en tant que sous-requête dans Arel (ou Active Record) pour accomplir ce qui précède?

Peut-être qu'il existe une manière tout à fait différente d'accomplir cette requête qui n'utilise pas de requêtes imbriquées?

Répondre

8

La question est pourquoi auriez-vous besoin d'une "requête imbriquée"? Nous n'avons pas besoin d'utiliser des "requêtes imbriquées" c'est penser dans l'état d'esprit de SQL non algèbre relationnelle. Avec l'algèbre relationnelle que nous tirons des relations et utiliser la sortie d'une relation en entrée à l'autre de sorte que le suivant serait vrai:

points = Table(:points, {:as => 'sorted'}) # rename in the options hash 
final_points = points.order('timestamp DESC').group(:client_id, :timestamp).project(:client_id, :timestamp) 

Il est préférable, si nous laissons le changement de nom à Arel, sauf nécessité absolue.

Ici, la projection de client_id ET timestamp est TRES importante car nous ne pouvons pas projeter tous les domaines de la relation (c'est-à-dire triés. *). Vous devez spécifiquement projeter tous les domaines qui seront utilisés dans l'opération de regroupement pour la relation. La raison en est qu'il n'y a pas de valeur pour * qui serait distinctement représentatif d'un id_client groupé. Par exemple que vous avez le tableau suivant

client_id | score 
---------------------- 
    4  | 27 
    3  | 35 
    2  | 22 
    4  | 69 

ici si vous regroupez vous ne pouvez effectuer une projection sur le domaine de points parce que la valeur pourrait être soit 27 ou 69 mais vous pouvez projeter une somme (score)

Vous pouvez uniquement projeter les attributs de domaine ayant des valeurs uniques dans le groupe (qui sont généralement des fonctions d'agrégation telles que sum, max, min). Avec votre requête, peu importe si les points ont été triés par date et heure, car à la fin, ils seront groupés par client_id. l'ordre d'horodatage n'est pas pertinent car il n'y a pas d'horodatage unique qui pourrait représenter un regroupement.

S'il vous plaît laissez-moi savoir comment je peux vous aider avec Arel. Aussi, j'ai travaillé sur une série d'apprentissage pour que les gens utilisent Arel à la base. Le premier de la série est à http://Innovative-Studios.com/#pilot Je peux vous dire que vous commencez à savoir comment vous avez utilisé Table (: points) plutôt que le point modèle ActiveRecord.

+0

Merci pour la réponse détaillée. "l'ordre d'horodatage n'est pas pertinent car il n'y a pas d'horodatage unique qui pourrait représenter un regroupement." Tu as raison; J'entends ce que vous dites. Il semble que MySQL contourne cette incohérence en renvoyant juste la première ligne du groupe client_id, ce que je visais. Je vois maintenant ce n'est pas un comportement sur lequel je devrais compter. Mon objectif est de renvoyer le point le plus récent pour tous les clients_ids, c'est-à-dire un seul point avec l'horodatage maximum pour chaque groupement client_id. Il est important de faire une requête car elle sera souvent interrogée. – Schrockwell

+0

Nous aurions besoin d'utiliser une fonction d'agrégat. Si nous nous demandons "Qu'est-ce que nous essayons de faire?" La réponse serait de trouver la date la plus récente ou "maximum" afin que nous passions max (timestamp) en sql. Cela correspondrait à Arel :: Attribute :: Expression :: Maximum qui peut être appelé avec du sucre syntaxique sur un attribut Arel :: tel que trié [: timestamp] .maximum(). Il y a une mise en garde. Assurez-vous d'ajouter l'horodatage à l'opération de groupe #group ('client_id, horodatage') ou l'ensemble du scénario de regroupement produira une erreur. Je sais que la fonction d'agrégat MAX fonctionne sur les dates dans Postgres et j'en suis sûr aussi dans MySQL. – Snuggs

+1

Premièrement, le tri et l'ordre ne font pas partie de l'algèbre relationnelle. Arel le définit quand même. Deuxièmement, que les sous-requêtes fassent ou non partie de l'algèbre relationnelle n'est pas pertinent. Conceptuellement, le résultat d'un SELECT n'est pas visible tant que la clause WHERE n'est pas exécutée. Par conséquent, toutes les bases de données (par exemple, Postgres) n'autorisent pas les alias de colonnes dans les clauses WHERE et dépendent plutôt des sous-requêtes. Si Arel ne peut pas gérer les sous-requêtes, les noms de la clause WHERE ne peuvent pas être aliasés. Cela peut devenir compliqué quand vous ne pouvez pas compter sur Arel pour générer des noms. –

7

Bien que je ne pense pas que ce problème nécessite des requêtes imbriquées, comme Snuggs mentionné. Pour ceux qui ont besoin de requêtes imbriquées. C'est ce que j'ai travaillé jusqu'à présent, pas génial mais ça marche:

class App < ActiveRecord::Base 
    has_many :downloads 

    def self.not_owned_by_users(user_ids) 
    where(arel_table[:id].not_in( 
     Arel::SqlLiteral.new(Download.from_users(user_ids).select(:app_id).to_sql))) 
    end 
end 

class Download < ActiveRecord::Base 
    belongs_to :app 
    belongs_to :user 

    def self.from_users(user_ids) 
    where(arel_table[:user_id].in user_ids) 
    end 

end 

class User < ActiveRecord::Base 
    has_many :downloads 
end 

App.not_owned_by_users([1,2,3]).to_sql #=> 
# SELECT `apps`.* FROM `apps` 
# WHERE (`apps`.`id` NOT IN (
# SELECT app_id FROM `downloads` WHERE (`downloads`.`user_id` IN (1, 2, 3)))) 
# 
+0

Solution la plus pure, astuce étant 'Arel :: SqlLiteral' il –

+0

Petite correction, au lieu d'utiliser' '' '' Arel :: SqlLiteral''', le correct serait '' '' '' 'Arel :: Nodes :: SqlLiteral''' – jonathanccalixto

23

Voici mon approche des tables temporaires et Arel. Il utilise Arel # de la méthode passant dans la requête interne avec Arel # to_sql.

inner_query = YourModel.where(:stuff => "foo") 
outer_query = YourModel.scoped # cheating, need an ActiveRelation 
outer_query = outer_query.from(Arel.sql("(#{inner_query.to_sql}) as results")). 
          select("*") 

Maintenant, vous pouvez faire quelques belles choses avec le outer_query, paginer, sélectionnez, groupe, etc ...

inner_query ->

select * from your_models where stuff='foo' 

outer_query ->

select * from (select * from your_models where stuff='foo') as results; 
+1

Vous pouvez Obtenez également une requête externe sans avoir à spécifier un faux nom de modèle ou de table.Les deux dernières lignes de ce qui précède peuvent être remplacées par cette ligne, qui est ce que "from" appelle de toute façon: outer_query = Arel :: SelectManager.new (Arel :: Table.engine, Arel.sql ("(# { inner_query.to_sql}) comme résultats ")) – DSimon

+1

Ceci est légendaire Mr.Todd – beck03076

6
Point. 
from(Point.order(Point.arel_table[:timestamp].desc).as("sorted")). 
select("sorted.*"). 
group("sorted.client_id") 
1

Pour ce faire, dans "pur" Arel, cela a fonctionné pour moi:

points = Arel::Table.new('points') 
sorted = Arel::Table.new('points', as: 'sorted') 
query = sorted.from(points.order('timestamp desc').project('*')).project(sorted[Arel.star]).group(sorted[:client_id]) 
query.to_sql 

Bien sûr , dans votre cas, les points et triés seraient récupérés et queue ored à partir du modèle de points par opposition à fabriqué comme ci-dessus.

Questions connexes