vinhboy
vinhboy

Reputation: 8982

belongs_to through associations

Given the following associations, I need to reference the Question that a Choice is attached through from the Choice model. I have been attempting to use belongs_to :question, through: :answer to perform this 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

I am getting

NameError uninitialized constant User::Choice

when I try to do current_user.choices

It works fine, if I don't include the

belongs_to :question, :through => :answer

But I want to use that because I want to be able to do the validates_uniqueness_of

I am probably overlooking something simple. Any help would be appreciated.

Upvotes: 174

Views: 141709

Answers (7)

mrm
mrm

Reputation: 5212

Just use has_one instead of belongs_to in your :through, like this:

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

Unrelated, but I'd be hesitant to use validates_uniqueness_of instead of using a proper unique constraint in your database. When you do this in ruby you have race conditions.

Upvotes: 160

MZaragoza
MZaragoza

Reputation: 10111

So you cant have the behavior that you want but you can do something that feels like it. You want to be able to do Choice.first.question

what I have done in the past is something like this

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

this way the you can now call question on Choice

Upvotes: 3

Renra
Renra

Reputation: 5649

You can also delegate:

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

Upvotes: 433

Eric Hu
Eric Hu

Reputation: 18198

My approach was to make a virtual attribute instead of adding database columns.

class Choice
  belongs_to :user
  belongs_to :answer

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

  # extra sugar
  def question_id
    answer.question_id
  end
end

This approach is pretty simple, but comes with tradeoffs. It requires Rails to load answer from the db, and then question. This can be optimized later by eager loading the associations you need (i.e. c = Choice.first(include: {answer: :question})), however, if this optimization is necessary, then stephencelis' answer is probably a better performance decision.

There's a time and place for certain choices, and I think this choice is better when prototyping. I wouldn't use it for production code unless I knew it was for an infrequent use case.

Upvotes: 3

Adam Tanner
Adam Tanner

Reputation: 917

It sounds like what you want is a User who has many Questions.
The Question has many Answers, one of which is the User's Choice.

Is this what you are after?

I would model something like that along these lines:

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

Upvotes: 1

stephencelis
stephencelis

Reputation: 4964

A belongs_to association cannot have a :through option. You're better off caching the question_id on Choice and adding a unique index to the table (especially because validates_uniqueness_of is prone to race conditions).

If you're paranoid, add a custom validation to Choice that confirms that the answer's question_id matches, but it sounds like the end user should never be given the opportunity to submit data that would create this kind of mismatch.

Upvotes: 65

Michael Melanson
Michael Melanson

Reputation: 1335

The has_many :choices creates an association named choices, not choice. Try using current_user.choices instead.

See the ActiveRecord::Associations documentation for information about about the has_many magic.

Upvotes: 0

Related Questions