2010-06-14 22 views
20

我有一個非常簡單的Rails應用程序,它允許用戶在一組課程中註冊他們的出席。 ActiveRecord的型號如下:如何避免我的Rails應用程序出現競態狀況?

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 

attendances_count是一個計數器緩存列保存爲特定ScheduledRun記錄創建出席關聯的數量。

我的問題是,我不完全知道正確的方法,以確保當一個或多個人試圖在同一時間註冊課程的最後一個可用位置時不會出現競爭狀況。

我的出勤控制器看起來是這樣的:

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表 -

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對象上的更新出席方法:

# 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 
+0

固定在最新的導軌。 – 2012-04-10 10:10:30

+0

您需要使用樂觀鎖定。這個截屏會告訴你如何做到這一點:[鏈接文本](http://railscasts.com/episodes/59-optimistic-locking) – rtacconi 2010-06-14 15:23:03

+0

你是什麼意思,德米特里? – Edward 2015-06-04 01:07:44

回答

13

樂觀鎖定是要走的路,但正如您可能已經注意到的,您的代碼永遠不會引發ActiveRecord :: StaleObjectError,因爲在has_many關聯中創建子對象會跳過鎖定機制。看看下面的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 

上面的語句表示樂觀鎖是如何實現的:請注意WHERE子句中的lock_version = 1。當競爭情況發生時,併發進程嘗試運行這個確切的查詢,但只有第一個成功,因爲第一個原子更新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 

這並不意味着要嚴格避免競爭狀態,但實際上它應該在大多數情況下工作。

+0

謝謝Kenn。我沒有意識到,創建子對象跳過了鎖定機制。 我把事情也包裹在一個事務中,只是這樣,如果子對象創建失敗,父對象不會不必要地更新。 – Cathal 2010-07-27 13:39:29

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已滿。

我還沒有測試過這個代碼......但我相信沒關係。

+0

這是行不通的。問題是記錄@run表示可能已經被另一個請求更新,使@run與數據庫中表示的內容不一致。據我所知,樂觀鎖定是解決這個問題的方法。然而,你如何去應用這種聯繫? – Cathal 2010-06-14 13:13:25

+0

對...我編輯了我的答案:] – 2010-06-14 13:39:11

相關問題