如何在 Rails 中控制关系的非固定cardinality?

问题描述 投票:1回答:1

我正在编写一个应用程序,用于管理不同服务的预约。每个服务的 "容量 "由一个或多个Timetables决定,这意味着服务A可能在6月1日到6月30日有2个 "办公桌",而在7月1日到8月31日只有1个,所以我可以为 "2020-06-03 9:00 "创建2个预约,但为 "2020-07-03 9:00 "只创建1个。所有的模型都是正确的,我为Appointments在创建时有一个自定义的验证器来检查cardinality,但这不足以防止两个用户同时创建最后一个可用的预约,是吗?

如何控制这种关系的正确cardinality而不阻塞整个Appointments表?

预约的创建在代码中只有一个地方完成,在Appointment.create_appointment(params)中,有没有办法让这个方法在rails中被锁定?

ruby-on-rails locking relation cardinality
1个回答
0
投票

有几种方法可以实现这种限制,最好的方法是让数据库处理硬约束。

方案一

考虑到您有两个模型,即时间表和预约,请添加 available_slots 整数列到Timetable模型中,并在创建预约时减少这个数字,如果这个数字低于零,就让数据库引发一个异常。在这种情况下,Postgress会在更新该列的同时锁定该列,防止出现竞赛条件。

所以Timetable可以是这样的。

+----+--------------+--------------+-----------------+
| ID |  time_from   |   time_to    | available_slots |
+----+--------------+--------------+-----------------+
| 1  | '2020-03-21' | '2020-04-21' |               2 |
| 2  | '2020-04-22' | '2020-05-21' |               3 |
+----+--------------+--------------+-----------------+

在MySQL中,你会把它变成一个无符号的整数 但由于Postgres不支持它,你可以选择添加一个正数检查约束到 available_slots 列。

纯粹的SQL。

ALTER TABLE timetables ADD CONSTRAINT available_slots CHECK (available_slots > 0)

迁移是这样的:

class AddPositiveConstraintToTimetable < ActiveRecord::Migration[6.0]
  def self.up
    execute "ALTER TABLE timetables ADD CONSTRAINT available_slots CHECK (available_slots > 0)"
  end

  def self.down
    execute "ALTER TABLE timetables DROP CONSTRAINT available_slots."
  end
end

在Appointment模型中加入减少available_slot的逻辑。

belongs_to :timetable
before_create :decrease_slots

def decrease_slots
  # this will through an exception from the database
  # in case if available_slots are already 0
  # that will prevent the instance from being created.
  
  timetable.decrement!(:available_slots)
end

从AppointmentsController中捕获异常。

def create
  @appointment = Appointment.new(appointment_params)

  # here will be probably some logic to find out the timetable
  # based on the time provided by the user (or maybe it's in the model).

  if @appointment.save
    redirect_to @appointment, notice: 'Appointment was successfully created.'
  else
    render :new
  end
end

选项二

另一种方法是增加一个新的模型,例如。AvailableSlot 将属于预约和时间表,表中的每条记录将代表一个可用的插槽。

例如,如果时间表的id为 1 将有三个可用的插槽,该表将看起来像。

Timetable.find(1).available_slots

+----+---------------+
| ID |  timetable_id |
+----+---------------+
| 1  | 1             |
| 2  | 1             |
| 3  | 1             |
+----+---------------+

然后添加一个唯一的索引约束到 available_slot_id 列在appointments表中。

add_index :appointments, :available_slot_id, unique: true

所以每当你创建一个预约 并将其与一个可用的插槽相关联, 数据库将通过一个异常, 如果有一个记录 与相同的available_slot_id。

你就必须添加逻辑来寻找可用的槽位。预约模型中的一个原始例子。

before_create :find_available_slot

def find_available_slot
  # first find a timetable
  timetable = Timetable.where("time_from >= ? AND time_to <= ?", appointment_time, appointment_time).first
  
  # then check if there are available slots
  taken_slots = Appintment.where(timetable.id: timetable.id).size
  all_slots = timetable.available_slots.size

  raise "no available slots" unless (all_slots - taken_slots).positive?

  # huzzah! there are slots, lets take the last one
  self.available_slot = timetable.available_slots.last
end

如果你在available_slots中添加一个状态列,当一个预约被创建时,这个代码可以被简化,但我让你自己去想办法。


这些选项是基于我在生产中的Rails应用中看到的类似的方法,这些应用有大量的并发事务发生(每天几百万),可能会导致rise条件。

© www.soinside.com 2019 - 2024. All rights reserved.