Penso che ciò possa essere realizzato usando un piccolo tavolo doppio di fantasia e alcuni vincoli.
Partiamo da una struttura (non completamente normalizzata):
/* Everything goes to one schema... */
CREATE SCHEMA bookings ;
SET search_path = bookings ;
/* A table for theatre sessions (or events, or ...) */
CREATE TABLE sessions
(
session_id integer /* serial */ PRIMARY KEY,
session_theater TEXT NOT NULL, /* Should be normalized */
session_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
performance_name TEXT, /* Should be normalized */
UNIQUE (session_theater, session_timestamp) /* Alternate natural key */
) ;
/* And one for bookings */
CREATE TABLE bookings
(
session_id INTEGER NOT NULL REFERENCES sessions (session_id),
seat_number INTEGER NOT NULL /* REFERENCES ... */,
booker TEXT NULL,
PRIMARY KEY (session_id, seat_number),
UNIQUE (session_id, seat_number, booker) /* Needed redundance */
) ;
Le prenotazioni dei tavoli, invece di avere una is_booked
colonna, hanno una booker
colonna. Se è nullo, il posto non è prenotato, altrimenti questo è il nome (id) del booker.
Aggiungiamo alcuni dati di esempio ...
-- Sample data
INSERT INTO sessions
(session_id, session_theater, session_timestamp, performance_name)
VALUES
(1, 'Her Majesty''s Theatre',
'2017-01-06 19:30 Europe/London', 'The Phantom of the Opera'),
(2, 'Her Majesty''s Theatre',
'2017-01-07 14:30 Europe/London', 'The Phantom of the Opera'),
(3, 'Her Majesty''s Theatre',
'2017-01-07 19:30 Europe/London', 'The Phantom of the Opera') ;
-- ALl sessions have 100 free seats
INSERT INTO bookings (session_id, seat_number)
SELECT
session_id, seat_number
FROM
generate_series(1, 3) AS x(session_id),
generate_series(1, 100) AS y(seat_number) ;
Creiamo una seconda tabella per le prenotazioni, con una limitazione:
CREATE TABLE bookings_with_bookers
(
session_id INTEGER NOT NULL,
seat_number INTEGER NOT NULL,
booker TEXT NOT NULL,
PRIMARY KEY (session_id, seat_number)
) ;
-- Restraint bookings_with_bookers: they must match bookings
ALTER TABLE bookings_with_bookers
ADD FOREIGN KEY (session_id, seat_number, booker)
REFERENCES bookings.bookings (session_id, seat_number, booker) MATCH FULL
ON UPDATE RESTRICT ON DELETE RESTRICT
DEFERRABLE INITIALLY DEFERRED;
Questa seconda tabella conterrà una COPIA delle tuple (session_id, seat_number, booker), con un FOREIGN KEY
vincolo; ciò non consentirà AGGIORNAMENTO delle prenotazioni originali da parte di un'altra attività. [Supponendo che non ci siano mai due compiti relativi allo stesso booker ; in tal caso, task_id
aggiungere una determinata colonna.]
Ogni volta che è necessario effettuare una prenotazione, la sequenza di passaggi seguiti nella seguente funzione mostra la via:
CREATE or REPLACE FUNCTION book_session
(IN _booker text, IN _session_id integer, IN _number_of_seats integer)
RETURNS integer /* number of seats really booked */ AS
$BODY$
DECLARE
number_really_booked INTEGER ;
BEGIN
-- Choose a random sample of seats, assign them to the booker.
-- Take a list of free seats
WITH free_seats AS
(
SELECT
b.seat_number
FROM
bookings.bookings b
WHERE
b.session_id = _session_id
AND b.booker IS NULL
ORDER BY
random() /* In practice, you'd never do it */
LIMIT
_number_of_seats
FOR UPDATE /* We want to update those rows, and book them */
)
-- Update the 'bookings' table to have our _booker set in.
, update_bookings AS
(
UPDATE
bookings.bookings b
SET
booker = _booker
FROM
free_seats
WHERE
b.session_id = _session_id AND
b.seat_number = free_seats.seat_number
RETURNING
b.session_id, b.seat_number, b.booker
)
-- Insert all this information in our second table,
-- that acts as a 'lock'
, insert_into_bookings_with_bookers AS
(
INSERT INTO
bookings.bookings_with_bookers (session_id, seat_number, booker)
SELECT
update_bookings.session_id,
update_bookings.seat_number,
update_bookings.booker
FROM
update_bookings
RETURNING
bookings.bookings_with_bookers.seat_number
)
-- Count real number of seats booked, and return it
SELECT
count(seat_number)
INTO
number_really_booked
FROM
insert_into_bookings_with_bookers ;
RETURN number_really_booked ;
END ;
$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF STRICT
COST 10000 ;
Per effettuare davvero una prenotazione, il tuo programma dovrebbe provare a eseguire qualcosa del tipo:
-- Whenever we wich to book 37 seats for session 2...
BEGIN TRANSACTION ;
SELECT
book_session('Andrew the Theater-goer', 2, 37) ;
/* Three things can happen:
- The select returns the wished number of seats
=> COMMIT
This can cause an EXCEPTION, and a need for (implicit)
ROLLBACK which should be handled and the process
retried a number of times
if no exception => the process is finished, you have your booking
- The select returns less than the wished number of seats
=> ROLLBACK and RETRY
we don't have enough seats, or some rows changed during function
execution
- (There can be a deadlock condition... that should be handled)
*/
COMMIT /* or ROLLBACK */ TRANSACTION ;
Ciò si basa su due fatti 1. Il FOREIGN KEY
vincolo non consente la rottura dei dati . 2. AGGIORNIAMO la tabella delle prenotazioni, ma solo INSERISCI (e mai AGGIORNAMENTI ) su bookings_with_bookers uno (la seconda tabella).
Non ha bisogno del SERIALIZABLE
livello di isolamento, il che semplificherebbe notevolmente la logica. In pratica, tuttavia, sono previsti deadlock e il programma che interagisce con il database deve essere progettato per gestirli.