0

I have 3 models in my rails application, User, Course, and CourseTemplate.

A Course belongs to a User and a CourseTemplate belongs to a Course.

What I want to do is to validate the uniqueness between the CourseTemplate name and the User id.

Is this possible?

InesM
  • 331
  • 4
  • 16

1 Answers1

1

Without denormalization of data

class CourseTemplate < ActiveRecord::Base
  belongs_to :course
  has_one :user, through: :course
  validate :unique_course_template_for_user

  private

  def unique_course_template_for_user
    errors.add(:name, 'Must be unique') if CourseTemplate.find_by(user: user.id, name: self.name).count > 0
  end
end

With denormalization of data

If you're ok with some denormalization of your data, you could add user_id to CourseTemplate, and then simply use the scope feature of validates uniqueness.

Below I show how to use callbacks to maintain the user_id in the CourseTemplate. Note that it assumes a course cannot be moved to a different user.

class CourseTemplate < ActiveRecord::Base
  before_create :copy_user_id_from_course

  validates :name, uniqueness: { scope: :user_id, message: 'Must be unique for the same user'}

  private

  def copy_user_id_from_course
    self.user_id = course.user_id
    true
  end
end

If the course can be moved to a different user, you should add a callback on Course:

class Course < ActiveRecord::Base
  after_save :set_course_templates_user, if: :user_id_changed?

  private

  def set_course_templates_user
    course_templates.update_all user_id: self.user_id
  end

end
AmitA
  • 3,239
  • 1
  • 22
  • 31
  • The problem is that my CourseTemplate doesn't have a user so I get an error: undefined local variable or method `user' for # – InesM Apr 30 '16 at 00:34
  • See my updates above of how to add :user to the CourseTemplate. Specifically, `has_one :user, through: :course` – AmitA Apr 30 '16 at 00:37
  • Thanks that helped a bit I think. However I'm creating the CourseTemplate as a nested attribute of Course and is saying that my user is nil? Any ideia why? – InesM Apr 30 '16 at 00:42
  • I assume you do something like `Course.new(course_params)` where `course_params` contains a key called `course_templates_attributes`. If the `course_params` does not contain on the top level the `user_id` key, then you will get that error. If, however, you do have `user_id` in your hash, then maybe it has something to do with the order of saves.. maybe ActiveRecord saves the children before the parent (though I don't think this is the case) – AmitA Apr 30 '16 at 00:49
  • BTW, you can also simply avoid the issue by either doing `return unless user` in the first line, or doing `user.try(:id)`. This means that it won't fail in situations when a user was not assigned yet to a course. However, ask yourself whether this should ever be allowed. Also, the two methods slightly differ (the latter will actually claim to be invalid if two course templates with the same name have unassigned user). – AmitA Apr 30 '16 at 00:53
  • Actually I'm doing `Course.new(course_params)` has you say but the user doesn't go on `course_params`. I'm adding the user like this `@course.user = current_user` and after this I'm saving `@course.save` – InesM Apr 30 '16 at 00:53
  • And with denormalization of data I'm also getting an error saying `column course_templates.user_id does not exist` – InesM Apr 30 '16 at 00:57
  • Try merging it to course_params instead. Like `Course.new course_params.merge(user_id: current_user.id)` – AmitA Apr 30 '16 at 00:57
  • Re your denormalization method question - you have to add user_id to course_templates in a migration to have that method work. – AmitA Apr 30 '16 at 00:57
  • Isn't that a bit inefficient? Because I already have the user id on course. I would be repeating data. – InesM Apr 30 '16 at 00:59
  • That's the definition of denormalization :) Whether it is inefficient depends on what you are optimizing for. If you're optimizing for query time - it is actually more efficient, as you don't need to do a join every time to find the user_id. It is very similar to Rails's counter caching, only here you're caching a "parent_id" (user_id) instead of "child_records_count". The downside is typically the issue of maintaining data integrity, but ActiveRecord callbacks solve this problem nicely (if done right). – AmitA Apr 30 '16 at 01:03