これは、少し凝ったダブルテーブルといくつかの制約を使用することで達成できると思います。
いくつかの(完全に正規化されていない)構造から始めましょう。
/* 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 */
) ;
テーブルの予約は、is_booked
列の代わりに列を持っていbooker
ます。nullの場合、座席は予約されません。それ以外の場合、これは予約者の名前(id)です。
いくつかのサンプルデータを追加します...
-- 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) ;
予約用に2つ目のテーブルを作成しますが、1つの制限があります。
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;
この2番目のテーブルには、(session_id、seat_number、booker)タプルのCOPYが含まれ、1つのFOREIGN KEY
制約があります。元の予約を別のタスクで更新することはできません。[同じ予約者を扱う2つのタスクは決してないと仮定します。その場合は、特定のtask_id
列を追加する必要があります。]
予約を行う必要があるときはいつでも、次の関数内で実行される一連のステップが方法を示します。
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 ;
実際に予約するには、プログラムは次のようなものを実行する必要があります。
-- 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 ;
これは二つの事実に依存している1. FOREIGN KEY
制約は、データをすることができません壊れました。2.予約テーブルをUPDATEしますが、bookings_with_bookers 1(2番目のテーブル)に対してはINSERTのみ(UPDATEはしない)のみを行います。
SERIALIZABLE
ロジックを大幅に簡略化する分離レベルは必要ありません。ただし、実際にはデッドロックが予想され、データベースと対話するプログラムはそれらを処理するように設計する必要があります。