私は、ユーザーが一連のコースへの出席を登録できる、非常にシンプルなRailsアプリケーションを持っています。ActiveRecordモデルは次のとおりです。
class Course < ActiveRecord::Base
has_many :scheduled_runs
...
end
class ScheduledRun < ActiveRecord::Base
belongs_to :course
has_many :attendances
has_many :attendees, :through => :attendances
...
end
class Attendance < ActiveRecord::Base
belongs_to :user
belongs_to :scheduled_run, :counter_cache => true
...
end
class User < ActiveRecord::Base
has_many :attendances
has_many :registered_courses, :through => :attendances, :source => :scheduled_run
end
ScheduledRunインスタンスには、使用可能な場所の数に制限があり、制限に達すると、それ以上の出席を受け入れることができなくなります。
def full?
attendances_count == capacity
end
Atcentances_countは、特定のScheduledRunレコードに対して作成された出席アソシエーションの数を保持するカウンターキャッシュ列です。
私の問題は、1人以上の人が同時にコースの最後の利用可能な場所に登録しようとしたときに、競合状態が発生しないようにする正しい方法を完全に知らないことです。
私の出席コントローラーは次のようになります。
class AttendancesController < ApplicationController
before_filter :load_scheduled_run
before_filter :load_user, :only => :create
def new
@user = User.new
end
def create
unless @user.valid?
render :action => 'new'
end
@attendance = @user.attendances.build(:scheduled_run_id => params[:scheduled_run_id])
if @attendance.save
flash[:notice] = "Successfully created attendance."
redirect_to root_url
else
render :action => 'new'
end
end
protected
def load_scheduled_run
@run = ScheduledRun.find(params[:scheduled_run_id])
end
def load_user
@user = User.create_new_or_load_existing(params[:user])
end
end
ご覧のとおり、ScheduledRunインスタンスがすでに容量に達した場所は考慮されていません。
これに関する助けをいただければ幸いです。
アップデート
この場合、これが楽観的ロックを実行する正しい方法であるかどうかはわかりませんが、これが私が行ったことです。
ScheduledRunsテーブルに2つの列を追加しました-
t.integer :attendances_count, :default => 0
t.integer :lock_version, :default => 0
また、ScheduledRunモデルにメソッドを追加しました。
def attend(user)
attendance = self.attendances.build(:user_id => user.id)
attendance.save
rescue ActiveRecord::StaleObjectError
self.reload!
retry unless full?
end
出席モデルが保存されると、ActiveRecordは先に進み、ScheduledRunモデルのカウンターキャッシュ列を更新します。これがどこで発生するかを示すログ出力です-
ScheduledRun Load (0.2ms) SELECT * FROM `scheduled_runs` WHERE (`scheduled_runs`.`id` = 113338481) ORDER BY date DESC
Attendance Create (0.2ms) INSERT INTO `attendances` (`created_at`, `scheduled_run_id`, `updated_at`, `user_id`) VALUES('2010-06-15 10:16:43', 113338481, '2010-06-15 10:16:43', 350162832)
ScheduledRun Update (0.2ms) UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481)
新しい出席モデルが保存される前にScheduledRunモデルに後続の更新が発生した場合、これによりStaleObjectError例外がトリガーされます。その時点で、容量にまだ達していない場合は、すべてが再試行されます。
アップデート#2
@kennの応答に続いて、SheduledRunオブジェクトの更新されたattendメソッドがあります。
# creates a new attendee on a course
def attend(user)
ScheduledRun.transaction do
begin
attendance = self.attendances.build(:user_id => user.id)
self.touch # force parent object to update its lock version
attendance.save # as child object creation in hm association skips locking mechanism
rescue ActiveRecord::StaleObjectError
self.reload!
retry unless full?
end
end
end