我正在编写一个应用程序,用于管理不同服务的预约。每个服务的 "容量 "由一个或多个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中被锁定?
有几种方法可以实现这种限制,最好的方法是让数据库处理硬约束。
考虑到您有两个模型,即时间表和预约,请添加 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条件。