appartient à via des associations

141

Étant donné les associations suivantes, je dois faire référence au Questionqui Choiceest attaché par le Choicemodèle. J'ai essayé d'utiliser belongs_to :question, through: :answerpour effectuer cette action.

class User
  has_many :questions
  has_many :choices
end

class Question
  belongs_to :user
  has_many :answers
  has_one :choice, :through => :answer
end

class Answer
  belongs_to :question
end

class Choice
  belongs_to :user
  belongs_to :answer
  belongs_to :question, :through => :answer

  validates_uniqueness_of :answer_id, :scope => [ :question_id, :user_id ]
end

Je reçois

NameError Constante non initialisée User::Choice

quand j'essaye de faire current_user.choices

Cela fonctionne bien, si je n'inclus pas le

belongs_to :question, :through => :answer

Mais je veux l'utiliser parce que je veux pouvoir faire le validates_uniqueness_of

J'oublie probablement quelque chose de simple. Toute aide serait appréciée.

vinhboy
la source
1
Peut-être vaut-il la peine de changer la réponse acceptée par celle du délégué?
23inhouse le

Réponses:

60

Une belongs_toassociation ne peut pas avoir d' :throughoption. Vous feriez mieux de mettre en cache le question_idon Choiceet d'ajouter un index unique à la table (surtout parce qu'il validates_uniqueness_ofest sujet à des conditions de concurrence).

Si vous êtes paranoïaque, ajoutez une validation personnalisée Choicequi confirme que la réponse question_idcorrespond, mais il semble que l'utilisateur final ne devrait jamais avoir la possibilité de soumettre des données qui créeraient ce type d'incohérence.

stephencelis
la source
Merci Stephen, je ne voulais vraiment pas avoir à m'associer directement à question_id, mais je suppose que c'est le moyen le plus simple. Ma pensée initiale était, puisque "réponse" appartient à "question", je peux toujours passer par "réponse" pour arriver à la "question". Mais pensez-vous que ce n'est pas facile à faire, ou pensez-vous que ce n'est qu'un mauvais schéma?
vinhboy
Si vous souhaitez une contrainte / des validations uniques, les champs de portée doivent exister dans la même table. N'oubliez pas qu'il existe des conditions de course.
stephencelis
1
>> il semble que l'utilisateur final ne devrait jamais avoir la possibilité de soumettre des données qui créeraient ce type de non-concordance. - Vous ne pouvez jamais garantir que l'utilisateur "n'a pas la possibilité de faire quelque chose" à moins que vous n'effectuiez une vérification explicite côté serveur pour cela.
Konstantin
376

Vous pouvez également déléguer:

class Company < ActiveRecord::Base
  has_many :employees
  has_many :dogs, :through => :employees
end

class Employee < ActiveRescord::Base
  belongs_to :company
  has_many :dogs
end

class Dog < ActiveRecord::Base
  belongs_to :employee

  delegate :company, :to => :employee, :allow_nil => true
end
Renra
la source
27
+1, c'est la façon la plus propre de le faire. (au moins ce que je peux penser)
Orlando
9
Existe-t-il un moyen de le faire avec JOIN afin qu'il n'utilise pas autant de requêtes?
Tallboy
1
J'aimerais me connaître. Tout ce que j'ai essayé a tiré 3 sélections. Vous pouvez spécifier un lambda "-> {joins: quelque chose}" sur une association. La jointure est déclenchée mais ensuite une autre sélection est quand même. Je n'ai pas pu régler ça.
Renra
2
@Tallboy Quelques requêtes de sélection parfaitement indexées sur les clés primaires sont presque toujours meilleures que n'importe quelle requête JOIN unique. Les jointures font travailler la base de données.
Ryan McGeary
1
Que fait le allow_nil? Un employé ne devrait-il pas toujours avoir une entreprise?
codage aaron le
115

Utilisez simplement has_oneau lieu de belongs_todans votre :through, comme ceci:

class Choice
  belongs_to :user
  belongs_to :answer
  has_one :question, :through => :answer
end

Sans rapport, mais j'hésiterais à utiliser validates_uniqueness_of au lieu d'utiliser une contrainte unique appropriée dans votre base de données. Lorsque vous faites cela en rubis, vous avez des conditions de course.

mrm
la source
38
Gros avertissement avec cette solution. Chaque fois que vous enregistrez Choice, il enregistre toujours la question à moins qu'elle ne autosave: falsesoit définie.
Chris Nicola
@ChrisNicola pouvez-vous expliquer ce que vous vouliez dire, je n'ai pas compris ce que vous vouliez dire.
aks le
Qu'est-ce que je voulais dire où? Si vous voulez dire une contrainte unique appropriée, je veux dire ajouter un index UNIQUE à la colonne / champ qui doit être unique dans la base de données.
Chris Nicola
4

Mon approche était de créer un attribut virtuel au lieu d'ajouter des colonnes de base de données.

class Choice
  belongs_to :user
  belongs_to :answer

  # ------- Helpers -------
  def question
    answer.question
  end

  # extra sugar
  def question_id
    answer.question_id
  end
end

Cette approche est assez simple, mais s'accompagne de compromis. Il faut que Rails se charge à answerpartir de la base de données, puis question. Cela peut être optimisé plus tard en chargeant avec impatience les associations dont vous avez besoin (c'est-à-dire c = Choice.first(include: {answer: :question})), cependant, si cette optimisation est nécessaire, la réponse de stephencelis est probablement une meilleure décision de performance.

Il y a un moment et un lieu pour certains choix, et je pense que ce choix est meilleur lors du prototypage. Je ne l'utiliserais pas pour le code de production à moins de savoir que c'était pour un cas d'utilisation peu fréquent.

Eric Hu
la source
1

Cela ressemble à ce que vous voulez, c'est un utilisateur qui a de nombreuses questions.
La question a de nombreuses réponses, dont l'une est le choix de l'utilisateur.

C'est ce que vous recherchez?

Je modéliserais quelque chose comme ça dans le sens suivant:

class User
  has_many :questions
end

class Question
  belongs_to :user
  has_many   :answers
  has_one    :choice, :class_name => "Answer"

  validates_inclusion_of :choice, :in => lambda { answers }
end

class Answer
  belongs_to :question
end
Adam Tanner
la source
1

Vous ne pouvez donc pas avoir le comportement que vous souhaitez, mais vous pouvez faire quelque chose qui vous ressemble. Tu veux pouvoir faireChoice.first.question

ce que j'ai fait dans le passé est quelque chose comme ça

class Choice
  belongs_to :user
  belongs_to :answer
  validates_uniqueness_of :answer_id, :scope => [ :question_id, :user_id ]
  ...
  def question
    answer.question
  end
end

de cette façon, vous pouvez maintenant appeler la question sur Choice

MZaragoza
la source
-1

Le has_many :choicescrée une association nommée choices, non choice. Essayez d'utiliser à la current_user.choicesplace.

Consultez la documentation ActiveRecord :: Associations pour plus d'informations sur la has_manymagie.

Michael Melanson
la source
1
Merci pour votre aide Michael, cependant, c'est une faute de frappe de ma part. Je fais déjà current_user.choices. Cette erreur a quelque chose à voir avec le fait que je souhaite attribuer appartient à l'utilisateur et à la question.
vinhboy