2010-06-14 30 views
20

Tôi có một ứng dụng Rails thực sự đơn giản cho phép người dùng đăng ký tham dự của họ trên một tập hợp các khóa học. Các mô hình ActiveRecord như sau:Làm cách nào để tránh tình trạng cuộc đua trong ứng dụng Rails của tôi?

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 

Ví dụ ScheduledRun có số lượng hữu hạn các địa điểm có sẵn, và khi đạt đến giới hạn, không thể chấp nhận thêm.

def full? 
    attendances_count == capacity 
end 

Attances_count là cột bộ nhớ cache truy cập giữ số lượng liên kết tham dự được tạo cho bản ghi ScheduledRun cụ thể.

Vấn đề của tôi là tôi không hoàn toàn biết đúng cách để đảm bảo rằng điều kiện chủng tộc không xảy ra khi 1 hoặc nhiều người cố đăng ký địa điểm cuối cùng có sẵn trên một khóa học cùng một lúc.

điều khiển tham dự của tôi trông như thế này:

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 

Như bạn thấy, nó không đưa vào tài khoản mà dụ ScheduledRun đã đạt công suất.

Bất kỳ trợ giúp nào về điều này sẽ được đánh giá cao.

Cập nhật

Tôi không chắc chắn nếu điều này là đúng cách để thực hiện khóa lạc quan trong trường hợp này, nhưng đây là những gì tôi đã làm:

tôi bổ sung thêm hai cột vào bảng ScheduledRuns -

t.integer :attendances_count, :default => 0 
t.integer :lock_version, :default => 0 

tôi cũng đã thêm một phương pháp để mô hình ScheduledRun:

def attend(user) 
    attendance = self.attendances.build(:user_id => user.id) 
    attendance.save 
    rescue ActiveRecord::StaleObjectError 
    self.reload! 
    retry unless full? 
    end 

Khi mô hình tham dự được lưu, ActiveRecord tiếp tục và cập nhật cột bộ nhớ cache truy cập trên mô hình ScheduledRun. Đây là hiển thị dữ liệu ghi nhận nơi này xảy ra -

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) 

Nếu một bản cập nhật tiếp theo xảy ra với mô hình ScheduledRun trước khi mô hình tham dự mới được lưu, điều này sẽ kích hoạt các ngoại lệ StaleObjectError. Tại thời điểm đó, toàn bộ điều được thử lại, nếu năng lực chưa đạt được.

Update # 2

Tiếp theo từ @ phản ứng Kenn ở đây là phương pháp tham dự được cập nhật trên các đối tượng 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

Cố định trong đường ray mới nhất. –

+0

Bạn cần sử dụng khóa lạc quan. Screencast này sẽ chỉ cho bạn cách thực hiện: [link text] (http://railscasts.com/episodes/59-optimistic-locking) – rtacconi

+0

Ý bạn là gì, dmitry? – Edward

Trả lời

13

Khóa lạc quan là cách để đi, nhưng như bạn có thể đã nhận thấy, mã của bạn sẽ không bao giờ tăng ActiveRecord :: StaleObjectError, vì việc tạo đối tượng con trong liên kết has_many bỏ qua cơ chế khóa. Hãy nhìn vào SQL sau đây:

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

Khi bạn cập nhật thuộc tính trong mẹ đối tượng, bạn thường thấy SQL sau đây thay vì:

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

Những tuyên bố trên cho thấy cách khóa lạc quan được thực hiện : Chú ý số lock_version = 1 trong mệnh đề WHERE. Khi điều kiện chủng tộc xảy ra, các quá trình đồng thời cố gắng chạy truy vấn chính xác này, nhưng chỉ lần đầu tiên thành công, vì cái đầu tiên cập nhật nguyên tắc lock_version thành 2 và các quá trình tiếp theo sẽ không thành công tìm bản ghi và tăng ActiveRecord :: StaleObjectError, vì cùng một bản ghi không còn lock_version = 1 nữa.

Vì vậy, trong trường hợp của bạn, một cách giải quyết có thể là để chạm vào cha mẹ ngay trước khi bạn tạo/hủy diệt một đối tượng trẻ em, như vậy:

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 

Nó không có nghĩa là để tránh chặt chẽ điều kiện chủng tộc, nhưng trên thực tế nó nên làm việc trong hầu hết các trường hợp.

+0

Cảm ơn Kenn. Tôi đã không nhận ra rằng việc tạo đối tượng con đã bỏ qua cơ chế khóa. Tôi đã bao gồm toàn bộ điều trong một giao dịch quá, chỉ để đối tượng cha mẹ không cập nhật không cần thiết nếu việc tạo đối tượng con không thành công. – Cathal

0

Bạn không chỉ cần phải kiểm tra nếu @run.full??

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

    # ... 
end 

Sửa

gì nếu bạn thêm một xác nhận như:

class Attendance < ActiveRecord::Base 
    validate :validates_scheduled_run 

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

Nó sẽ không lưu các @attendance nếu liên scheduled_run đầy.

Tôi chưa thử nghiệm mã này ... nhưng tôi tin là không sao.

+0

Điều đó sẽ không hoạt động. Vấn đề là bản ghi @run đại diện có thể đã được cập nhật bởi một yêu cầu khác, để lại @run không nhất quán với những gì được trình bày trong cơ sở dữ liệu. Theo hiểu biết của tôi, khóa lạc quan là cách để giải quyết vấn đề này. Tuy nhiên, làm thế nào để bạn đi về việc áp dụng điều này cho các hiệp hội? – Cathal

+0

Phải ... Tôi đã chỉnh sửa câu trả lời của mình:] –

Các vấn đề liên quan