Есть несколько способов реализации таких ограничений, и лучший из них — позволить базе данных обрабатывать жесткие ограничения.
Вариант первый
Учитывая, что у вас есть две модели, Расписание и Назначение, добавьте целочисленный столбец available_slots
в модель Расписание и уменьшите это число при создании встречи, и пусть база данных вызовет исключение, если это число станет меньше нуля. В этом случае Postgress заблокирует столбец, одновременно обновляя его, предотвращая состояние гонки.
Итак, расписание может выглядеть так:
+----+--------------+--------------+-----------------+
| 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_slots:
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
, которая будет принадлежать «Назначению» и «Расписанию», и каждая запись в таблице будет представлять доступный слот.
Например, если в расписании с идентификатором 1
будет три свободных слота, таблица будет выглядеть так:
Timetable.find(1).available_slots
+----+---------------+
| ID | timetable_id |
+----+---------------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
+----+---------------+
Затем добавьте ограничение уникального индекса в столбец available_slot_id
в таблице назначений:
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 с большим количеством параллельных транзакций (миллионы в день), которые могут вызвать условия повышения.
person
Aleksander Lopez
schedule
03.06.2020