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