Learning Oracle Database Programming
Date: 1 Dec 2001 22:13:09 -0800
Message-ID: <440264b2.0112012213.17d9ba99_at_posting.google.com>
Learning Oracle Database Programming
Chapter 6: Referential Integrity
The next table you'll create is the account table. Each client has one or more accounts. Each
account has a unique 6 character account id which must be in the format nn-nnn ( n stands for
number). Like the client code, this may be expanded in the future. Once again you'll use a
system-assigned number as the primary key.
Defining Integrity Relationships
Each account must belong to one client: an account can't exist without a client. Before an
account is set up, its client must be in the client table. Therefore, the client_no, which is
the primary key of the client table, will be repeated in the account table. The client_no
column in the account table references the client_no in the client_table. This is referential
integrity. It helps protect the database from invalid data. The client table is considered the
parent and the account table is its child. Referential integrity constraints are placed on the
child, not the parent.
When you create a referential integrity constraint, you must decide what action should be taken
when the parent is deleted. The choices are
- delete the child or
- prevent the parent's deletion as long as a child exists.
Orphaned children aren't allowed. If you want to permit orphans, you can't use a referential
integrity constraint to support this.
Creating a Referential Integrity Constraint
For the account table, you'll tell Oracle to delete a client's accounts when the client is deleted. Create the account table by typing:
SQL> create table account (
2 accnt_no integer primary key,
3 accnt_id varchar2(6) not null unique,
4 client_no integer not null references
5 client(client_no) on delete cascade,
6 curr_balance number(11,2));
Table created.
The client_no column in the account table is a foreign key. A foreign
key references a primary
key or a unique column in another table. The constraint could also be defined as "references
client" without explicitely stating the column because client_no is the primary key of the client
table.
Declaring client_no to be not null in addition to the references constraint is necessary because
references means the value must exist in the client.client_no column or be null. The business
requirements state each account must belong to a client. Null values are inappropriate here.
Check the constraints this create table statement created. Type:
SQL> select * from user_constraints
2 where table_name = 'ACCOUNT';
OWNER CONSTRAINT_NAME CTABLE_NAME
------------------------------ ------------------------------ -
SEARCH_CONDITION
R_OWNER R_CONSTRAINT_NAMEDELETE_RU
------------------------------ ------------------------------
STATUS
LOP SYS_C00377 CACCOUNT
ACCNT_ID IS NOT NULL ENABLED
LOP SYS_C00378 CACCOUNT
CLIENT_NO IS NOT NULL ENABLED
LOP SYS_C00379 PACCOUNT ENABLED
LOP SYS_C00380 UACCOUNT ENABLED
LOP SYS_C00381 RACCOUNT
LOP SYS_C00369 CASCADEENABLED Constraint SYS_C00381 is the foreign key constraint. It references constraint SYS_C00369.
Check that constraint. Type:
SQL> select * from user_constraints
2 where constraint_name = 'SYS_C00369';
OWNER CONSTRAINT_NAME CTABLE_NAME
------------------------------ ------------------------------ -
SEARCH_CONDITION
R_OWNER R_CONSTRAINT_NAMEDELETE_RU
------------------------------ ------------------------------
STATUS
LOP SYS_C00369 PCLIENT ENABLED Constraint SYS_C00369 is the client table's primary key constraint.
Next you'll modify the lop_util package body adding account.accnt_no to lop_util.get_next_seqno.
Edit file utilb.sql (changes are in bold):
/*
Creating package body for lop_util
File Name: utilb.sql
Purpose: To create the lop_util package body
Changes: 7/20/96 F.E. Added account.accnt_no to if condition.
*/
create or replace package body lop_util is
- Use errors -20001 to -20029 when raising errors in this package.
function get_next_seqno(object_name in varchar2) return integer is
seqno integer;
err_msg varchar2(80);
begin
if (upper(object_name) = 'CLIENT.CLIENT_NO' or
upper(object_name) = 'ACCOUNT.ACCNT_NO') then
select lop_seq.nextval
into seqno
from dual;
return seqno;
else
raise_application_error(-20002,'There is no source for ' ||
object_name);
end if;
exception
when others then
err_msg := sqlerrm;
raise_application_error(-20001,
'Error raised in get_next_seqno. ' || err_msg);
end get_next_seqno;
end lop_util;
/
Replace the package body:
SQL> set document on
SQL> _at_c:\lop\utilb
DOC>Creating package body for lop_util
DOC>File Name: utilb.sql
DOC>Purpose: To create the lop_util package body
DOC>Changes: 7/20/96 F.E. Added account.accnt_no to if condition.
DOC>*/
Package body created.
Create a pre-insert for each row trigger similar to the one you
created for the client table
to assign a number to the primary key, accnt_no. Type and then save or create a file,
acbri.sql (account before row insert), and then execute it:
SQL> create or replace trigger account_bri
2 before insert on account
3 for each row
4 begin
5 /* Get a sequence number for the primary key */
6 :new.accnt_no := lop_util.get_next_seqno('account.accnt_no');
7 end;
8 /
Trigger created.
SQL> save c:\lop\acbri
Created file c:\lop\acbri
Also create a pre-update for each row trigger to prevent the updating
of account.accnt_no.
Type and then save (or create and then execute a file):
SQL> create or replace trigger account_bru
2 before update on account
3 for each row
4 begin
5 /* No updates allowed on primary key */
6 if (:new.accnt_no != :old.accnt_no) then
7 raise_application_error(-20000,
8 'Accnt_no is a primary key - not updatable.');
9 end if;
10 end;
11 /
Trigger created.
SQL> save c:\lop\acbru
Created file c:\lop\acbru
Before you test your triggers, you'll add several check constraints to
the account table to
ensure an accnt_code is in the form nn-nnn. You'll also redefine the curr_balance column so
it will be given a default value of 0 if a record is inserted without any value for that column.
Type:
SQL> alter table account add(
2 check(length(accnt_id) = 6),
3 check(ltrim(substr(accnt_id,1,2),'1234567890') is null),
4 check(ltrim(substr(accnt_id,4),'1234567890') is null),
5 check(substr(accnt_id,3,1) = '-'));
Table altered.
Notice the various usages of the substr function. Substr(accnt_id,1,2)
returns the first two
characters of the accnt_id. Substr(accnt_id,4) returns the characters in accnt_id from the
fourth character to the end of the string.
Next, use the modify form of the alter table command to add the default value option to the
column definition. This is not considered a constraint. (Remember, the only type of constraint
that can be created with the modify form of alter table is the not null constraint.) Type:
SQL> alter table account modify (curr_balance default 0);
Table altered.
Testing Constraints
Test your triggers and changes to lop_util by inserting some valid and some invalid records.
You'll need to know the valid values of client_no. Type:
SQL> select * from client;
CLIENT_NO CLIEN SHORT_NAME
--------- ----- ----------
LONG_NAME
1 12345 LASCO
2 03030 ROBSEC
Robin Securities
3 34534 INSTRES
The Institute for Research
Now insert some records. You only have to include data for accnt_id
and client_no because
your trigger will assign a value to the mandatory accnt_ no column, and you told Oracle to
assign a default value to the curr_balance column. Your values for client_no may be different.
Type (you can leave out the comments):
SQL> insert into account(accnt_id,client_no)
2 values ('22-333',1) /* valid record */;
1 row created.
SQL> select * from account;
ACCNT_NO ACCNT_ CLIENT_NO CURR_BALANCE
--------- ------ --------- ------------
35 22-333 1 0
SQL> insert into account(accnt_id,client_no)
2 values('11-222',6) /* invalid client_no */;
insert into account(accnt_id,client_no)
*
ERROR at line 1:
ORA-02291: integrity constraint (LOP.SYS_C00381) violated - parent key not found
SQL> insert into account(accnt_id,client_no)
2 values('11-222',null) /* client_no is not null (fk only would allow nulls) */;
insert into account(accnt_id,client_no)
*
ERROR at line 1:
ORA-01400: mandatory (NOT NULL) column is missing or NULL during insert
SQL> insert into account(accnt_id,client_no)
2 values('22-333',2) /* Duplicate accnt_id */;
insert into account(accnt_id,client_no)
*
ERROR at line 1:
ORA-00001: unique constraint (LOP.SYS_C00380) violated SQL> insert into account(accnt_id,client_no)
2 values(null,1) /* accnt_id is a not null column */;
insert into account(accnt_id,client_no)
*
ERROR at line 1:
ORA-01400: mandatory (NOT NULL) column is missing or NULL during insert
SQL> insert into account(accnt_id,client_no)
2 values('2B-333',1) /* invalid accnt_id - B is not a number */;
insert into account(accnt_id,client_no)
*
ERROR at line 1:
ORA-02290: check constraint (LOP.SYS_C00386) violated SQL> insert into account(accnt_id,client_no)
2 values('22*555',1) /* invalid accnt_id - asterisk should be hyphen */;
insert into account(accnt_id,client_no)
*
ERROR at line 1:
ORA-02290: check constraint (LOP.SYS_C00388) violated SQL> insert into account(accnt_id,client_no)
2 values('22-5555',1) /* too long id is trapped by Oracle before constraint */;
insert into account(accnt_id,client_no)
*
ERROR at line 1:
ORA-01401: inserted value too large for column SQL> insert into account(accnt_id,client_no)
2 values('22-4',1) /* invalid accnt_id - less than 6 characters */;
insert into account(accnt_id,client_no)
*
ERROR at line 1:
ORA-02290: check constraint (LOP.SYS_C00385) violated SQL> select * from account;
ACCNT_NO ACCNT_ CLIENT_NO CURR_BALANCE
-------- ------ --------- ------------
35 22-333 1 0
Your trigger prevents updating accnt_no. Error ORA-20000 is produced
by the trigger. The
other error messages were produced by Oracle. Type:
SQL> update account set accnt_no = 6
2 where accnt_no = 35 /* trigger prevents update of pk */;
update account set accnt_no = 6
*
ERROR at line 1:
ORA-20000: Accnt_no is a primary key - not updatable. ORA-06512: at line 4 ORA-04088: error during execution of trigger 'LOP.ACCOUNT_BRU'SQL> commit;
Commit complete.
Cascading Deletes
Delete the client you used in the valid insert into account statement. Before you do this,
be sure you've committed your work since the following delete statement will be rolled back.
Type:
SQL> delete from client where client_no = 1;
1 row deleted
Your account record should be gone because its foreign key constraint
referencing
client.client_no was defined as delete cascade. Type:
SQL> select * from account;
no rows selected
Rollback the delete so your client record and its account record
remain in their tables. Type:
SQL> rollback;
Rollback complete.
Actions since the last rollback are undone. Type:
SQL> select * from account;
ACCNT_NO ACCNT_ CLIENT_NO CURR_BALANCE
--------- ------ --------- ------------
35 22-333 1 0
The account table now has a constraining relationship to the client
table. The client
table may not be dropped until the foreign key (references) constraint in the account table
is dropped (not just disabled). The primary key index for the client table may not be dropped
until the foreign key constraint is either disabled or dropped. Because of this, you can't
create referential integrity constraints which point to a parent table in another schema
(userid) unless that userid has granted you references privilege on its table. If you want
to allow orphans when a parent is deleted, or to set the foreign key value to either null or
a default value when a parent is deleted, Oracle suggests you create the referential integrity
constraint, disable it, and then create triggers to support this. The constraint documents the
relationship and prevents dropping the parent table or the primary key or unique indexes.
Dropping an Index with Dependencies
This relationship between the parent and child tables does complicate system maintenance.
For example, you could have a problem with the parent's primary key index. Many of the
constraints you created are supported by indexes (primary key or unique). When Oracle created
those indexes, it used the default storage parameters. Indexes have storage parameters
similar to those of tables (first extent, next extent, percent increase). If the defaults
turn out to be too small, the index can reach the maximum number of extents allowed. At
that point, no more records can be inserted into the table because the next entry for the
index can't be created. Before referential integrity, the index could simply be dropped
and recreated. Now all the foreign key constraints which reference this primary key must
be disabled before the parent's primary key can be dropped and recreated.
You'll drop and recreate the client table's primary key index. First, find the client table's
primary key constraint. Type:
SQL> select constraint_name
2 from user_constraints
3 where table_name = 'CLIENT'
4 and constraint_type = 'P';
CONSTRAINT_NAME
SYS_C00369
The index enforcing this constraint will have the same name as the constraint. Try to drop it.
Type:
SQL> drop index sys_c00369;
drop index sys_c00369
*
ERROR at line 1:
ORA-02429: cannot drop index used for enforcement of unique/primary
key
The primary key constraint on the client table must be disabled. When
a constraint is
disabled, the index enforcing it is dropped. Try to use the alter table disable command
to disable the primary key. Type:
SQL> alter table client disable primary key;
alter table client disable primary key
*
ERROR at line 1:
ORA-02297: cannot disable constraint (LOP.SYS_C00369) - dependencies
exist
The client primary key constraint can't be disabled until any
dependencies caused by
referential integrity constraints are disabled. This can be done by adding the keyword
cascade to the alter table disable command. However, since there is no cascade option
for the alter table enable command, it's a good idea to identify the enabled dependent
constraints before you do this. If you don't, you won't know which of the dependent
constraints were enabled before this operation (some may have been disabled for other reasons).
Type:
SQL> select table_name,constraint_name
2 from user_constraints
3 where r_constraint_name = 'SYS_C00369' and status = 'ENABLED';
TABLE_NAME CONSTRAINT_NAMENow disable the client table's primary key. Type:
------------------------------ ------------------------------
ACCOUNT SYS_C00381
SQL> alter table client disable primary key cascade;
Table altered.
The dependent constraint should be disabled. Type:
SQL> select status from user_constraints
2 where constraint_name = 'SYS_C00381';
STATUS
DISABLED
The index enforcing the client table's primary should be gone. Type:
SQL> select * from user_indexes
2 where index_name = 'SYS_C00369';
no rows selected
When you enable the primary key constraint, you can tell Oracle the
storage parameters to use
for the primary key index. This will be done through the using index clause of the alter
table...enable constraint command. (This also could have been part of the original create
table statement.) Type:
SQL> alter table client
2 enable primary key
3 using index storage(initial 5000 next 1000 pctincrease 100);
Table altered.
The enable clause could have been written enable constraint
sys_c00369. This command created
the new primary key index. Oracle gives it the same name as the constraint. Look at the
storage parameters for this index. Type:
SQL> select uniqueness,initial_extent,next_extent,pct_increase
2 from user_indexes
3 where index_name = 'SYS_C00369';
UNIQUENES INITIAL_EXTENT NEXT_EXTENT PCT_INCREASE
--------- -------------- ----------- ------------
UNIQUE 6144 2048 100
Oracle created an initial extent of 6144 bytes. On this system
(Windows95), the Oracle
blocksize is 2048 bytes. The request was for an initial extent of 5000 bytes which isn't
a multiple of the blocksize; so, Oracle rounded the initial extent to 3 blocks. It also
rounded up the size of the next extent to be 2048 since the requested size was less than
one block. The minimum number of blocks for a first extent is two. The minimum number
of blocks for a next extent is 1. (This applies to tables as well as indexes.)
The foreign key constraint of the account table can be enabled now that the client table's
primary key has been restored. Type:
SQL> alter table account enable constraint sys_c00381;
Table altered.
Check the account table's foreign key constraints. Type:
SQL> select * from user_constraints
2 where table_name = 'ACCOUNT'
3 and constraint_type = 'R';
OWNER CONSTRAINT_NAME CTABLE_NAME
------------------------------ ------------------------------ -
SEARCH_CONDITION
R_OWNER R_CONSTRAINT_NAMEDELETE_RU
------------------------------ ------------------------------
STATUS
LOP SYS_C00381 RACCOUNT
LOP SYS_C00442 CASCADEENABLED Check the columns on which the constraints were placed. Type:
SQL> select * from user_cons_columns
2 where constraint_name in ('SYS_C00369','SYS_C00381');
OWNER CONSTRAINT_NAMEPOSITION
------------------------------ ------------------------------
TABLE_NAME COLUMN_NAME
------------------------------ ------------------------------
LOP SYS_C00369 CLIENT CLIENT_NO1
LOP SYS_C00381 ACCOUNT CLIENT_NOConstraint sys_c00381 is defined on the account.client_no column and it references constraint
sys_c00369, the primary key constraint defined on the client.client_no column. Received on Sun Dec 02 2001 - 07:13:09 CET