Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 4 HABTM custom validation on associations

I've got a simple scenario, but I can't seem to find any proposed solutions that apply to Rails 4. I want to simply add a custom validator that checks the amount of stored associations between my HABTM association. Easier said and done, to my surprise?

I've searched for a solution but only end up with answers for older versions of Rails it seems. I've got the following:

class User < ActiveRecord::Base

  has_and_belongs_to_many :roles
  after_save :check_maximum_number_of_roles

  .
  .
  .

  private

  def check_maximum_number_of_roles
    if self.roles.length > 3
      errors.add(:roles, 'Users can only have three roles assigned.')
      return false
    end
  end

end

class Role < ActiveRecord::Base

  has_and_belongs_to_many :users

end

The reason I use after_save is because as far as I understand the stored association is first available after it has been added. I've also tried to write an custom validator (e.g. validate: :can_only_have_one_role), but that does not work either.

I add the association in the following manner and have done this in the rails console (which should work just fine?):

user.roles << role

Nevertheless, it adds more than one role to users and does not care of any type of validation.

Help much appreciated, thanks!

like image 443
Jens Björk Avatar asked Jul 03 '14 17:07

Jens Björk


1 Answers

user.roles << role performs no validation on user. The user is largely uninvolved. All this does is insert a new record into your joining table.

If you want to enforce that a user has only one role, you have two options, both involve throwing away has_and_belongs_to_many, which you really shouldn't use anymore. Rails provides has_many :through, and that has been the preferred way of doing many-to-many relationship for some time.

So, the first (and I think best) way would be to use has_many/belongs_to. That is how you model one-to-many relationships in Rails. It should be this simple:

class Role
  has_many :users
end

class User
  belongs_to :role
end

The second way, which is over complex for enforcing a single associated record, is to create your joining model, call it UserRole, use a has_many :through, and perform the validation inside UserRole.

class User
  has_many :user_roles
  has_many :roles, through: :user_roles
end

class UserRole
  belongs_to :user
  belongs_to :role

  # Validate that only one role exists for each user
  validates :user_id, uniqueness: { scope: :role_id }

  # OR, to validate at most X roles are assigned to a user
  validate :at_most_3_roles, on: :create

  def at_most_3_roles
    duplicates = UserRole.where(user_id: user_id, role_id: role_id).where('id != ?', id)
    if duplicates.count > 3
      errors.add(:base, 'A user may have at most three roles')
    end
  end
end

class Role
  has_many :user_roles
  has_many :users, through: :user_roles
end
like image 61
meagar Avatar answered Nov 15 '22 09:11

meagar