我有一个非常简单的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

Attentings_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实例已经达到容量的位置.

在这方面的任何帮助都将不胜感激.

Update

我不确定在这种情况下这是否是执行乐观锁定的正确方式,但我是这样做的:

我在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模型上的计数器缓存列.以下是显示发生这种情况的位置的日志(log)输出-

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异常.在这一点上,如果尚未达到容量,整个过程将再次重试.

Update #2

在@Kenn的响应之后,下面是SheuledRun对象上更新的Add方法:

# 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

推荐答案

乐观锁定是可行的,但是您可能已经注意到,您的代码永远不会引发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)

更新parent对象中的属性时,通常会看到以下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.当竞争条件发生时,并发进程try 运行这个确切的查询,但只有第一个成功,因为第一个进程自动将lock_version更新为2,后续进程将无法find记录并引发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

这并不是为了严格避免比赛条件,但实际上它在大多数情况下都应该有效.

Database相关问答推荐

Spring Boot:如何使用多个模式并在运行时动态 Select 使用哪一个

Entity Framework:如何检测对数据库的外部更改

如何在运行时备份嵌入式 H2 数据库引擎?

如何验证 SQLAlchemy ORM 中的列数据类型?

如何在构建时创建填充的 MySQL Docker 映像

从 XML 读取数据

nvarchar (50) 与 nvarchar (max) 的含义

更改列类型而不丢失数据

应用程序用户应该是数据库用户吗?

MySQL 转储所有数据库并在导入时创建(或重新创建)它们?

使用 JSON 作为存储/传输格式的数据库

如何在一行中显示 redis 中的所有键?

在数据库字段中存储数字数组

Redis-cli - Select 哪个实例?

如何一次插入1000行

使用默认路径中的文件创建数据库

Django Atomic Transaction 是否锁定数据库?

分离实体和被管理实体

LevelDB 支持 java 吗?

美国城市和州的列表/数据库