Rails 4 HABTM custom validation on associations
B

1

6

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!

Baguio answered 3/7, 2014 at 17:0 Comment(3)
I believe you want a has_many/belongs_to pairing instead of a has_and_belongs_to_many...Chemoprophylaxis
@meagar, updated my question to number of roles to be three. Disregard the model association and whether or not it's appropriate or not :) I need to solve this for a many-to-many relationship.Karole
Then, drop has_and_belongs_to_many and see my answer below.Chemoprophylaxis
C
9

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
Chemoprophylaxis answered 3/7, 2014 at 17:7 Comment(2)
Hi! Regarding the has_many-belongs_to association, I just updated my question to number of roles to three. Im aware of that my question was stupid for restricting roles to 1, since that would make no sense to have my kind of association.Karole
@JensBjörk yes. The joining model is where you would enforce this kind of validation. This is one of the many reasons has_and_belongs_to_many is frowned upon for all but the most simple of many-to-many associations. In practice, you shouldn't ever user it. I have literally not used it a single time since the days of Rails 2.3, and I doubt I will ever find a valid use for it again.Chemoprophylaxis

© 2022 - 2024 — McMap. All rights reserved.