enforce mutual exclusivity

Hi,

First post, please be gentle! I couldn't find any mention of this already

I'm using ActiveRecord and I want to enforce mutual exclusivity on a has_and_belongs_to_many.

A concrete example: I have a User which can have one or more Roles (student, tutor, headmaster, administrator etc). However if a user is a student they cannot hold any other role. I was hoping to find something like validate_exclusive but that doesn't appear to exist. Can anyone suggest how I can implement this. Otherwise, is there a better pattern I could use to model this?

Many thanks,

Toby.

Jamal,

Sorry, perhaps I wasn't clear. a user only has_one if that one is "student" otherwise it is valid that a user could have multiple roles.

I'm very much a newbie with rails, what do the options offer?

Thanks,

Toby.

Toby,

I think you have a couple of options.

If you want to continue with has_and_belongs_to_many, then there is a before_add callback available:

class User < AR::Base   has_and_belongs_to_many :roles, :before_add => :check_student_exclusive   def check_student_exclusive     # do your checks here for exclusiveness and raise if problem   end end

The trick with the before_add, after_add, before_remove and after_remove callbacks which you can use with has_many and has_and_belongs_to_many is that is is only called when adding an object to the collection with <<. It appears that someone has submitted a patch to trac to get it working in other cases though: http://dev.rubyonrails.org/ticket/8854

Another way with has_and_belongs_to_many would be to just add the roles with a custom method in the User model:

class User < AR::Base   has_and_belongs_to_many :roles

  def give_role( r ) # r would be a string perhaps     # do your checks here for exclusiveness and return false if problem

    # add the role to users roles if checks pass     roles << r     return true   end end

What you want leads me to think that using a join model instead (with has_many :through) is the better solution. You use a join model when you need more control over the relationship or you need to save information about the relationship which would be unique to it.

class User < AR::Base   has_many :assignments   has_many :roles, :through => :assignments end

class Role < AR::Base   has_many :assignments   has_many :users, :through => :assignments end

class Assignment < AR::Base   belongs_to :user   belongs_to :role   before_save :check_student_exclusive

  def check_student_exclusive      # do your checks here for exclusiveness and return false if problem      # else return true   end end

Keep in mind that Rails DB restrictions are not guaranteed in the case multiple applications access the database together.

If you can define these restrictions in the database level, e.g. as unique indexes, you're much safer.

Amir