How to create an Oracle audit trigger? - sql

CREATE OR REPLACE TRIGGER EVALUATION
BEFORE INSERT OR UPDATE OR DELETE ON BOOKING
FOR EACH ROW
DECLARE
BEGIN
SELECT BOOKING_EVALUATION FROM BOOKING WHERE BOOKING_EVALUATION > 2;
SELECT BOOKING_EVALUATION INTO EVALUATIONAUDIT FROM BOOKING;
IF INSERTING THEN
INSERT INTO EVALUATIONAUDIT (VOYAGES_ID,CUSTOMER_NAME,START_DATE,SHIP_NAME,BOOKING_EVALUATION)
VALUES(:NEW.VOYAGES_ID,:NEW.CUSTOMER_NAME,:NEW.START_DATE,:NEW.SHIP_NAME,:NEW.BOOKING_EVALUATION);
END IF;
IF UPDATING THEN
INSERT INTO EVALUATIONAUDIT (VOYAGES_ID,CUSTOMER_NAME,START_DATE,SHIP_NAME,BOOKING_EVALUATION)
VALUES(:OLD.VOYAGES_ID,:OLD.CUSTOMER_NAME,:OLD.START_DATE,:OLD.SHIP_NAME,:OLD.BOOKING_EVALUATION);
END IF;
IF DELETING THEN
INSERT INTO EVALUATIONAUDIT (VOYAGES_ID,CUSTOMER_NAME,START_DATE,SHIP_NAME,BOOKING_EVALUATION)
VALUES(:OLD.VOYAGES_ID,:OLD.CUSTOMER_NAME,:OLD.START_DATE,:OLD.SHIP_NAME,:OLD.BOOKING_EVALUATION);
END IF;
END;
This is my audit table:
desc evaluationaudit;
Name Null? Type
----------------------------------------- -------- -----------------------
AUDITT_ID NOT NULL NUMBER(10)
VOYAGES_ID NOT NULL NUMBER(10)
CUSTOMER_NAME NOT NULL VARCHAR2(20)
START_DATE NOT NULL DATE
SHIP_NAME NOT NULL VARCHAR2(20)
BOOKING_EVALUATION NOT NULL NUMBER(20)
and this is my show error output:
SQL> SHOW ERRORS;
Errors for TRIGGER EVALUATION:
LINE/COL ERROR
-------- -------------------------------------------------------------
12/24 PLS-00049: bad bind variable 'NEW.CUSTOMER_NAME'
12/43 PLS-00049: bad bind variable 'NEW.START_DATE'
12/59 PLS-00049: bad bind variable 'NEW.SHIP_NAME'
18/24 PLS-00049: bad bind variable 'OLD.CUSTOMER_NAME'
18/43 PLS-00049: bad bind variable 'OLD.START_DATE'
18/59 PLS-00049: bad bind variable 'OLD.SHIP_NAME'
24/24 PLS-00049: bad bind variable 'OLD.CUSTOMER_NAME'
24/43 PLS-00049: bad bind variable 'OLD.START_DATE'
24/59 PLS-00049: bad bind variable 'OLD.SHIP_NAME'
I wanted to add to the audit table. If a customer gives a poor evaluation of 2 or less, the details of their voyage (customer_name, name and date of the cruise, ship name and evaluation) but I'm getting error
Warning: Trigger created with compilation errors.
And the audit table is empty, showing no rows selected. I can't seem to find the problem where I am going wrong.

Maybe the following code snippets will help you. Suppose we have 2 tables: BOOKING, and EVALUATION_AUDIT (everything written in uppercase letters is taken from the code in your question).
Tables
create table booking (
VOYAGES_ID number primary key
, CUSTOMER_NAME VARCHAR2(20) NOT NULL
, START_DATE DATE NOT NULL
, SHIP_NAME VARCHAR2(20) NOT NULL
, BOOKING_EVALUATION NUMBER(20) NOT NULL
) ;
create table evaluationaudit (
AUDITT_ID number generated always as identity start with 5000 primary key
, trg_cond_pred varchar2( 64 ) default 'Row not added by evaluation trigger!'
, VOYAGES_ID NUMBER(10) NOT NULL
, CUSTOMER_NAME VARCHAR2(20) NOT NULL
, START_DATE DATE NOT NULL
, SHIP_NAME VARCHAR2(20) NOT NULL
, BOOKING_EVALUATION NUMBER(20) NOT NULL
) ;
Trigger
-- "updating" and "deleting" code omitted for clarity
CREATE OR REPLACE TRIGGER EVALUATION_trigger
BEFORE INSERT OR UPDATE OR DELETE ON BOOKING
FOR EACH ROW
BEGIN
case
when INSERTING then
if :new.booking_evaluation <= 2 then
INSERT INTO EVALUATIONAUDIT
( trg_cond_pred,
VOYAGES_ID, CUSTOMER_NAME, START_DATE, SHIP_NAME, BOOKING_EVALUATION )
VALUES (
'INSERTING'
, :NEW.VOYAGES_ID
, :NEW.CUSTOMER_NAME
, :NEW.START_DATE
, :NEW.SHIP_NAME
, :NEW.BOOKING_EVALUATION
);
end if ;
end case ;
END ;
/
Testing
One of your requirements (in your question) is:
I wanted to add to the audit table. If a customer gives a poor
evaluation of 2 or less, the details of their voyage (customer_name,
name and date of the cruise, ship name and evaluation)
delete from evaluationaudit ;
delete from booking ;
-- booking_evaluation greater than 2 -> no entry in audit table
insert into booking
( VOYAGES_ID, CUSTOMER_NAME, START_DATE, SHIP_NAME, BOOKING_EVALUATION )
values ( 1111, 'customer1', date '2018-05-24', 'ship1', 9999 ) ;
select * from evaluationaudit ;
-- no rows selected
-- booking_evalution = 2 -> insert a row into the audit table
insert into booking
( VOYAGES_ID, CUSTOMER_NAME, START_DATE, SHIP_NAME, BOOKING_EVALUATION )
values ( 1112, 'customer1', date '2018-05-24', 'ship1', 2 ) ;
select * from evaluationaudit ;
AUDITT_ID TRG_COND_PRED VOYAGES_ID CUSTOMER_NAME START_DATE SHIP_NAME BOOKING_EVALUATION
5000 INSERTING 1112 customer1 24-MAY-18 ship1 2
__Update__
If - as you wrote in your comment - you need to pull in some more data from other tables, maybe you want to try the following approach: keep the trigger code rather brief, and use it to call a procedure for the more complicated stuff eg
Tables
create table evaluationaudit (
AUDITT_ID number generated always as identity start with 7000 primary key
, trg_cond_pred varchar2( 64 ) default 'Row not added by evaluation trigger!'
, VOYAGES_ID NUMBER NOT NULL
, CUSTOMER_NAME VARCHAR2(20) NOT NULL
, SHIP_NAME VARCHAR2(20) NOT NULL
, BOOKING_EVALUATION NUMBER(20) NOT NULL
) ;
create table ships ( name varchar2( 64 ), id number unique ) ;
create table customers ( name varchar2( 64 ), id number unique ) ;
insert into ships ( name, id ) values ( 'ship1', 501 );
insert into ships ( name, id ) values ( 'ship2', 502 );
insert into ships ( name, id ) values ( 'ship3', 503 );
insert into customers ( name, id ) values ( 'customer1', 771 ) ;
insert into customers ( name, id ) values ( 'customer2', 772 ) ;
insert into customers ( name, id ) values ( 'customer3', 773 ) ;
create table bookings (
id number generated always as identity start with 5000 primary key
, voyagesid number
, shipid number
, customerid number
, evaluation number
, bookingdate date
);
Trigger
create or replace trigger bookingeval
before insert on bookings
for each row
when ( new.evaluation <= 2 ) -- Use NEW without colon here! ( see documentation )
begin
auditproc( :new.voyagesid, :new.customerid, :new.shipid, :new.evaluation ) ;
end ;
/
Procedure
create or replace procedure auditproc (
voyagesid_ number
, customerid_ number
, shipid_ number
, evaluation_ number
)
as
customername varchar2( 64 ) := '' ;
shipname varchar2( 64 ) := '' ;
begin
-- need to find the customername and shipname before INSERT
select name into customername from customers where id = customerid_ ;
select name into shipname from ships where id = shipid_ ;
insert into evaluationaudit
( trg_cond_pred,
voyages_id, customer_name, ship_name, booking_evaluation )
values (
'INSERTING'
, voyagesid_
, customername
, shipname
, evaluation_
);
end ;
/
Testing
-- evaluation > 2 -> no INSERT into evaluationaudit
insert into bookings
( voyagesid, customerid, shipid, evaluation, bookingdate )
values ( 1111, 771, 501, 9999, sysdate ) ;
select * from evaluationaudit ;
-- no rows selected
-- evaluation = 2
-- -> trigger calling audit procedure -> inserts into evaluationaudit
insert into bookings
( voyagesid, customerid, shipid, evaluation, bookingdate )
values ( 1112, 772, 502, 2, sysdate ) ;
select * from evaluationaudit ;
AUDITT_ID TRG_COND_PRED VOYAGES_ID CUSTOMER_NAME SHIP_NAME BOOKING_EVALUATION
7000 INSERTING 1112 customer2 ship2 2

Related

Create trigger to update balance after transactions from an account and to an account - SQL Oracle

Could someone help me here? I'm trying to create a trigger that updates balance in ACCOUNT table after transactions from the TRANSFERS table. The whole table model is bigger, but here are two of them:
--ACCOUNT--
CREATE TABLE account(
account_num NUMBER(8) NOT NULL PRIMARY KEY,
ktnr REFERENCES account_type(ktnr),
regdate DATE NOT NULL,
balance NUMBER(10,2));
--TRANSFERS--
CREATE TABLE transfers(
row_nr NUMBER(9) NOT NULL PRIMARY KEY,
pers_nr REFERENCES customer(pers_nr),
from_account_num REFERENCES account(account_num),
to_account_num REFERENCES account(account_num),
amount NUMBER(10,2),
date DATE NOT NULL);
And the code I think is quite wrong but that's the idea of solution. Not sure how to write it in a better way.
create or replace trigger aifer_transfers
after insert
on transfers
for each row
when (new.amount is not null)
begin
if :new.to_account_num != :new.pers_nr then
update account a
set a.balance = a.balance - :new.amount
where :old.account_num = :new:account_num;
elsif
:new.to_account_num = :new.pers_nr then
update account a
set a.balance = a.balance + :new.amount
where :old.account_num = :new:account_num;
end if;
end;
Many different errors, depending on chosen variables:
Line/Col: 3/9 PL/SQL: SQL Statement ignored
Line/Col: 5/26 PLS-00049: bad bind variable 'NEW'
Your table declarations are missing some data types (since we don't have your other tables) and, from Oracle 12, you can use IDENTITY columns for the primary keys (and have some CHECK constraints and DEFAULT values for DATE columns):
CREATE TABLE account(
account_num NUMBER(8,0)
GENERATED ALWAYS AS IDENTITY
NOT NULL
CONSTRAINT account__account_num__pk PRIMARY KEY,
ktnr NUMBER(8,0)
-- CONSTRAINT account__ktnr__fk REFERENCES account_type(ktnr)
,
regdate DATE
DEFAULT SYSDATE
NOT NULL,
balance NUMBER(10,2)
NOT NULL
);
CREATE TABLE transfers(
row_nr NUMBER(9)
GENERATED ALWAYS AS IDENTITY
NOT NULL
CONSTRAINT transfers__row_nr__pk PRIMARY KEY,
pers_nr NUMBER(8,0)
NOT NULL
-- CONSTRAINT transfers__pers_nr__fk REFERENCES customer(pers_nr)
,
from_account_num NOT NULL
CONSTRAINT transfers_from__fk REFERENCES account(account_num),
to_account_num NOT NULL
CONSTRAINT transfers__to__fk REFERENCES account(account_num),
amount NUMBER(10,2)
NOT NULL
CONSTRAINT transfers__amount_chk CHECK ( amount > 0 ),
datetime DATE
DEFAULT SYSDATE
NOT NULL,
CONSTRAINT transfers__from_ne_to__chk CHECK ( from_account_num != to_account_num )
);
Then you can create the trigger:
CREATE TRIGGER aifer_transfers
AFTER INSERT ON TRANSFERS
FOR EACH ROW
BEGIN
UPDATE account
SET balance = CASE account_num
WHEN :NEW.from_account_num
THEN balance - :NEW.amount
WHEN :NEW.to_account_num
THEN balance + :NEW.amount
ELSE balance
END
WHERE account_num IN ( :new.from_account_num, :new.to_account_num );
END;
/
If you create 3 accounts:
INSERT INTO account ( ktnr, balance ) VALUES ( 0, 100 );
INSERT INTO account ( ktnr, balance ) VALUES ( 0, 100 );
INSERT INTO account ( ktnr, balance ) VALUES ( 0, 100 );
Then after:
INSERT INTO transfers (
pers_nr, from_account_num, to_account_num, amount
) VALUES (
0, 1, 2, 20
);
Then:
SELECT * FROM account;
Outputs:
ACCOUNT_NUM
KTNR
REGDATE
BALANCE
1
0
18-MAY-21
80
2
0
18-MAY-21
120
3
0
18-MAY-21
100
Then after:
INSERT INTO transfers (
pers_nr, from_account_num, to_account_num, amount
) VALUES (
0, 1, 3, 15
);
It would be:
ACCOUNT_NUM
KTNR
REGDATE
BALANCE
1
0
18-MAY-21
65
2
0
18-MAY-21
120
3
0
18-MAY-21
115
Then after:
INSERT INTO transfers ( pers_nr, from_account_num, to_account_num, amount )
SELECT 0, 3, 2, 10 FROM DUAL UNION ALL
SELECT 0, 2, 3, 20 FROM DUAL UNION ALL
SELECT 0, 1, 2, 10 FROM DUAL;
It would be:
ACCOUNT_NUM
KTNR
REGDATE
BALANCE
1
0
18-MAY-21
55
2
0
18-MAY-21
120
3
0
18-MAY-21
125
db<>fiddle here
Update
For your tables, without any additional CHECK constraints, you may want:
CREATE TRIGGER aifer_transfers
AFTER INSERT ON TRANSFERS
FOR EACH ROW
BEGIN
IF :NEW.amount <= 0 THEN
RAISE_APPLICATION_ERROR( -20000, 'Amount must be above zero.' );
END IF;
IF :NEW.from_account_num IS NULL THEN
RAISE_APPLICATION_ERROR( -20000, 'From account must be non-null.' );
END IF;
IF :NEW.to_account_num IS NULL THEN
RAISE_APPLICATION_ERROR( -20000, 'To account must be non-null.' );
END IF;
IF :NEW.from_account_num = :NEW.to_account_num THEN
RAISE_APPLICATION_ERROR( -20000, 'Accounts must be different.' );
END IF;
UPDATE account
SET balance = CASE account_num
WHEN :NEW.from_account_num
THEN balance - :NEW.amount
WHEN :NEW.to_account_num
THEN balance + :NEW.amount
ELSE balance
END
WHERE account_num IN ( :new.from_account_num, :new.to_account_num );
END;
/
(But that validation should all be implemented as CHECK constraints on the transfers table rather than in a trigger.)
db<>fiddle here

How to insert data into table which has a ref and scope

This is my user defined Type which i have created.
create or Replace TYPE cust_address_typ_new AS OBJECT
( add_id NUMBER,
street_address VARCHAR2(40)
, postal_code VARCHAR2(10)
, city VARCHAR2(30)
, state_province VARCHAR2(10)
, country_id CHAR(2)
);
and the below is the table of new type
CREATE TABLE address_table OF cust_address_typ_new;
Now i created another table as below
CREATE TABLE customer_addresses (
add_id NUMBER,
address REF cust_address_typ_new
SCOPE IS address_table);
and now i'm trying to insert values into customer_addresses table
insert into customer_addresses
values(1,SYSTEM.CUST_ADDRESS_TYP_NEW(1,'hi','87987','city','state',''))
Using sqlcl (Oracle 12.1):
create or Replace TYPE cust_address_typ_new AS OBJECT (
add_id NUMBER
, street_address VARCHAR2(40)
, postal_code VARCHAR2(10)
, city VARCHAR2(30)
, state_province VARCHAR2(10)
, country_id CHAR(2)
);
/
-- Type CUST_ADDRESS_TYP_NEW compiled
CREATE TABLE address_table OF cust_address_typ_new ;
-- Table ADDRESS_TABLE created.
CREATE TABLE customer_addresses (
add_id NUMBER
, address REF cust_address_typ_new SCOPE IS address_table
);
-- Table CUSTOMER_ADDRESSES created.
Errors
insert into customer_addresses
values ( 1, SYSTEM.CUST_ADDRESS_TYP_NEW(1,'hi','87987','city','state','') ) ;
-- ORA-00904: "SYSTEM"."CUST_ADDRESS_TYP_NEW": invalid identifier
insert into customer_addresses
values ( 1, CUST_ADDRESS_TYP_NEW( 1,'hi','87987','city','state','' ) ) ;
-- ORA-00932: inconsistent datatypes: expected REF ...CUST_ADDRESS_TYP_NEW got ...CUST_ADDRESS_TYP_NEW
From the documentation:
REF takes as its argument a correlation variable (table alias)
associated with a row of an object table or an object view. A REF
value is returned for the object instance that is bound to the
variable or row.
The following examples my help you:
-- {1} insert into the address_table ( 3 examples )
insert into address_table
values ( 1,'hi','87987','city','state','' ) ;
-- 1 row inserted.
insert into address_table
values( cust_address_typ_new( 2,'hi again','12345','city2','state2','' ) ) ;
-- 1 row inserted
insert into address_table
values( new cust_address_typ_new( 3,'hmpf','23456','city3','state3','' ) ) ;
-- 1 row inserted.
-- result
SQL> select * from address_table;
ADD_ID STREET_ADDRESS POSTAL_CODE CITY STATE_PROVINCE COUNTRY_ID
1 hi 87987 city state NULL
2 hi again 12345 city2 state2 NULL
3 hmpf 23456 city3 state3 NULL
Maybe you don't need the sequence (just added to fill the add_id column in the customer_addresses table).
-- {2} select from the address_table, use ref(),
-- insert into customer_addresses
SQL> create sequence ca_seq start with 1000 increment by 1;
Sequence CA_SEQ created.
insert into customer_addresses
select ca_seq.nextval, ref(AT) from address_table AT ;
-- result
select * from customer_addresses;
ADD_ID
----------
ADDRESS
------------------------------------------------------------------------
1000
2202086FC2AC912CD41038E0530100007F525D6FC2AC912CCD1038E0530100007F525D
1001
2202086FC2AC912CD51038E0530100007F525D6FC2AC912CCD1038E0530100007F525D
1002
2202086FC2AC912CD61038E0530100007F525D6FC2AC912CCD1038E0530100007F525D

How to insert foreign key from pre-generated table?

I have 3 tables:
create table customer
(
customer_id integer primary key,
customer_first_name varchar2(50) not null,
customer_surrname varchar2(50) not null,
phone_number varchar2(15) not null,
customer_details varchar2(200) default 'There is no special notes'
);
create table place
(
table_number integer primary key,
table_details varchar2(200) default 'There is no details'
);
create table booking
(
booking_id integer primary key,
date_of_booking date,
number_of_persons number(2) not null,
customer_id integer not null,
foreign key(customer_id) references customer(customer_id),
table_number integer not null,
foreign key(table_number) references place(table_number)
);
I have to generate customer table using this kind of generator:
set SERVEROUTPUT on format wrapped;
set define off;
drop sequence customer_seq;
drop sequence place_seq;
--CUSTOMER TABLE INSERT ROW GENERATOR
create sequence customer_seq START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE OR REPLACE TRIGGER customer_id_trigger
BEFORE INSERT ON customer
FOR EACH ROW
BEGIN
SELECT customer_seq.nextval INTO :new.customer_id FROM dual;
END;
/
DELETE FROM customer;
DECLARE
TYPE TABSTR IS TABLE OF VARCHAR2(250);
first_name TABSTR;
surrname TABSTR;
qname number(5);
phonenum number(15);
details TABSTR;
BEGIN
first_name := TABSTR ('Jhon','Poul','Jesica','Arnold','Max','Teemo','Tim','Mikel','Michael',
'Kristian','Adela','Mari','Anastasia','Robert','Jim','Juana','Adward',
'Jana','Ola','Kristine','Natali','Corey','Chester','Naomi','Chin-Chou');
surrname := TABSTR ('Grey','Brown','Robins','Chiho','Lee','Das','Edwins','Porter','Potter',
'Dali','Jordan','Jordison','Fox','Washington','Bal','Pitney','Komarowski',
'Banks','Albra','Shwiger');
details := TABSTR ('Exellent Customer','Good Customer','Always drunked','Left big tips',
'Bad Customer','Did not pay last bill','New Customer','VIP client');
qname := 100; — CHANGE THIS TO MANAGE HOW MANY ROWS YOU WANT TO BE ADDED
FOR i IN 1..qname LOOP
phonenum := dbms_random.value(111111111,999999999);
INSERT INTO customer VALUES (NULL, first_name(dbms_random.value(1,25)),
surrname(dbms_random.value(1,20)), phonenum, details(dbms_random.value(1,8)));
END LOOP;
DBMS_OUTPUT.put_line('Customers done!');
END;
/
--TABLE INSERT
DELETE FROM place;
create sequence place_seq start with 1 increment by 1;
insert into place values (place_seq.nextval, 'Near the window');
insert into place values (place_seq.nextval, default);
insert into place values (place_seq.nextval, 'Near the door');
insert into place values (place_seq.nextval, 'Near the window');
insert into place values (place_seq.nextval, 'Near the window');
insert into place values (place_seq.nextval, default);
insert into place values (place_seq.nextval, 'Near the door');
insert into place values (place_seq.nextval, 'Big table');
insert into place values (place_seq.nextval, default);
insert into place values (place_seq.nextval, 'Big table');
So the question is how can I insert client_id in "booking" table which have one of the numbers in "customers" table? Because every time I regenerate data in "Customers" table numbers are changing so I should somehow select numbers in an array and then randomly choose one of them from this array. The thing is I don't really know how to select from table to array. Can anybody help?
For PL/SQL version you can use BULK COLLECT and standard sys.odcinumberlist array.
create sequence booking_seq start with 1 increment by 1;
declare
customerIds sys.odcinumberlist;
placeIds sys.odcinumberlist;
number_of_generated_records number := 150; -- number of records to be generated
begin
-- fill the array of customer_id values
select customer_id
bulk collect into customerIds
from customer;
-- fill the array of place numbers
select table_number
bulk collect into placeIds
from place;
for i in 1..number_of_generated_records loop
insert into booking(booking_id,date_of_booking,number_of_persons,customer_id,table_number)
values(
booking_seq.nextval, -- booking_id
trunc(sysdate) + round(dbms_random.value(1,365)), -- date_of_booking
round(dbms_random.value(1,99)), -- number_of_persons
customerIds(round(dbms_random.value(1,customerIds.count))), -- customer_id
placeIds(round(dbms_random.value(1,placeIds.count))) -- table_number
);
end loop;
end;
But, for your case I would prefer pure sql:
insert into booking(booking_id,date_of_booking,number_of_persons,customer_id,table_number)
with
customer_subq as (
select customer_id, row_number() over (order by customer_id) rn from customer
),
place_subq as (
select table_number, row_number() over (order by table_number) rn from place
),
params as (
select 1500 number_of_generated_records,
(select count(1) from customer) customer_count,
(select count(1) from place) place_count
from dual
),
random_numbers as (
select round(dbms_random.value(1,1000)) random_number1,
round(dbms_random.value(1,1000)) random_number2,
round(dbms_random.value(1,1000)) random_number3,
round(dbms_random.value(1,1000)) random_number4
from dual,params
connect by level <= number_of_generated_records
)
select booking_seq.nextval booking_id,
trunc(sysdate) + mod(random_number1,365) date_of_booking,
mod(random_number1,100) number_of_persons,
customer_id,
table_number
from random_numbers,
params,
customer_subq,
place_subq
where mod(random_number1,customer_count) + 1 = customer_subq.rn
and mod(random_number2,place_count) + 1 = place_subq.rn

Check if ID of parent table's record is contained in just one of the child tables

To preface this, I have 3 tables, there is table
Product
- id
- name
- availability
Then is has 2 child tables:
Tables
- id
- product_id (foreign key to product(id))
- size
Chairs
- id
- product_id (foreign key to product(id))
- color
What I want to do is everytime I insert a new record into the chairs/tables table, I want to check whether it is not already contained in one of them. Or in other words, one product cannot be chair and table at once. How do I do this?
Thank you.
You could use a CHECK constraint for this, which, in combination with some other constraints, may give you the required behaviour. There are some restrictions on check constraints, one of them being:
The condition of a check constraint can refer to any column in the
table, but it cannot refer to columns of other tables.
( see the documentation )
In the following example, the ID columns "chairs.id" and "tables.id" have been moved into the "products" table, which contains the CHECK constraint. The UNIQUE constraints enforce a one-to-one relationship as it were ( allowing only one value for each of the REFERENCED ids ). The DDL code looks a bit busy, but here goes:
Tables
create table products (
id number generated always as identity primary key
, name varchar2(128)
, availability varchar2(32)
);
-- product_id removed
create table tables (
id number primary key
, size_ number
) ;
-- product_id removed
create table chairs (
id number primary key
, color varchar2(32)
);
Additional columns and constraints
alter table products
add (
table_id number unique
, chair_id number unique
, check (
( table_id is not null and chair_id is null )
or
( table_id is null and chair_id is not null )
)
);
alter table tables
add constraint fkey_table
foreign key ( id ) references products ( table_id ) ;
alter table chairs
add constraint fkey_chairs
foreign key ( id ) references products ( chair_id ) ;
Testing
-- {1} Add a chair: the chair_id must exist in PRODUCTS.
insert into chairs ( id, color ) values ( 1000, 'maroon' ) ;
-- ORA-02291: integrity constraint ... violated - parent key not found
-- Each chair needs an entry in PRODUCTS first:
insert into products ( name, availability, chair_id )
values ( 'this is a chair', 'in stock', 1000 ) ;
insert into chairs ( id, color ) values ( 1000, 'maroon' ) ;
-- okay
-- {2} We cannot add another chair that has the same chair_id. Good.
insert into products ( chair_id ) values ( 1000 ) ;
-- ORA-00001: unique constraint ... violated
-- {3} Add a table.
insert into products ( name, availability, table_id )
values ( 'this is a table', 'unavailable', 1000 ) ;
-- okay
insert into tables ( id, size_ ) values ( 1000, 60 ) ;
-- {4} Is it possible to add another table, with the same table_id? No. Good.
insert into tables ( id, size_ ) values ( 1000, 60 ) ;
-- ORA-00001: unique constraint ... violated
insert into products ( name, availability, table_id )
values ('this is a table', 'unavailable', 1000 ) ;
-- ORA-00001: unique constraint ... violated
-- {5} We cannot add something that is a chair _and_ a table (at the same time).
insert into products ( name, availability, table_id, chair_id )
values ( 'hybrid', 'awaiting delivery', 2000, 2000 ) ;
-- ORA-02290: check constraint ... violated
Resultset
SQL> select * from products;
ID NAME AVAILABILITY TABLE_ID CHAIR_ID
21 this is a chair in stock NULL 1000
23 this is a table unavailable 1000 NULL
NOTE: The product ID (NOT the table_id or the chair_id) "identifies" a particular table/chair. The values in the TABLE_ID and CHAIR_ID columns are only used to "link" to the (unique) IDs in the child table(s).
Alternative: object-relational approach.
This solution may be more suitable for solving the problem. Here, we create a supertype (product_t) first, and then 2 subtypes (chair_t and table_t, respectively). This allows us to create a table PRODUCTS_, using product_t for storing the data we need.
Types and table
create or replace type product_t as object (
name varchar2(64)
, availability varchar2(64)
)
not final;
/
create or replace type chair_t under product_t (
color varchar2(64)
)
/
create or replace type table_t under product_t (
size_ number
)
/
create table products_ (
id number generated always as identity primary key
, product product_t
);
Testing
-- "standard" INSERTs
begin
insert into products_ ( product )
values ( chair_t( 'this is a chair', 'in stock', 'maroon' ) );
insert into products_ ( product )
values ( table_t( 'this is a table', 'not available', 60 ) );
end;
/
-- unknown types cannot be inserted
insert into products_ ( product )
values ( unknown_t( 'type unknown!', 'not available', 999 ) );
-- ORA-00904: "UNKNOWN_T": invalid identifier
insert into products_ ( product )
values ( product_t( 'supertype', 'not available', 999 ) );
-- ORA-02315: incorrect number of arguments for default constructor
-- object of SUPERtype can be inserted
insert into products_ ( product )
values ( product_t( 'supertype', 'not available' ) );
-- 1 row inserted.
Query
select
id
, treat( product as table_t ).name as name_of_table
, treat( product as chair_t ).name as name_of_chair
, case
when treat( product as table_t ) is not null
then 'TABLE_T'
when treat( product as chair_t ) is not null
then 'CHAIR_T'
when treat( product as product_t ) is not null
then 'PRODUCT_T'
else
'TYPE unknown :-|'
end which_type_is_it
from products_ ;
-- result
ID NAME_OF_TABLE NAME_OF_CHAIR WHICH_TYPE_IS_IT
1 NULL this is a chair CHAIR_T
2 this is a table NULL TABLE_T
3 NULL NULL PRODUCT_T -- neither a chair nor a table ...

Integrity constraint for tables not immediately related

I am a SQL beginner and I can't figure out how to properly create an integrity constraint for situations like this:
The schema describes a delivery system - each Restaurant offers some items, which can be delivered to customers (outside the visible schema).
The problem comes with the in_delivery table - items from menu are registered with the delivery through this table. With the current state of things, it is possible to add a menu_item to a delivery which is done by a restaurant, but that restaurant may not offer the menu_item!
When inserting into in_delivery, I need to somehow check, if the Menu_Item_MenuItem_ID is present in offers, that has Restaurant_RestaurantID equal to RestaurantID in Delivery associated with the table.
I don't know if I can use a foreign key here, because the tables are not "adjacent"..
What comes into mind is to have a RestaurantID in in_delivery, that would be a foreign key both to Restaurant and Delivery. Then I could find that in offers. Is there a better way?
Thanks for your help
You could enforce your constraint with the following changes:
add the restaurant_id column in the in_delivery table
add a unique constraint on delivery (delivery_id, restaurant_id) (needed for 3.)
change the foreign key from in_delivery -> delivery to point to (delivery_id, restaurant_id)
change the foreign key from in_delivery -> menu_item to in_delivery -> offers
Alternatively you can use a trigger to check the constraint:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE Restaurants (
RestaurantID NUMBER(2) PRIMARY KEY,
Name VARCHAR2(30) NOT NULL
)
/
INSERT INTO Restaurants
SELECT 1, 'Soylent Green Express' FROM DUAL
UNION ALL SELECT 2, 'Helga''s House of Ribs' FROM DUAL
/
CREATE TABLE Menu_Items (
Menu_Item_ID NUMBER(2) PRIMARY KEY,
Name VARCHAR2(20) NOT NULL
)
/
INSERT INTO Menu_Items
SELECT 1, 'Soylent Green' FROM DUAL
UNION ALL SELECT 2, 'Ribs' FROM DUAL
/
CREATE TABLE Offers (
RestaurantID NUMBER(2),
Menu_Item_ID NUMBER(2),
PRIMARY KEY ( RestaurantID, Menu_Item_ID ),
FOREIGN KEY ( RestaurantID ) REFERENCES Restaurants ( RestaurantID ),
FOREIGN KEY ( Menu_Item_ID ) REFERENCES Menu_Items ( Menu_Item_ID )
)
/
INSERT INTO Offers
SELECT 1, 1 FROM DUAL
UNION ALL SELECT 2, 2 FROM DUAL
/
CREATE TABLE Deliveries (
RestaurantID NUMBER(2) NOT NULL,
Delivery_ID NUMBER(2) PRIMARY KEY,
FOREIGN KEY ( RestaurantID ) REFERENCES Restaurants ( RestaurantID )
)
/
INSERT INTO Deliveries
SELECT 1, 1 FROM DUAL
UNION ALL SELECT 2, 2 FROM DUAL
/
CREATE TABLE in_delivery (
Delivery_ID NUMBER(2),
Menu_Item_ID NUMBER(2),
PRIMARY KEY ( Delivery_ID, Menu_Item_ID ),
FOREIGN KEY ( Delivery_ID ) REFERENCES Deliveries ( Delivery_ID ),
FOREIGN KEY ( Menu_Item_ID ) REFERENCES Menu_Items ( Menu_Item_ID )
)
/
Just for ease of reading I've created two useful functions (you would probably want some exception handling in them):
CREATE OR REPLACE FUNCTION get_Delivery_RestaurantID (
p_Delivery_ID Deliveries.Delivery_ID%TYPE
) RETURN Restaurants.RestaurantID%TYPE
AS
v_RestaurantID Restaurants.RestaurantID%TYPE;
BEGIN
SELECT RestaurantID
INTO v_RestaurantID
FROM Deliveries
WHERE Delivery_ID = p_Delivery_ID;
RETURN v_RestaurantID;
END get_Delivery_RestaurantID;
/
CREATE OR REPLACE FUNCTION does_Restaurant_Offer_Item (
p_RestaurantID Restaurants.RestaurantID%TYPE,
p_Menu_Item_ID Menu_Items.Menu_Item_ID%TYPE
) RETURN NUMBER
AS
v_exists NUMBER(1);
BEGIN
SELECT CASE WHEN EXISTS ( SELECT 1
FROM Offers
WHERE RestaurantID = p_RestaurantID
AND Menu_Item_ID = p_Menu_Item_ID
)
THEN 1
ELSE 0
END
INTO v_exists
FROM DUAL;
RETURN v_exists;
END does_Restaurant_Offer_Item;
/
Then just add a trigger to the table to check that the Restaurant offers the item and, if not, raise an exception.
CREATE TRIGGER check_Valid_Delivery_Item
BEFORE INSERT OR UPDATE OF Delivery_ID, Menu_Item_ID
ON in_delivery
FOR EACH ROW
BEGIN
IF does_restaurant_Offer_Item( get_Delivery_RestaurantID( :new.Delivery_ID ), :new.Menu_Item_ID ) = 0
THEN
RAISE_APPLICATION_ERROR (-20100, 'Invalid Delivery Item');
END IF;
END check_Valid_Delivery_Item;
/
INSERT INTO in_delivery VALUES( 1, 1 )
/
INSERT INTO in_delivery VALUES( 2, 2 )
/
Query 1:
SELECT * FROM in_delivery
Results:
| DELIVERY_ID | MENU_ITEM_ID |
|-------------|--------------|
| 1 | 1 |
| 2 | 2 |
If you try to do:
INSERT INTO in_delivery VALUES( 1, 2 );
Then you get:
ORA-20100: Invalid Delivery Item ORA-06512: at "USER_4_F9593.CHECK_VALID_DELIVERY_ITEM", line 4 ORA-04088: error during execution of trigger 'USER_4_F9593.CHECK_VALID_DELIVERY_ITEM' : INSERT INTO in_delivery VALUES( 1, 2 )