class Course < ActiveRecord::Base
has_many :registrations
has_many :users, :through => :registrations
end
class User < ActiveRecord::Base
has_many :registrations
has_many :courses, :through => :registrations
end
class Registration < ActiveRecord::Base
belongs_to :user
belongs_to :course
end
each course model has a 'capacity' attribute, which dictates how many
places are available on that particular course. This is how I enforce
the 'capacity' constraint when a registration is created:
class Course < ActiveRecord::Base
...
def enrol(user)
if registration_count >= capacity
errors.add_to_base("this course is now full")
return false
end
self.registrations.create(:user_id => user.id)
end
def registration_count
registrations.length
end
...
end
My concern is that my code won't guard against a race condition where
2 or more users try to register for a course that is near it capacity
limit. My first thought was to wrap the registration code in a
transaction and rollback if the capacity of registrations on a course
has been exceeded.
I'm not sure if this is a valid approach and I would love to hear from
someone who could provide some adivice on how I might proceed.
My concern is that my code won't guard against a race condition where
2 or more users try to register for a course that is near it capacity
limit. My first thought was to wrap the registration code in a
transaction and rollback if the capacity of registrations on a course
has been exceeded.
[...]
Use check constraints in the DB and let *it*, not your app, worry about
concurrency issues!
If your DB server can't handle check constraints, get one that can.
My concern is that my code won't guard against a race condition where
2 or more users try to register for a course that is near it capacity
limit. My first thought was to wrap the registration code in a
transaction and rollback if the capacity of registrations on a course
has been exceeded.
[...]
Use check constraints in the DB and let *it*, not your app, worry about
concurrency issues!
That's one approach. Wrapping it up in a transaction with read isolation
would work fine too. Alternatively (and better for concurrency), keep a
counter cache on the registration_count and use the default isolation
level.
Wrap everything up in a transaction, fetch the registration count,
administer the new registration and update the counter cache. Any
concurrent registrations will be rolled back by the database -- don't
forget to catch that exception and deal with it.