2009-06-20 8 views
6

Une partie de pourquoi j'aime Rails est que je déteste SQL - Je pense que c'est plus comme un langage d'assemblage qui devrait être manipulé avec des outils de niveau supérieur tels que ActiveRecord. Cependant, j'ai l'impression d'avoir atteint les limites de cette approche et je suis complètement dépassé par le SQL.Requêtes complexes sur les rails? sous-sélectionner? puis-je encore utiliser named_scope?

J'ai un modèle complexe avec beaucoup de sous-enregistrements. J'ai aussi un ensemble de 30-40 named_scopes qui implémentent la logique métier du client. Ces étendues sont enchaînées conditionnellement, c'est pourquoi j'ai ces étendues joins_ afin que les jointures ne soient pas tronquées.

J'ai quelques-uns d'entre eux qui ne fonctionnent pas correctement, ou du moins pas comment le client veut qu'ils travaillent. Voici une idée approximative de la structure du modèle, avec quelques portées nommées (pas toutes nécessaires pour l'exemple) qui illustrent mon approche et indiquent mes problèmes. (s'il vous plaît pardonner toutes les erreurs de syntaxe)

class Man < ActiveRecord::Base 
    has_many :wives 

    named_scope :has_wife_named  lambda { |n| { :conditions => { :wives => {:name => n}}}} 
    named_scope :has_young_wife_named lambda { |n| { :conditions => { :wives => {:name => n, :age => 0..30}}}} 
    named_scope :has_yw_named_v2  lambda { |n| { :conditions => ["wives.name = ? AND wives.age <= 30", n]}} 
    named_scope :joins_wives   :joins => :wives 

    named_scope :has_red_cat   :conditions => { :cats => {:color => 'red'}}   
    named_scope :has_cat_of_color  lambda { |c| { :conditions => { :cats => {:color => c}}}} 
    named_scope :has_7yo_cat   :conditions => { :cats => {:age => 7}} 
    named_scope :has_cat_of_age  lambda { |a| { :conditions => { :cats => {:age => a}}}} 
    named_scope :has_cat_older_than lambda { |a| { :conditions => ["cats.age > ?", a] }} 
    named_scope :has_cat_younger_than lambda { |a| { :conditions => ["cats.age < ?", a] }} 
    named_scope :has_cat_fatter_than lambda { |w| { :conditions => ["cats.weight > ?", w] } } 
    named_scope :joins_wives_cats  :joins => {:wives => :cats} 
end 

class Wife < ActiveRecord::Base 
    belongs_to :man 
    has_many :cats 
end 

class Cat < ActiveRecord::Base 
    belongs_to :wife 
end 
  1. Je peux trouver les hommes dont les femmes ont des chats qui sont rouges et sept ans

    @men = Man.has_red_cat.has_7yo_cat.joins_wives_cats.scoped({:select => 'DISTINCT men'}) 
    

    Et je peux même trouver des hommes dont les femmes ont les chats qui ont plus de 20 livres et plus de 6 ans

    @men = Man.has_cat_fatter_than(20).has_cat_older_than(5).joins_wives_cats.scoped({:select => 'DISTINCT men'}) 
    

    Mais ce n'est pas ce que je veux. Je veux trouver les hommes dont les femmes ont au moins un chat rouge et un chat âgé de sept ans, qui n'ont pas besoin d'être le même chat, ou de trouver les hommes dont les femmes ont au moins un chat au-dessus d'un poids donné et un chat plus âgé qu'un âge donné.
    (dans les exemples suivants, s'il vous plaît supposer la présence du joins_ et DISTINCT approprié)

  2. Je peux trouver des hommes avec les femmes nommées Esther

    @men = Man.has_wife_named('Esther') 
    

    Je peux même trouver des hommes avec les femmes nommées Esther, Ruth OR Ada (sweet!)

    @men = Man.has_wife_named(['Esther', 'Ruth', 'Ada']) 
    

    mais je veux trouver des hommes avec des femmes nommées Esther ET Ruth ET Ada.

  3. ha Ha, cela ne plaisante, en fait, je dois: Je peux trouver des hommes avec les femmes de moins de 30 nommé Esther

    @men = Man.has_young_wife_named('Esther') 
    

    trouver des hommes avec des jeunes femmes nommées Esther, Ruth ou Ada

    @men = Man.has_young_wife_named(['Esther', 'Ruth', 'Ada']) 
    

    mais comme ci-dessus je veux trouver des hommes avec de jeunes femmes nommées Esther AND Ruth AND Ada. Heureusement, le minimum est fixé dans ce cas, mais il serait bon de spécifier également un âge minimum.

  4. est-il un moyen de tester une inégalité avec une syntaxe de hachage, ou avez-vous toujours de revenir à :conditions => ["", n] - noter la différence entre has_young_wife_named et has_yw_named_v2 - J'aime le premier meilleur, mais la plage ne fonctionne que pour fini valeurs. Si vous cherchez une vieille femme, je suppose que vous pouvez utiliser a..100 mais quand une femme a 101 ans, elle abandonne la recherche. (hmm.peut-elle cuisiner? j/k)

  5. Y at-il un moyen d'utiliser une portée dans une portée? Je serais ravi si :has_red_cat pouvait utiliser :has_cat_of_color d'une façon ou d'une autre, ou s'il y avait un moyen d'utiliser la portée d'un enregistrement enfant dans son parent, donc je pourrais mettre les étendues liées au chat dans le modèle Wife.

Je ne veux vraiment pas faire cela dans SQL directement sans utiliser named_scope, à moins qu'il ya quelque chose d'autre en fait plus agréable - suggestions pour les plugins et autres joyeusetés grandement appréciés, ou la direction dans le genre de SQL que je vais devoir apprendre. Un ami a suggéré que les UNIONs ou les sous-recherches fonctionneraient ici, mais celles-ci ne semblent pas être beaucoup discutées dans le contexte de Rails. Je ne connais pas encore les opinions - seraient-elles utiles? Y a-t-il une façon heureuse de les fabriquer?

Merci!

Comme j'allais St Ives
J'ai rencontré un homme avec sept femmes
Chaque femme avait sept sacs
Chaque sac avait sept chats
Chaque chat a sept kits
Kits, chats, sacs, femmes
Combien allaient à St Ives?

+1

C'est une question vraiment intéressante dans l'abstrait. Et je comprends que vos noms de modèle et de portée sont dérivés d'une comptine. Mais c'est offensant, pour moi. Femme: appartient à l'homme? Sérieusement? –

+0

ouais, j'ai pensé à ça quand je l'écrivais, mais le poème était apparu dans ma tête et je ne pouvais pas m'en débarrasser. –

+0

Je voudrais trouver un bon moyen de faire 'UNION's dans les portées nommées en AR. Je sais que vous pouvez le faire dans SQLAlchemy. – mikelikespie

Répondre

2

Eh bien, j'ai eu d'excellents résultats avec named_scope s comme ceux-ci:

named_scope :has_cat_older_than lambda { |a| { :conditions => ["men.id in (select man_id from wives where wives.id in (select wife_id from cats where age > ?))", a] } } 

et

named_scope :has_young_wife_named lambda { |n| { :conditions => ["men.id in (select man_id from wives where name = ? and age < 30)", n] } } 

Je peux maintenant faire avec succès

Member.has_cat_older_than(6).has_young_wife_named('Miriam').has_young_wife_named('Vashti') 

et obtenir ce que je Je m'attends. Ces étendues ne nécessitent pas l'utilisation des jointures, et elles semblent bien jouer avec les autres jointures stylisées.

w00t!

Commentaires obtenus pour savoir s'il s'agit d'un moyen efficace de le faire, ou s'il y a plus de «rails-y». Une certaine façon d'inclure une portée d'un autre modèle sous la forme d'un fragment de sous-requête sql pourrait être utile ...

0

Vous avez utilisé la solution la plus native pour Rails. SQL droit aura les mêmes performances donc il n'y a aucune raison de l'utiliser.

+0

Je ne cherche pas plus de performance - je cherche la requête pour fonctionner correctement. J'aimerais savoir comment structurer correctement ma requête dans Rails, ou comment écrire SQL qui va le faire. –

2

J'ai utilisé construct_finder_sql pour accomplir la sous-sélection d'un named_scope dans un autre. Ce n'est peut-être pas pour tout le monde, mais l'utiliser nous permet de sécher deux named_scopes que nous avons utilisés pour les rapports.

Man.has_cat_older_than(6).send(:construct_finder_sql,{}) 

Essayez ceci dans votre script/console.

+0

Il m'a fallu 7 mois pour enfin comprendre cela, et c'est vraiment très pratique. Je pense que cela résoudrait mon problème d'origine, et cela m'a permis de dénouer un peu le même problème. Pro-tip: 'envoyer (: construct_finder_sql, {: select => 'men.id'}))' pour transformer un champ en une chaîne de sorte que vous excluez des enregistrements avec, par exemple: 'named_scope: excluding_sql, lambda {| sql | {: conditions => "men.id PAS DANS (# {sql})"}} ' comme ceci: ' Man.complex_scope_one.exclude_sql (Man.complex_scope_two.send (: construct_finder_sql, {: select => 'men .id '}))) ' cela va exclure complex_scope_two de complex_scope_one Merci! –

Questions connexes