I have a really simple Rails application that allows users to register their attendance on a set of courses. The ActiveRecord models are as follows:
class Co
Optimistic locking is the way to go, but as you might have noticed already, your code will never raise ActiveRecord::StaleObjectError, since child object creation in has_many association skips the locking mechanism. Take a look at the following SQL:
UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481)
When you update attributes in the parent object, you usually see the following SQL instead:
UPDATE `scheduled_runs` SET `updated_at` = '2010-07-23 10:44:19', `lock_version` = 2 WHERE id = 113338481 AND `lock_version` = 1
The above statement shows how optimistic locking is implemented: Notice the lock_version = 1
in WHERE clause. When race condition happens, concurrent processes try to run this exact query, but only the first one succeeds, because the first one atomically updates the lock_version to 2, and subsequent processes will fail to find the record and raise ActiveRecord::StaleObjectError, since the same record doesn't have lock_version = 1
any longer.
So, in your case, a possible workaround is to touch the parent right before you create/destroy a child object, like so:
def attend(user)
self.touch # Assuming you have updated_at column
attendance = self.attendances.create(:user_id => user.id)
rescue ActiveRecord::StaleObjectError
#...do something...
end
It's not meant to strictly avoid race conditions, but practically it should work in most cases.
Don't you just have to test if @run.full?
?
def create
unless @user.valid? || @run.full?
render :action => 'new'
end
# ...
end
Edit
What if you add a validation like:
class Attendance < ActiveRecord::Base
validate :validates_scheduled_run
def scheduled_run
errors.add_to_base("Error message") if self.scheduled_run.full?
end
end
It won't save the @attendance
if the associated scheduled_run
is full.
I haven't tested this code... but I believe it's ok.