Enforcing special behavior of child rows in HABTM


#1

Hi,

I have a scenario where a doctor can have one or more specialties.
For each doctor, one and only one of her specialties can be designated
as primary.

So I have tables called doctors, specialties, and doctors_specialties,
the last of which has a boolean is_primary column.

The doctor model class specifies that:
has_and_belongs_to_many :specialties

I want to enforce, at the lowest possible level, the constraint that
a doctor can only have one primary specialty.

So, whenever a member of the association is added or updated with the
value is_primary set to true, code is run to first set all records in
the association to primary = false, thus ensuring that the row about to
be saved is the only one with is_primary = true.

I looked into passing a code block to the HABTM, but couldn’t quite get
that to work. Anyone have any suggestions?

This is for a demo to show how one would do this in Rails as opposed to
existing code that does it in .NET. So I’d like the solution to be very
elegant and Rails-esque. Can someone help?

Thanks


#2

Ray B. wrote:

validates_uniqueness_of :primary, :scope => :doctor_id

That won’t work. There are only two values of primary, true and false.
Under this logic there can only be two specialties. Go with the filter.

Ray


#3

Dan T. wrote:

I have a scenario where a doctor can have one or more specialties.
For each doctor, one and only one of her specialties can be designated
as primary.

So I have tables called doctors, specialties, and doctors_specialties,
the last of which has a boolean is_primary column.

The doctor model class specifies that:
has_and_belongs_to_many :specialties

I want to enforce, at the lowest possible level, the constraint that
a doctor can only have one primary specialty.

One way: rename doctors_specialties to something like specializations
and make it a full-fledged model. Then replace your habtm relationship
with a has_many :through relationship. The specialization model can
enforce the uniqueness with either a validation or a before filter.

class doctor < AR::B
has_many :specializations
has_many :specialties, :through => :specializations
end

class specialization < AR::B
belongs_to :doctors
belongs_to :specialties
validates_uniqueness_of :primary, :scope => :doctor_id
end

class specialty < AR::B
has_many :specializations
has_many :doctors, :through => :specializations
end

So, whenever a member of the association is added or updated with the
value is_primary set to true, code is run to first set all records in
the association to primary = false, thus ensuring that the row about to
be saved is the only one with is_primary = true.

The above doesn’t handle unsetting the other specialties. You would have
to add a before_save filter to do that.

Ray


#4

Ray B. wrote:

Ray B. wrote:

validates_uniqueness_of :primary, :scope => :doctor_id

That won’t work. There are only two values of primary, true and false.
Under this logic there can only be two specialties. Go with the filter.

Ray

This works great. Thanks.


#5

Running into a problem…original post (with code) as at bottom, for
context.

The problem is, I am not sure how to elegantly add to a doctor’s
“specialties” collection. I can construct a new Specialization and save
it, but that doesn’t seem like the Rails way. When I try this:

d = Doctor.find 1
=> #<Doctor:0xb77f07b0 @attributes={“name”=>“name”, “title”=>“title”,
“id”=>“1”}>

s = Specialty.find 1
=> #<Specialty:0xb77eaa68 @attributes={“name”=>“neuro”, “id”=>“1”}>

s.is_primary = true
=> true

d.specialties<<s
=> [#<Specialty:0xb77eaa68 @attributes={“name”=>“neuro”, “id”=>“1”},
@is_primary=true>]

Specialization.find_all
=> []

As you can see, nothing is written to Specializations.

Similarly:

d.specializations<<s
ActiveRecord::AssociationTypeMismatch: Specialization expected, got
Specialty

I thought perhaps my before_save method was the culprit, and got rid of
it temporarily, but still got the same behavior. Any thoughts?
Thanks.

Original post:
Ray B. wrote:

Dan T. wrote:

I have a scenario where a doctor can have one or more specialties.
For each doctor, one and only one of her specialties can be designated
as primary.

So I have tables called doctors, specialties, and doctors_specialties,
the last of which has a boolean is_primary column.

The doctor model class specifies that:
has_and_belongs_to_many :specialties

I want to enforce, at the lowest possible level, the constraint that
a doctor can only have one primary specialty.

One way: rename doctors_specialties to something like specializations
and make it a full-fledged model. Then replace your habtm relationship
with a has_many :through relationship. The specialization model can
enforce the uniqueness with either a validation or a before filter.

class Doctor < AR::B
has_many :specializations
has_many :specialties, :through => :specializations
end

class Specialization < AR::B
belongs_to :doctors
belongs_to :specialties

before_save filter which handles enforcement goes here

end

class Specialty < AR::B
has_many :specializations
has_many :doctors, :through => :specializations
attr_accessor :is_primary
end

So, whenever a member of the association is added or updated with the
value is_primary set to true, code is run to first set all records in
the association to primary = false, thus ensuring that the row about to
be saved is the only one with is_primary = true.


#6

Do you need to save the doctor for the connection to be made?

d.save


#7

Eden B. wrote:

Do you need to save the doctor for the connection to be made?

d.save

No, that didn’t make a difference…


#8

Dan,

Did you switch to a join model? If so, then this probably answers your
question:

http://blog.hasmanythrough.com/articles/read/150


#9

Eden B. wrote:

Dan,

Did you switch to a join model? If so, then this probably answers your
question:

http://blog.hasmanythrough.com/articles/read/150

Awesome…that answers the question quite thoroughly. I had no idea
there was a whole site devoted to has_many :through.