2010-03-04 3 views
49

Est-il possible d'avoir plusieurs relations has_many :through qui se croisent dans Rails? J'ai reçu la suggestion de le faire comme une solution pour une autre question que j'ai posté, mais j'ai été incapable de le faire fonctionner.Ruby-on-Rails: Multiple has_many: à travers possible?

Les amis sont une association cyclique via une table de jointure. L'objectif est de créer un has_many :through pour friends_comments, donc je peux prendre un User et faire quelque chose comme user.friends_comments pour obtenir tous les commentaires de ses amis dans une seule requête.

class User 
    has_many :friendships 
    has_many :friends, 
      :through => :friendships, 
      :conditions => "status = #{Friendship::FULL}" 
    has_many :comments 
    has_many :friends_comments, :through => :friends, :source => :comments 
end 

class Friendship < ActiveRecord::Base 
    belongs_to :user 
    belongs_to :friend, :class_name => "User", :foreign_key => "friend_id" 
end 

Cela semble très bien, et est logique, mais ne fonctionne pas pour moi. Ceci est l'erreur que je reçois dans la partie pertinente lorsque je tente d'accéder aux friends_comments d'un utilisateur:
ERROR: column users.user_id does not exist
: SELECT "comments".* FROM "comments" INNER JOIN "users" ON "comments".user_id = "users".id WHERE (("users".user_id = 1) AND ((status = 2)))

Quand je rentre juste user.friends, qui fonctionne, c'est la requête qu'il exécute:
: SELECT "users".* FROM "users" INNER JOIN "friendships" ON "users".id = "friendships".friend_id WHERE (("friendships".user_id = 1) AND ((status = 2)))

Il semble donc qu'il oublie complètement la relation d'amitié has_many d'origine, puis tente d'utiliser la classe User comme table de jointure de façon inappropriée.

que je fais quelque chose de mal, ou est-ce tout simplement pas possible?

Répondre

69

Edit:

Rails 3.1 prend en charge les associations imbriquées. Par exemple:

has_many :tasks 
has_many :assigments, :through => :tasks 
has_many :users, :through => :assignments 

Il n'y a pas besoin de la solution ci-dessous. Référez-vous à this screencast pour plus de détails.

Réponse originale

Vous passez une association has_many :through comme source pour une autre association has_many :through . Je ne pense pas que ça va marcher.

has_many :friends, 
      :through => :friendships, 
      :conditions => "status = #{Friendship::FULL}" 
    has_many :friends_comments, :through => :friends, :source => :comments 

Vous avez trois approches pour résoudre ce problème.

1) une extension d'association

has_many :friends, 
      :through => :friendships, 
      :conditions => "status = #{Friendship::FULL}" do 
    def comments(reload=false) 
     @comments = nil if reload 
     @comments ||=Comment.find_all_by_user_id(map(&:id)) 
    end 
end 

Maintenant, vous pouvez obtenir les commentaires des amis comme suit:

user.friends.comments 

2) Ajouter une méthode à la classe User.

def friends_comments(reload=false) 
    @friends_comments = nil if reload 
    @friends_comments ||=Comment.find_all_by_user_id(self.friend_ids) 
    end 

Maintenant, vous pouvez obtenir des commentaires les amis comme suit:

user.friends_comments 

3) Si vous voulez que ce soit alors encore plus efficace:

def friends_comments(reload=false) 
    @friends_comments = nil if reload 
    @friends_comments ||=Comment.all( 
      :joins => "JOIN (SELECT friend_id AS user_id 
           FROM friendships 
           WHERE user_id = #{self.id} 
         ) AS friends ON comments.user_id = friends.user_id") 
    end 

Maintenant vous pouvez obtenir les amis commentaires comme suit:

user.friends_comments 

Toutes les méthodes mettent en cache les résultats. Si vous voulez recharger les résultats procédez comme suit:

user.friends_comments(true) 
user.friends.comments(true) 

ou mieux encore:

user.friends_comments(:reload) 
user.friends.comments(:reload) 
+1

Malheureusement, l'objectif initial de cette démarche était pour que je puisse obtenir tous les commentaires des amis de SQL dans une requête. Voir ici: http://stackoverflow.com/questions/2382642/ruby-on-rails-how-to-pull-out-most-recent-entries-from-a-limited-subset-of-a-dat Si j'écris une fonction, cela entraînera un nombre N + 1 de requêtes. –

+1

Non ce ne sera pas. Ce sera 2 requêtes. Première requête pour obtenir les amis et deuxième requête pour obtenir les commentaires pour ** tous ** les amis. Si vous avez déjà chargé les amis sur le modèle utilisateur, vous n'encourez aucun coût. J'ai mis à jour la solution avec deux autres approches. Regarde. –

+0

@williamjones Vous n'avez pas de problème N + 1 dans les trois approches. Vous pouvez vérifier votre fichier journal pour vérifier cela. Personnellement, j'aime l'approche 1 car elle est assez élégante, c'est-à-dire 'user.friends.comments' est mieux que' user.friends_comments' –

8

Il y a un plugin qui permet de résoudre votre problème, jetez un oeil à this blog.

Vous installez le plugin avec

script/plugin install git://github.com/ianwhite/nested_has_many_through.git 
4

Bien que cela ne fonctionne pas dans le passé, il fonctionne très bien dans Rails 3.1 maintenant.

Questions connexes