How can set unique key checking by another table - sql

i have create three tables: supplier, item and purchase. supplier id has relation with item table and item id has relation with purchase table. I do not want to insert itemid on purchase table in same supplier item. How can I set constraint?
CREATE TABLE csupplier(
supid NUMBER(10) PRIMARY KEY ,
supname VARCHAR2(30)
);
CREATE TABLE ctitem(
itemid NUMBER(10) PRIMARY KEY,
itemname VARCHAR2(50),
supid NUMBER(10)
);
ALTER TABLE CTITEM
ADD CONSTRAINT CTITEM_FK1 FOREIGN KEY(SUPID )REFERENCES CSUPPLIER(SUPID );
CREATE TABLE cPurchase(
purchaseid NUMBER(10) PRIMARY KEY,
itemid NUMBER(10),
purchaseqty NUMBER(10)
);
ALTER TABLE CPURCHASE
ADD CONSTRAINT CPURCHASE_FK1 FOREIGN KEY(ITEMID )REFERENCES CTITEM(ITEMID )

i don not want insert item-1 and item-3 in a same time under purchase
The problem is Oracle does not understand concept of at the same time. It understands transactions, it understands DML statements, it understands unique keys. So we need to frame your question in terms Oracle can understand: for instance, a given purchase cannot have more than one item from the same supplier.
Your first problem is that your data model can't support such a rule. Your cpurchase table has a primary key of purchaseid which means you have one record per item purchased. There is no set of purchased items against which we can enforce a rule. So, the first thing is to change the data model:
CREATE TABLE cPurchase(
purchaseid NUMBER(10) PRIMARY KEY );
CREATE TABLE cPurchaseItem(
purchaseid NUMBER(10),
itemid NUMBER(10),
purchaseqty NUMBER(10)
);
ALTER TABLE CPURCHASEITEM
ADD CONSTRAINT CPURCHASEITEM_PK PRIMARY KEY(PURCHASEID,ITEMID);
ALTER TABLE CPURCHASEITEM
ADD CONSTRAINT CPURCHASEITEM_FK1 FOREIGN KEY(PURCHASEID )REFERENCES CPURCHASE;
ALTER TABLE CPURCHASE
ADD CONSTRAINT CPURCHASE_FK2 FOREIGN KEY(ITEMID )REFERENCES CTITEM(ITEMID );
Now we have a header-detail structure which assigns multiple items to one purchase, which means we can attempt to enforce the rule.
The next problem is that supplierid is not an attribute of cpurchaseitem. There is no way to build a check constraint on a table or column which executes a query on another table. What you are after is a SQL Assertion, which is a notional construct that would allow us to define such rules. Alas Oracle (nor any other RDBMS) supports Assertions at the moment.
So that leaves us with three options:
Go procedural, and write a transaction API which enforces this rule.
Denormalise cpurchaeitem to include supplierid then build a unique constraint on (purchaseid, supplierid). You would need to populate supplierid whenever you populate cpurchaseitem.
Write an after statement trigger:
(Warning: this is coded wildstyle and may contain bugs and/or compilation errors.)
create or replace trigger cpurchaseitem_trg
after insert or update on cpurchaseitem
declare
rec_count number;
begin
select count(*)
into rec_count
from cpurchaseitem pi
join citem I on pi.itemid = i.itemid
group by pi.purchaseid, i.supplierid having count(*) > 1;
if rec_count > 0 then
raise_application_error(-20000
, 'more than one item for a supplier!');
end if;
end;
Frankly none of these solutions is especially appealing. The API is a solid solution but open to circumvention. The trigger will suffer from scaling issues as the number of purchases grows over time (although this can be mitigated by writing a compound trigger instead, left as an exercise for the reader). Denormalisation is the safest (and probably most performative) solution, even though it's not modelling best practice.

There are 2 solutions to your problem:
1. Alter the table cPurchase and add the supid column in the table. and make the unique key on this column. This will solve your problem.
CREATE TABLE cPurchase(
purchaseid NUMBER(10) PRIMARY KEY,
itemid NUMBER(10),
purchaseqty NUMBER(10),
supid NUMBER(10) UNIQUE KEY
);
If alter is not possible on this table, write a row level, Before Insert/update trigger. In this trigger write the logic to find the Supid based on the Item_id ctitem and them find any item on this supplier exists in your purchase table.
CREATE [ OR REPLACE ] TRIGGER SUP_CHECK
BEFORE INSERT
ON cPurchase
FOR EACH ROW
DECLARE
L_COUNT NUMBER;
BEGIN
SELECT COUNT(*) INTO L_COUNT
FROM cPurchase c
WHERE C.itemid in (Select itemid from ctitem ct where ct.supid = (Select supid
from ctitem where itemid = :new.itemid) );
EXCEPTION
WHEN ...
-- exception handling
END;

Related

Trigger for 1:m relationship

I have 2 tables: customers and orders. How can I use trigger to create 1:M relationship?
I have next tables
CREATE TABLE Customer(
customer_id INT GENERATED BY DEFAULT AS IDENTITY,
customer_name VARCHAR2(50) NOT NULL,
datachange TIMESTAMP NOT NULL,
PRIMARY KEY(customer_id)
);
CREATE TABLE Orders(
order_id INT GENERATED BY DEFAULT AS IDENTITY,
customer_id INT NOT NULL,
PRIMARY KEY(order_id)
);
And I have to do a trigger instead of:
ALTER TABLE Orders add constraint FK_CUSTOMER_ID FOREIGN KEY (customer_id) REFERENCES Customer(customer_id);
here is an example how to create a trigger:
https://www.siteground.com/kb/mysql-triggers-use/
but relation is the best solution at this moment:
if there is no specific reason to complicate something, don't complicate it. Use simple and widely used solutions to avoid many problems in the future :-)
ps. I'm not sure if you are using MySQL or some other database because you didn't specify ...
If you look at trigger syntax. It clearly mentions
CREATE TRIGGER trigger_name
{BEFORE | AFTER} {INSERT | UPDATE| DELETE }
ON table_name FOR EACH ROW
trigger_body;
Can't find any trigger on CREATE TABLE.
Can you properly explain why the trigger needs to be fired after you create table?
Maybe you are misunderstanding the functions of a trigger

how to set a constraint that sets null on only one of the fields in the composite foreign key when the parent record is deleted?

I have 2 postgres tables.
table one:
|id|user_id|master_team_id|role_id
table two:
|uuid|user_id|master_team_id|number
master_team_id in table two can be null.
user_id and master_id foreign key references the user_id and
master_team_id in table one.
in order for master_team_id in table two to not be null, the user_id
and master_team_id combo must exist in table one.
how do i add a constraint that sets null on only master_team_id in the composite key(user_id, master_team_id) in table two when the referenced row in table one is deleted?
in the FK constraint specify ON DELETE SET NULL
https://www.techonthenet.com/sql_server/foreign_keys/foreign_delete_null.php
Side Note: I would suggest using adding a new column to table two called "TableOneID" that way you can know if the matching record exists or not.
You can't do that yet.
What a coincidence. The ability to do that was committed yesterday, and (if all goes according to plan) will be included in v15, which is not due out for nearly a year.
You can use a trigger, which would look like this:
create table p (c1 int, c2 int, unique (c1,c2));
create table c (c1 int, c2 int, r double precision);
alter table c add constraint fk99999 foreign key (c1,c2) references p (c1,c2);
create function update_c() returns trigger language plpgsql as $$
BEGIN
update c set c2=null where c1=old.c1 and c2=old.c2;
return old;
END; $$;
create trigger sdjflsfd before delete ON p for each row execute function update_c();

How to solve deadlock when inserting rows in sql many-to-many relationship with minimum cardinality one restriction?

This year I've been learning about relational databases and how to design them. In order to strenghten my knowledge, I'm trying to design and implement a database using Python and sqlite3.
The database is about a textile company, and, among other thigs, they want to keep information about the following:
Materials they use to make their products
Shops where they look for materials
Some shops (the ones where they do buy materials) are considered suppliers.
They want to know what suppliers provide what materials
About this last relationship, there are some restrictions:
A supplier can provide more than one material (Supplier class maximum cardinality many)
A material can be provided by more than one supplier (Material class maximum carindality many)
All materials must be provided by at least one supplier (Material class minimum cardinality one)
All suppliers must provide at least one material (Supplier class minimum cardinality one)
This is how I think the ER diagram looks giving these indications:
Entity-Relation diagram for "Provides" relationship
Given the minimum cardinality one, I think I have to implement integrity restrictions by triggers. This is how I think the logic design (the actual tables in the database) looks:
Logical diagram for "Provides" relationship
With the following integrity restrictions:
IR1. Minimum cardinality one in Material-Provides: every value of the 'cod_material' attribute from the Material table must appear at least once as a value of the 'cod_material' attribute in the Provides table.
IR2. Minimum cardinality one in Supplier-Provides: every value of the 'cod_supplier' attribute from the Supplier table must appear at least once as a value of the 'cod_supplier' attribute in the Provides table.
All of this means that, when inserting new suppliers or materials, I will also have to insert what material they provided (in the case of the suppliers) or what supplier has provided it (in the case of the materials).
This is what the triggers I made to take into consideration the integrity restrictions look like (I should also add that I've been working with pl-sql, and sqlite uses sql, so I'm not that used to this syntax, and there may be some errors):
CREATE TRIGGER IF NOT EXISTS check_mult_provides_supl
AFTER INSERT ON Supplier
BEGIN
SELECT
CASE WHEN ((SELECT p.cod_supplier FROM Provides p WHERE p.cod_supplier = new.cod_supplier) IS NULL)
THEN RAISE(ABORT, 'Esta tienda no ha provisto aun ningun material')
END;
END;
CREATE TRIGGER IF NOT EXISTS check_mult_provides_mat
AFTER INSERT ON Material
BEGIN
SELECT
CASE WHEN ((SELECT m.cod_material FROM Material m WHERE m.cod_material = new.cod_material) IS NULL)
THEN RAISE(ABORT, 'Este material no ha sido provisto por nadie')
END;
END;
I've tried adding new rows to the tables Material and Supplier respectively, and the triggers are working (or at least they're not allowing me to insert new rows without a row in the Provides table).
This is when I reach the deadlock:
Having the database empty, if I try to insert a row in the tables Material or Supplier the triggers fire and they don't allow me (because first I need to insert the corresponding row in the table Provides). However, if I try to insert a row in the Provides table, I get a foreign key constraint error (obviously, since that supplier and material are not inserted into their respective tables yet), so basically I cannot insert rows in my database.
The only answers I can think of are not very satisfactory: momentary disabling any constraint (either the foreign key constraint or the integrity one by the trigger) puts the database integrity at risk, since new inserted rows don't fire the trigger even if this one gets enabled after. The other thing I thought of was relaxing the minimum cardinality restrictions, but I assume a many-to-many relationship with minimum cardinality one restriction should be usual in real databases, so there must be another kind of solutions.
How can I get out of this deadlock? Maybe a procedure (although sqlite doesn't have store procedures, I think I can make them with the Python API by create_function() in the sqlite3 module) would do the trick?
Just in case, if anyone wants to reproduce this part of the database, here is the code for the creation of the tables (I finally decided to autoincrement the primary key, so the datatype is an integer, as opposed to the ER diagram and the logical diagram which said a datatype character)
CREATE TABLE IF NOT EXISTS Material (
cod_material integer AUTO_INCREMENT PRIMARY KEY,
descriptive_name varchar(100) NOT NULL,
cost_price float NOT NULL
);
CREATE TABLE IF NOT EXISTS Shop (
cod_shop integer AUTO_INCREMENT PRIMARY KEY,
name varchar(100) NOT NULL,
web varchar(100) NOT NULL,
phone_number varchar(12),
mail varchar(100),
address varchar(100)
);
CREATE TABLE IF NOT EXISTS Supplier (
cod_proveedor integer PRIMARY KEY CONSTRAINT FK_Supplier_Shop REFERENCES Shop(cod_shop)
);
CREATE TABLE IF NOT EXISTS Provides (
cod_material integer CONSTRAINT FK_Provides_Material REFERENCES Material(cod_material),
cod_supplier integer CONSTRAINT FK_Provides_Supplier REFERENCES Supplier(cod_supplier),
CONSTRAINT PK_Provides PRIMARY KEY (cod_material, cod_supplier)
);
I believe that you want a DEFERRED FOREIGN KEY. The triggers, however, will interfere as they would be triggered.
However, you also need to consider the code that you have posted. There is no AUTO_INCREMENT keyword it is AUTOINCREMENT (however you very probably do not do not need AUTOINCREMENT as INTEGER PRIMARY KEY will do all that you required).
If you check SQLite AUTOINCREMENT along with
The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed. It is usually not needed.
The Supplier table is useless as you have coded it is simply a single column that references a shop with no other data. However, the Provides table references the Supplier table BUT to a non-existent column (cod_supplier).
Coding CONSTRAINT name REFERENCES table(column(s)) doesn't adhere to the SYNTAX as CONSTRAINT is a table level clause, whilst REFERENCES is a column level clause and this appears to cause some confusion.
I suspect that you may have resorted to Triggers because the FK conflicts weren't doing anything. By default FK processing is turned off and has to be enabled as per Enabling Foreign Key Support. I don't believe they are required.
Anyway I believe that the following, that includes changes to overcome the above issues, demonstrates DEFERREED FOREIGN KEYS :-
DROP TABLE IF EXISTS Provides;
DROP TABLE IF EXISTS Supplier;
DROP TABLE IF EXISTS Shop;
DROP TABLE IF EXISTS Material;
DROP TRIGGER IF EXISTS check_mult_provides_supl;
DROP TRIGGER IF EXISTS check_mult_provides_mat;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS Material (
cod_material integer PRIMARY KEY,
descriptive_name varchar(100) NOT NULL,
cost_price float NOT NULL
);
CREATE TABLE IF NOT EXISTS Shop (
cod_shop integer PRIMARY KEY,
name varchar(100) NOT NULL,
web varchar(100) NOT NULL,
phone_number varchar(12),
mail varchar(100),
address varchar(100)
);
CREATE TABLE IF NOT EXISTS Supplier (
cod_supplier INTEGER PRIMARY KEY, cod_proveedor integer /*PRIMARY KEY*/ REFERENCES Shop(cod_shop) DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE IF NOT EXISTS Provides (
cod_material integer REFERENCES Material(cod_material) DEFERRABLE INITIALLY DEFERRED,
cod_supplier integer REFERENCES Supplier(cod_supplier) DEFERRABLE INITIALLY DEFERRED,
PRIMARY KEY (cod_material, cod_supplier)
);
/*
CREATE TRIGGER IF NOT EXISTS check_mult_provides_supl
AFTER INSERT ON Supplier
BEGIN
SELECT
CASE WHEN ((SELECT p.cod_supplier FROM Provides p WHERE p.cod_supplier = new.cod_supplier) IS NULL)
THEN RAISE(ABORT, 'Esta tienda no ha provisto aun ningun material')
END;
END;
CREATE TRIGGER IF NOT EXISTS check_mult_provides_mat
AFTER INSERT ON Material
BEGIN
SELECT
CASE WHEN ((SELECT m.cod_material FROM Material m WHERE m.cod_material = new.cod_material) IS NULL)
THEN RAISE(ABORT, 'Este material no ha sido provisto por nadie')
END;
END;
*/
-- END TRANSACTION; need to use this if it fails before getting to commit
BEGIN TRANSACTION;
INSERT INTO Shop (name,web,phone_number,mail,address)VALUES('shop1','www.shop1.com','000000000000','shop1#email.com','1 Somewhere Street, SomeTown etc');
INSERT INTO Supplier (cod_proveedor) VALUES((SELECT max(cod_shop) FROM Shop));
INSERT INTO Material (descriptive_name,cost_price)VALUES('cotton',10.5);
INSERT INTO Provides VALUES((SELECT max(cod_material) FROM Material),(SELECT max(cod_supplier) FROM Supplier ));
COMMIT;
SELECT * FROM shop
JOIN Supplier ON Shop.cod_shop = cod_proveedor
JOIN Provides ON Provides.cod_supplier = Supplier.cod_supplier
JOIN Material ON Provides.cod_material = Material.cod_material
;
DROP TABLE IF EXISTS Provides;
DROP TABLE IF EXISTS Supplier;
DROP TABLE IF EXISTS Shop;
DROP TABLE IF EXISTS Material;
DROP TRIGGER IF EXISTS check_mult_provides_supl;
DROP TRIGGER IF EXISTS check_mult_provides_mat;
When run as is then the result is :-
However, if the INSERT into the Supplier is altered to :-
INSERT INTO Supplier (cod_proveedor) VALUES((SELECT max(cod_shop) + 1 FROM Shop));
i.e. the reference to the shop is not an existing shop (1 greater) then :-
The messages/log are :-
BEGIN TRANSACTION
> OK
> Time: 0s
INSERT INTO Shop (name,web,phone_number,mail,address)VALUES('shop1','www.shop1.com','000000000000','shop1#email.com','1 Somewhere Street, SomeTown etc')
> Affected rows: 1
> Time: 0.002s
INSERT INTO Supplier (cod_proveedor) VALUES((SELECT max(cod_shop) + 1 FROM Shop))
> Affected rows: 1
> Time: 0s
INSERT INTO Material (descriptive_name,cost_price)VALUES('cotton',10.5)
> Affected rows: 1
> Time: 0s
INSERT INTO Provides VALUES((SELECT max(cod_material) FROM Material),(SELECT max(cod_supplier) FROM Supplier ))
> Affected rows: 1
> Time: 0s
COMMIT
> FOREIGN KEY constraint failed
> Time: 0s
That is the deferred inserts were successful BUT the commit failed.
You may wish to refer to SQLite Transaction
I think the design of your database should be reconsidered, since the table Provides represents two different set of informations: which shop provides which materials, and which is the supplier for a certain material. A better design should be to separate those two kind of information, so that you can increase the constraints expressed through foreign keys.
Here is a sketch of the tables, not tied to a specific RDBMS.
Material (cod_material, descriptive_name, cost_price)
PK (cod_material)
Shop (cod_shop, name, web. phone_number, mail, address)
PK (cod_shop)
ShopMaterial (cod_shop, cod_material)
PK (cod_shop, cod_material),
cod_shop FK for Shop, cod_material FK for Material
SupplierMaterial (cod_sup, cod_material)
PK (cod_sup, cod_material)
cod_sup FK for Shop, cod_material FK for material
(cod_sup, cod_material) FK for ShopMaterial
The different foreign keys already take into account several constraints. The only constraint not enforced is, I think:
All materials must be provided by at least one supplier
This constraint cannot be enforced automatically since you have first to insert a material, then to add the corresponding pairs (cod_shop, cod_material) and then the pairs (cod_sup, cod_material). For this, I think the best option is to define, at the application level, a procedure that insert at the same time the material, the shops from which it can be obtained, and the supplier for it, as well as a procedure that remove the material, and the relevant pairs in ShopMaterial and SupplierMaterial tables.

Check constraint depending on another table (mutually exclusive relation)

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!
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:
create a booking;
check-in a booking;
annul a booking;
etc.
db<>fiddle
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.

oracle (or any relational) data model question. Parent with fixed number of Childeren?

This is a particular problem that I have come across many times,
but I have never really found a simple solution to this (seemingly) simple problem.
How to you ensure that a given parent has a fixed number of children?
1) Example.
How do you make sure a given class has only , say, 50 students enrolled..?
create table class(
class_id number primary key,
class_name varchar2(50),
class_attributes varchar2(50)
);
create table student(
student_id number primary key,
student_name varchar2(50),
student_attributes varchar2(50)
);
create table class_student_asc(
class_id number,
student_id number,
other_attributes varchar2(50),
constraint pk_class_student_asc primary key (class_id,student_id),
constraint fk_class_id foreign key (class_id) references class(class_id),
constraint fk_student_id foreign key (student_id) references student(student_id)
);
These are the implementations that I know of.
Let me know which one you'd prefer and if there is a simpler way to achieve this.
a)
Implementing it with triggers on the child table (class_student_asc).
Querying the same table in a before insert, update trigger to get the count.
Since this gives the mutating table error, this is split into two different statement-level
triggers (before-statement and after-statement) to achieve the result..
http://asktom.oracle.com/pls/asktom/ASKTOM.download_file?p_file=6551198119097816936
b)
Include a count variable in the class table and lock the parent record for update before inserting a record ito the child table.
So, something like..
create table class(
class_id number primary key,
class_name varchar2(50),
class_attributes varchar2(50),
class_count INTEGER,
constraint chk_count_Students check (class_count <=5)
);
and instead of exposing the table class_student_asc for inserts and so on...
write a procedure and then use it in all applications..
procedure assign_new_student(
i_student_id number,
i_class_id number)
is
begin
select class_count
from class
where class_id = i_class_id
for update ; -- or for update nowait, if you want the other concurrent transaction to fail..
insert into class_student_asc(
class_id, student_id)
values (i_class_id,i_student_id);
update class
set class_count = class_count + 1
where class_id = i_class_id;
commit;
end assign_new_student;
c)
There are, of course, cases like a user having two email adresses.
In such a scenario, the email address itself does not have any attribute
and the table could be as simple as
create table user_table
(
user_id number,
user_name varchar2(50),
user_email_primary varchar2(50),
user_email_secondary varchar2(50)
);
However, we cannot extend the same approach for the question above.....as the number of columns and the constraint checks would slow down the inserts and updates . Also, this would mean we'd need a new column added everytime we change the rule.. too.
Please advice.
For Oracle consider this approach.
Create a materialized view summarising the number of students per class. Have the mview refresh on commit and add a constraint to the mview that prohibits a count of more than 50 students per class.
This code demonstrates how to use a fast refresh on commit mview to enforce the student count limit,
insert into class(class_id, class_name) values (1, 'Constraints 101');
insert into class(class_id, class_name) values (2, 'Constraints 201');
insert into student(student_id, student_name) values(1, 'Alice');
insert into student(student_id, student_name) values(2, 'Bob');
insert into student(student_id, student_name) values(3, 'Carlos');
create materialized view log on class_student_asc with primary key, rowid, sequence including new values;
create materialized view class_limit refresh fast on commit as
select class_id, count(*) count from class_student_asc group by class_id;
alter table class_limit add constraint class_limit_max check(count <= 2);
insert into class_student_asc(class_id, student_id) values(1, 1);
insert into class_student_asc(class_id, student_id) values(1, 2);
insert into class_student_asc(class_id, student_id) values(1, 3);
The constraint will be violated when the transaction is committed, not when the third student is added to the class. This might make a difference to your application code. SQL Developer fails to display the error but sql*plus does display it.
I can think of a couple of ways:
1. Triggers
Have an INSERT Trigger on the table that checks on INSERT and does the validation for you.
2. One-to-One relationships
Let's say you want one parent to have only two children. Create two one-to-one relationships.
Another question had a similar requirement, which you can constrain using a combination of a CHECK constraint with a UNIQUE constraint on a "count" column:
How to fastly select data from Oracle