Enforce composite unique constraint that depends on parent column value

Viewed 324

With provided schema i want to somehow enforce that there is unique reserved_seat:seat_id per showing. In other words you can't reserve specific seat if it is already reserved in that showing.

One option is to also add showing_id to reservation_seat (which is redundant) and then make unique constraint on (showing_id, seat_id).

Can this be done in sql or it falls to application code?

enter image description here

The DDL:

CREATE TABLE showing
(
    id              INT  NOT NULL  AUTO_INCREMENT,
    name            VARCHAR(45) NOT NULL,
    PRIMARY KEY (id)
)

CREATE TABLE reservation
(
    id              INT  NOT NULL  AUTO_INCREMENT,
    showing_id      INT  NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (showing_id) REFERENCES showing(id)
)

CREATE TABLE reservation_seat
(
    id              INT  NOT NULL  AUTO_INCREMENT,
    reservation_id  INT  NOT NULL,
    seat_id         INT  NOT NULL,
    confirmed       TINYINT,
    PRIMARY KEY (id),
    FOREIGN KEY (reservation_id) REFERENCES reservation(id),
    FOREIGN KEY (seat_id) REFERENCES seat(id)
)

CREATE TABLE seat
(
    id              INT  NOT NULL  AUTO_INCREMENT,
    row             VARCHAR(45) NOT NULL,
    column          VARCHAR(45) NOT NULL,
    PRIMARY KEY (id)
)
2 Answers

I believe that this is one of those rare cases where the use of surrogate keys (auto_increment id's) instead of natural keys has led you astray. Consider how your table definitions would look if you used natural keys instead:

CREATE TABLE showing
(
    name            VARCHAR(45) NOT NULL,   -- globally unique
    PRIMARY KEY (name)
)

CREATE TABLE reservation
(
    showing_name    VARCHAR(45) NOT NULL,
    name            VARCHAR(45) NOT NULL,   -- only unique within showing_name
    PRIMARY KEY (name, showing_name),
    FOREIGN KEY (showing_name) REFERENCES showing(name)
)

CREATE TABLE reservation_seat
(
    showing_name    VARCHAR(45) NOT NULL,
    reservation_name VARCHAR(45) NOT NULL,
    seat_row        VARCHAR(45) NOT NULL,
    seat_column     VARCHAR(45) NOT NULL,
    confirmed       TINYINT,
    PRIMARY KEY (showing_name, reservation_name, seat_row, seat_column),
    FOREIGN KEY (showing_name, reservation_name) REFERENCES reservation(showing_name, name),
    FOREIGN KEY (seat_row, seat_column) REFERENCES seat(row, column)
)

Now you can add your reserved seat per showing constraint as an Alternate Key on reservation_seat:

CREATE TABLE reservation_seat
(
    showing_name    VARCHAR(45) NOT NULL,
    reservation_name VARCHAR(45) NOT NULL,
    seat_row        VARCHAR(45) NOT NULL,
    seat_column     VARCHAR(45) NOT NULL,
    confirmed       TINYINT,
    PRIMARY KEY (showing_name, reservation_name, seat_row, seat_column),
    FOREIGN KEY (showing_name, reservation_name) REFERENCES reservation(showing_name, name),
    FOREIGN KEY (seat_row, seat_column) REFERENCES seat(row, column),
    CONSTRAINT UC_seat_showing_reserved UNIQUE(showing_name, seat_row, seat_column)
)

However, this makes it clear that the primary key is superfluous because it's just a weaker version of the constraint that we have added, so we should replace it with our new constraint.

CREATE TABLE reservation_seat
(
    showing_name    VARCHAR(45) NOT NULL,
    reservation_name VARCHAR(45) NOT NULL,
    seat_row        VARCHAR(45) NOT NULL,
    seat_column     VARCHAR(45) NOT NULL,
    confirmed       TINYINT,
    PRIMARY KEY (showing_name, seat_row, seat_column),
    FOREIGN KEY (showing_name, reservation_name) REFERENCES reservation(showing_name, name),
    FOREIGN KEY (seat_row, seat_column) REFERENCES seat(row, column)
)

We may worry now that our reservation_seat could be referencing a reservation with a different showing_id than the reservation_seat itself, but that's not a problem for natural keys because the first foreign key reference prevents that.

Now all we need to do is to translate this back into surrogate keys:

CREATE TABLE reservation_seat
(
    id              INT  NOT NULL  AUTO_INCREMENT,
    showing_id      INT  NOT NULL,
    reservation_id  INT  NOT NULL,
    seat_id         INT  NOT NULL,
    confirmed       TINYINT,
    PRIMARY KEY (id),
    FOREIGN KEY (showing_id, reservation_id) REFERENCES reservation(showing_id, id),
    FOREIGN KEY (seat_id) REFERENCES seat(id),
    CONSTRAINT UC_seat_showing_reserved UNIQUE(showing_id, seat_id)
)

Because we're making the reservation_seat(id) the primary key, we have to change the named PK definition back into a unique constraint. Compared to your original reservation_seat definition, we end up with showing_id added, but with the modified stronger first foreign key definition we now insure both that reservation_seat are unique within a showing and that reservation_seat cannot have a showing_id different from its parent reservation.

(Note: you will probably have to quote the 'row' and 'column' column names in the SQL code above)

Additional Note: DBMS's vary on this (and I am not sure about MySql in this case), but many will require that a Foreign Key relation have a corresponding Primary Key or Unique Constraint on the target (referenced) table. This would mean that you would have to alter the reservation table with a new constraint like:

CONSTRAINT UC_showing_reserved UNIQUE(showing_id, id)

to match the new FK definition on reservation_seat that I suggested above:

FOREIGN KEY (showing_id, reservation_id) REFERENCES reservation(showing_id, id),

Technically, this would be a redundant constraint since it is a weaker version of the primary key on the reservation table, but in this case SQL would probably still require it to implement the FK.

Does it take 90 characters to specify a "seat"? The seats I am familiar with are like "103-45" or "J17". Or even "Sec 4 Row 43 Seat 105". You have not mentioned it, but row/column is not adequate to answer the question "are these two seats adjacent?"

My first approach to the problem is to get rid of the table seat, other than being able to enumerate all the seats in a venue.

Then I would question the table reservation_seat, which smells like a many-to-many mapping (plus a flag). Many:many implies non uniqueness. So, something has to give.

The raw, unnormalized, data seems to be

showing:  showing_id (PK), date, time, location
reservation:  showing_id, seat, confirmed

Having this (on reservation) probably answers your question:

PRIMARY KEY(showing_id, seat)

It ties the two tables together, provides a 'natural' PK, and still allows for the confirmed flag.

I don't know your logic for "confirming". I assume that you cannot reassign a seat while it is waiting to be confirmed?

Back to my starting comment. seat VARCHAR(15) might be appropriate. And, if you need it, another table could have

CREATE TABLE venue (
    venue_id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR (144) NOT NULL,
    location ...
    capacity SMALLINT UNSIGNED NOT NULL,
    ...
    PRIMARY KEY(venue_id)
) ENGINE=InnoDB

CREATE TABLE seat (
    venue_id SMALLINT UNSIGNED NOT NULL,
    seat_num VARCHAR(30) NOT NULL,
    is_handicap ...,
    strip_num SMALLINT UNSIGNED NOT NULL,  -- see below
    PRIMARY KEY(venue_id, seat_num)
) ENGINE=InnoDB

This fails to take care of a venue where you sometimes want to block off the balcony, thereby invalidating some of the seats. Having a different venue and id with most of the info the same might be direction to take.

Be sure to use transactions (BEGIN..COMMIT and FOR UPDATE) where appropriate.

CREATE TABLE showing (
    showing_id MEDIUM UNSIGNED NOT NULL AUTO_INCREMENT,
    venue_id SMALLINT UNSIGNED NOT NULL,
    date ...
    notes ...
    PRIMARY KEY(showing_id)
) ENGINE=InnoDB

To deal with seat adjacency, I suggest manually assign each stretch of adjacent seats a "strip" number that stops at aisles, posts, etc. Alas, that is not adequate for a middle section where seat "1" is in the middle, with even numbers going one way and odd going the other way. So K-8 and K-9 are quite far apart, but K-8 and K-10 are adjacent, in spite sorting far apart.

As for confirmed, it "belongs" in reservation. But it might be more convenient for other actions to have it in seat. We may need to work out the SQL statements to make that decision. Also, the SQL statements are necessary for deciding on what secondary INDEXes to have.

Related