Reputation: 53
I need help with a project, a DB for hotel administration .
I have three tables, bookings, check_ins and cancellations. I consider that any booking can end either as an cancellation or a check-in, but not both simultaneously.
The fact is that I would need a constraint to verify, at check-in, whether the booking was cancelled, and, at cancellation, if people actually came and checked-in.
I used the same id for the three PKs, but it turns out that a check constraint, in Oracle, can't have either a user-defined-function or a select clause inside.
I.e: I have tried both
ALTER TABLE chek_ins
ADD CONSTRAINT verif_ci CHECK(id_book NOT IN (SELECT id_book FROM bookings));
and creating a function verif_cancelled(id char(
a_number))
In the same time I cannot write all of them in the same table, for the guest's identification is unknown until check-in and I use the tables bookings and check-ins as a sort of two intersection tables to link the guest and the room (to solve a M:M relation).
Do you have any suggestions? (this semester I am studying SQL for the first time)
Thank you in advance!
Upvotes: 1
Views: 972
Reputation: 94914
Your check-in table has a booking ID, so that the check-in refers to a booking. This booking ID has probably a unique constraint (maybe it's even the table's primary key), in order to create the desired 1:{0,1} relation. Something along the lines of:
create table check_in
(
booking_id number(9),
check_in_time date,
number_of_persons number(2),
main_person_name varchar2(100),
constraint pk_check_in primary key (booking_id),
constraint fk_check_in_booking foreign key (booking_id) references booking (booking_id)
);
Your annulment table is probably constructed in the same way. However, as these are two different tables that the booking table is not aware of, you can both insert a check-in and an annulment for a booking, which you want to avoid. A check constraint is not possible, because it can only refer to the table itself, not to other tables. (Triggers in the check-in and annulment tables that check the other table to throw an exception in case of an entry there would be possible, but I don't recommend them in this scenario.)
1:1 relatated tables are a rare thing. I see that they are useful here, because you can make their columns mandatory or optional, e.g. the check-in time and main person name may be mandatory (NOT NULL
) while the number of persons may be optional. If these were just attributes directly placed in the booking table instead, you could not make check-in time and main person name mandotory, because they must only be mandatory in case of a check-in.
Luckily, with 1:1 you can simply relate them the other way round. I.e. remove the booking ID from the two satellite tables, give them independent primary keys and refer to these primary keys in the booking table instead:
create table check_in
(
check_in_id number(9),
check_in_time date,
number_of_persons number(2),
main_person_name varchar2(100),
constraint pk_check_in primary key (check_in_id)
);
create table annulment
(
annulment_id number(9),
...,
constraint pk_annulment primary key (annulment_id)
);
create table booking
(
booking_id number(9),
room_no number(3),
start_date date,
end_date date,
person_name varchar2(100),
check_in_id number(9),
annulment_id number(9),
constraint pk_booking primary key (booking_id),
constraint fk_booking_check_in foreign key (check_in_id) references check_in (check_in_id),
constraint fk_booking_annulment foreign key (annulment_id) references annulment (annulment_id),
constraint uq_booking_check_in unique (check_in_id),
constraint uq_booking_annulment unique (annulment_id),
constraint chk_booking check (check_in_id is null or annulment_id is null)
);
You see how easy it is to place the desired check constraint in the booking table now.
A last remark: While it is great to have all those integrity checks in place and be able to, say, select all annulments placed in March, you might be able to do with a much simpler database. Instead of two satellite tables, you could just put the information in single columns, e.g. a CLOB, JSON, or Oracle object. It would be like using a mere note rather than a form in real live. Less reliable, but maybe sufficient for the job. The database would reduce to a single table:
create table booking
(
booking_id number(9),
room_no number(3),
start_date date,
end_date date,
person_name varchar2(100),
check_in_data varchar2(4000),
annulment_data varchar2(4000),
constraint pk_booking primary key (booking_id),
constraint chk_booking check (check_in_data is null or annulment_data is null)
);
As mentioned, the DBMS can not guarantee here the check-in and annulment data to be complete (i.e. to the DBMS it makes no difference if annulment_data contains 'Cancelled on Feb 9, 2019 by Mr. Miller, because of sickness' or just 'Cancelled by Mr. Miller' or even 'dum deedle dum'), but maybe you are fine with this - your app will make sure only complete data gets written to the database.
Upvotes: 1
Reputation: 167981
To start with, have a table of possible booking statuses that enumerates all the possible states a booking can be in:
CREATE TABLE statuses (
id NUMBER(1,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT statuses__id__pk PRIMARY KEY,
description VARCHAR2(10)
CONSTRAINT statuses__desc__nn NOT NULL
CONSTRAINT statuses__desc__u UNIQUE
);
INSERT INTO statuses ( description )
SELECT 'Booked' FROM DUAL UNION ALL
SELECT 'Checked-In' FROM DUAL UNION ALL
SELECT 'Annulled' FROM DUAL;
ALTER TABLE statuses READ ONLY;
Then add a status
column to your bookings
table:
CREATE TABLE Bookings (
id NUMBER(10,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT bookings__id__pk PRIMARY KEY,
status NUMBER(1,0)
CONSTRAINT bookings__status__nn NOT NULL
CONSTRAINT bookings__status__fk REFERENCES statuses ( id ),
CONSTRAINT bookings__id__status__u UNIQUE ( id, status )
)
Then you can add a virtual column to your CheckIns
and Annullments
tables that have the appropriate statuses so that an entry cannot be made into the table unless the booking is in the correct status:
CREATE TABLE CheckIns (
id NUMBER(10,0)
CONSTRAINT CheckIns__id__pk PRIMARY KEY,
status NUMBER(1,0)
GENERATED ALWAYS AS ( 2 )
CONSTRAINT CheckIns__status__nn NOT NULL
CONSTRAINT CheckIns__status__fk REFERENCES statuses ( id ),
CONSTRAINT CheckIns__id__status__fk
FOREIGN KEY ( id, status )
REFERENCES bookings ( id, status )
);
CREATE TABLE Annullments (
id NUMBER(10,0)
CONSTRAINT annullments__id__pk PRIMARY KEY,
status NUMBER(1,0)
GENERATED ALWAYS AS ( 3 )
CONSTRAINT annullments__status__nn NOT NULL
CONSTRAINT annullments__status__fk REFERENCES statuses ( id ),
CONSTRAINT annullments__id__status__fk
FOREIGN KEY ( id, status )
REFERENCES bookings ( id, status )
);
Then if you try to enter a booking with the Checked-In
status into the Annullments
table it will fail (and vice versa):
DECLARE
p_id Bookings.id%type;
BEGIN
INSERT INTO bookings ( status )
VALUES( ( SELECT id FROM statuses WHERE description = 'Checked-In' ) )
RETURNING id INTO p_id;
BEGIN
INSERT INTO CheckIns( id )
VALUES ( p_id );
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE( 'Could not check-in' );
END;
BEGIN
INSERT INTO Annullments ( id )
VALUES ( p_id );
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE( 'Could not annul booking' );
END;
END;
/
outputs:
Could not annul booking
and if you try to change a booking to another status while there is a dependent entry in another table then it will fail:
UPDATE Bookings
SET status = ( SELECT id FROM statuses WHERE description = 'Annulled' )
WHERE status = ( SELECT id FROM statuses WHERE description = 'Checked-In' )
Outputs:
ORA-02292: integrity constraint (FIDDLE_CWUTVSMRLOQQQVDQLCGR.CHECKINS__ID__STATUS__FK) violated - child record found
You could further improve the API by creating a package with functions to:
Upvotes: 2