21

私は、ユーザーが一連のコースへの出席を登録できる、非常にシンプルな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
4

2 に答える 2

13

楽観的ロックが進むべき道ですが、すでにお気づきかもしれませんが、has_manyアソシエーションでの子オブジェクトの作成はロックメカニズムをスキップするため、コードでActiveRecord::StaleObjectErrorが発生することはありません。次のSQLを見てください。

UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481)

オブジェクトの属性を更新すると、通常、代わりに次のSQLが表示されます。

UPDATE `scheduled_runs` SET `updated_at` = '2010-07-23 10:44:19', `lock_version` = 2 WHERE id = 113338481 AND `lock_version` = 1

上記のステートメントは、楽観的ロックがどのように実装されているかを示していますlock_version = 1。inWHERE句に注意してください。競合状態が発生すると、並行プロセスはこの正確なクエリを実行しようとしますが、最初のプロセスはlock_versionを2にアトミックに更新するため、最初のプロセスのみが成功し、後続のプロセスはレコードの検索に失敗し、ActiveRecord::StaleObjectErrorを発生させます。同じレコードはもうありlock_version = 1ません。

したがって、あなたの場合、考えられる回避策は、次のように、子オブジェクトを作成/破棄する直前に親に触れることです。

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

競合状態を厳密に回避することを意図したものではありませんが、実際にはほとんどの場合に機能するはずです。

于 2010-07-23T11:05:59.353 に答える
0

あなたはただテストする必要がありませんか@run.full?

def create
   unless @user.valid? || @run.full?
      render :action => 'new'
   end

   # ...
end

編集

次のような検証を追加するとどうなりますか。

class Attendance < ActiveRecord::Base
   validate :validates_scheduled_run

   def scheduled_run
      errors.add_to_base("Error message") if self.scheduled_run.full?
   end
end

関連付けがいっぱいの@attendance場合は保存されません。scheduled_run

私はこのコードをテストしていません...しかし、私はそれが大丈夫だと信じています。

于 2010-06-14T12:28:33.740 に答える