Skip to Main Content

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, John.

Asked: August 10, 2004 - 10:04 am UTC

Last updated: March 01, 2010 - 9:27 am UTC

Version: 8.1.7

Viewed 1000+ times

You Asked

Hi Tom,

Using the two tables dept and emp, where emp includes a foreign key reference to dept, how can I prevent the creation of a dept record which does not have a referencing emp record?

That is, I want to enforce the fact that all departments have at least one employee. Clearly, the dept record must exist before the emp record; however, I want to ensure that both are created within the one transaction.

Thanks,
John.

and Tom said...

here is one approach - there are others (using on commit refresh materialized views for example)

applications see emp and dept, they are not exposed to empcnt, they do not know it exists:

ops$tkyte@ORA9IR2> create table dept_table
2 (deptno number primary key,
3 dname varchar2(10),
4 empcnt number default 0,
5 constraint check_children
6 check ( empcnt > 0 )
7 deferrable initially deferred
8 )
9 /

Table created.

ops$tkyte@ORA9IR2> create or replace view dept
2 as
3 select deptno, dname from dept_table;

View created.

ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2> create table emp
2 ( empno int primary key,
3 deptno references dept_table,
4 ename varchar2(10)
5 )
6 /

Table created.

ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2> create trigger emp_trigger
2 after insert or update or delete on emp for each row
3 begin
4 if (inserting or updating)
5 then
6 update dept_table set empcnt = empcnt+1 where deptno = :new.deptno;
7 end if;
8 if (deleting or updating)
9 then
10 update dept_table set empcnt = empcnt-1 where deptno = :old.deptno;
11 end if;
12 end;
13 /

Trigger created.

ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2> insert into dept values ( 10, '10' );

1 row created.

ops$tkyte@ORA9IR2> insert into emp values ( 1, 10, 'bob' );

1 row created.

ops$tkyte@ORA9IR2> commit;

Commit complete.

ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2> insert into dept values ( 20, '20' );

1 row created.

ops$tkyte@ORA9IR2> commit;
commit
*
ERROR at line 1:
ORA-02091: transaction rolled back
ORA-02290: check constraint (OPS$TKYTE.CHECK_CHILDREN) violated



Rating

  (15 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

UPDATING

A reader, August 10, 2004 - 11:02 am UTC

Why do you have the UPDATING in the trigger? Inserting should bump up the cnt and deleting should bump down, why care if EMP is updated?

Thanks

Tom Kyte
August 10, 2004 - 3:27 pm UTC

update emp set deptno = 20 where empno = 1235;

transfers.

Dave Aldridge, August 10, 2004 - 11:21 am UTC

>>Why do you have the UPDATING in the trigger? Inserting should bump up the cnt and deleting should bump down, why care if EMP is updated?

To allow for EMP records changing dept_id, of course.

Doh!

A reader, August 10, 2004 - 11:59 am UTC

Doh! Tom doesnt miss a thing, does he!

Mark, August 10, 2004 - 12:55 pm UTC

How about just a view on DEPT.

CREATE VIEW DEPT AS (
SELECT D.DEPTNO, D.DNAME, D.LOC
FROM DEPT_TABLE D, EMP E
WHERE D.DEPTNO = E.DEPTNO);

Now everyone sees the view dept with only dept that have employees.




View

A reader, August 10, 2004 - 1:50 pm UTC

Its not about simply viewing departments that have at least 1 employee, the original question was how to not even allow such departments to be created in the first place

:NEW and :OLD

A reader, August 10, 2004 - 2:09 pm UTC

Mr. Tom
Can you please explain what is :new and :old which you just used in your code.


Thanks.

Tom Kyte
August 10, 2004 - 3:46 pm UTC

see the application developers guide, fundemental. chapter on triggers. much will be shown to you there (available on otn.oracle.com)

Another way

A reader, August 10, 2004 - 2:47 pm UTC

If there is a foreign key arrangement between emp and dept, which there should be, you will have to enter a department before you can enter an employee. If you check to see if the dept has at least one employee, how will you ever put the employee in? But assuming that this is actually true, then try the following.



create trigger dept_trigger
after insert on dept for each row
emp_cnt number;
begin
select count(*)
from emp e
into emp_cnt
where e.dept = :new.dept;

if ent_cnt = 0 then
RAISE_APPLICATION_ERROR ( -20601, 'You have an orphaned Department');
end if;
end;


Tom Kyte
August 10, 2004 - 3:56 pm UTC

that way doesn't work.

first of all -- my constraint is checked upon commit -- not insert. So, as long as by the time you commit there is at least one employee -- we are OK.


Your method would prohit any rows from dept from being inserted (simple test would prove that out). You cannot insert the first dept.

And even if you could -- your trigger doesn't do the right thing, what about "delete from emp"? what now?


don't try to enforce integrity constraints via triggers -- use check constraints, unique indexes, whatnot. it is virtually impossible for you to enforce cross row or cross table integrity constraints via triggers yourself.

How about n-m relationship?

Piotr Jarmuz, August 13, 2004 - 9:12 am UTC

For example: suppose we have n-m relationship between task and employee.

Business rule would be:
Each employee must work upon no less than 2 and no more than 4 tasks at any time.

Again business rule may be violated during the transaction but upon commit must hold true.


Tom Kyte
August 13, 2004 - 4:49 pm UTC

use a similar technique? insert into many to many table would maintain a counter off to the side with a check constraint on it.

Delete parent record without any rule

Faisal, January 13, 2007 - 1:12 am UTC

Tom,

I have two tables

CREATE TABLE T_PARENT
(
parent_id NUMBER(9) NOT NULL
);

ALTER TABLE T_PARENT ADD (
CONSTRAINT T_PARENT_PK PRIMARY KEY (parent_id);

CREATE TABLE T_CHILD
(
child_id NUMBER(9) NOT NULL,
parent_id NUMBER(9) NOT NULL
);

ALTER TABLE T_CHILD ADD (
CONSTRAINT T_CHILD_PK PRIMARY KEY (child_id);

ALTER TABLE T_CHILD ADD (
CONSTRAINT T_CHILD_FK FOREIGN KEY (parent_id)
REFERENCES T_PARENT (parent_id));


Now,I want to delete parent record at the same time when child record gets deleted and have no further child left to avoide childless parent record situation. My approch is

CREATE OR REPLACE TRIGGER T_CHILD_ARD
AFTER DELETE
ON T_CHILD FOR EACH ROW
BEGIN
-- Delete childless parent record.
my_Pkg.delete_parent_record (:old.parent_id);
END;

The procedure in my_pkg is

PROCEDURE delete_parent_record (p_parent_id IN NUMBER) IS
parent_record_exist EXCEPTION;
PRAGMA EXCEPTION_INIT (parent_record_exist, -2292);
BEGIN
-- Delete childless parent record.
DELETE FROM t_parent
WHERE parent_id = p_parent_id;
EXCEPTION
WHEN parent_record_exist THEN
-- Do Not delete parent record because one or more child records exist.
NULL;
END delete_parent_record;

The above logic is working fine if there is only one child record and that gets deleted, but if there is more than one child record then I received the error 2292.

Is there any other better solution for this?

Thanks in advance

Faisal

Delete childless parent record

Faisal, April 14, 2007 - 6:49 am UTC

Tom,

I have two tables

CREATE TABLE T_PARENT
(
parent_id NUMBER(9) NOT NULL
);

ALTER TABLE T_PARENT ADD (
CONSTRAINT T_PARENT_PK PRIMARY KEY (parent_id);

CREATE TABLE T_CHILD
(
child_id NUMBER(9) NOT NULL,
parent_id NUMBER(9) NOT NULL
);

ALTER TABLE T_CHILD ADD (
CONSTRAINT T_CHILD_PK PRIMARY KEY (child_id);

ALTER TABLE T_CHILD ADD (
CONSTRAINT T_CHILD_FK FOREIGN KEY (parent_id)
REFERENCES T_PARENT (parent_id));


Now,I want to delete parent record at the same time when child record gets deleted and have no further child left to avoide childless parent record situation. My approch is

CREATE OR REPLACE TRIGGER T_CHILD_ARD
AFTER DELETE
ON T_CHILD FOR EACH ROW
BEGIN
-- Delete childless parent record.
my_Pkg.delete_parent_record (:old.parent_id);
END;

The procedure in my_pkg is

PROCEDURE delete_parent_record (p_parent_id IN NUMBER) IS
parent_record_exist EXCEPTION;
PRAGMA EXCEPTION_INIT (parent_record_exist, -2292);
BEGIN
-- Delete childless parent record.
DELETE FROM t_parent
WHERE parent_id = p_parent_id;
EXCEPTION
WHEN parent_record_exist THEN
-- Do Not delete parent record because one or more child records exist.
NULL;
END delete_parent_record;

The above logic is working fine if there is only one child record and that gets deleted, but if there is more than one child record then I received the error 2292.

Is there any other better solution for this?

Thanks,

Faisal

Tom Kyte
April 16, 2007 - 10:37 am UTC

why???

you have a childless parent upon the initial insert.

and you do realize that in a multi-user world, this won't work.

what happens with an update, how do you prevent a childless parent during insert only operations?????

Cascading Delete

Bill Buchan, October 24, 2007 - 12:33 pm UTC

Hi Tom,

I like your solution. However it prevents cascading delete (due to mutating table on the triggers). Is there an elegant way to handle this? (other, say, than coding a switch off option into the triggers based on a session value which says "let me by-pass the checking")

Thanks!
Bill
Tom Kyte
October 24, 2007 - 3:33 pm UTC

give entire example please, something I can cut, paste and run. To demonstrate your issue. I don't see where a cascade delete is coming into play.

Follow Up: Cascading Delete

Bill, October 24, 2007 - 5:17 pm UTC

Hi Tom,

Sorry, could have been clearer - the problem doesn't exist, of course, in your original example as there is no cascading delete!

Here's the problem - I've modified your code only to add an ON DELETE CASCADE clause (the rest is unchanged) to the detail table as follows:

create table dept_table
(deptno number primary key,
dname varchar2(10),
empcnt number default 0,
constraint check_children
check ( empcnt > 0 )
deferrable initially deferred
)
/

create or replace view dept
as
select deptno, dname from dept_table;

create table emp
( empno int primary key,
deptno references dept_table ON DELETE CASCADE,
ename varchar2(10)
)
/

create trigger emp_trigger
after insert or update or delete on emp for each row
begin
if (inserting or updating)
then
update dept_table set empcnt = empcnt+1 where deptno = :new.deptno;
end if;
if (deleting or updating)
then
update dept_table set empcnt = empcnt-1 where deptno = :old.deptno;
end if;
end;
/

insert into dept values ( 10, '10' );

insert into emp values ( 1, 10, 'bob' );

commit;

delete from dept where deptno = 10;

This leads to:

delete from dept where deptno = 10
*
ERROR at line 1:
ORA-04091: table DEPT_TABLE is mutating, trigger/function may not see
it
ORA-06512: at "EMP_TRIGGER", line 8
ORA-04088: error during execution of trigger 'EMP_TRIGGER'

While I can of course delete the child records first I wondered if there was a nice way (other than selectively disabling triggers) to avoid the problem and therefore retain the cascade delete functionality (useful in more complex hierarchies).

Thanks for your time!
Bill
Tom Kyte
October 25, 2007 - 6:20 pm UTC

you would need to move the trigger logic to an AFTER trigger (not for each row)

http://asktom.oracle.com/tkyte/Mutate/index.html

shows the technique, you would save up an array of deptno values to increment and an array of values to decrement - and then process these arrays in the AFTER trigger...

sorry Tom

Sokrates, October 25, 2007 - 12:13 pm UTC

Hi Bill from Scotland,

I fear Tom won't answer this as he hates triggers.

You have

sql > delete from dept where deptno = 10;
delete from dept where deptno = 10
*
ERROR at line 1:
ORA-04091: table DP.DEPT_TABLE is mutating, trigger/function may not see it
ORA-06512: at "DP.EMP_TRIGGER", line 8
ORA-04088: error during execution of trigger 'DP.EMP_TRIGGER'


Now you could nicely :-)

sql > create trigger dept_trigger
2 instead of delete on dept for each row
3 begin
4 delete emp where deptno = :old.deptno;
5 delete dept_table where deptno = :old.deptno;
6 end;
7 /

Trigger created.

sql > delete from dept where deptno = 10;

1 row deleted.

Thanks

Bill, October 26, 2007 - 5:10 am UTC

Thanks both Tom & Sokrates! 2 techniques that I'll experiment with, and see which suits us best. Much appreciated!

John Gilmore, February 25, 2010 - 3:55 am UTC

Hi Tom,

In your response to the original question you stated

... here is one approach - there are others (using on commit refresh materialized views for example) ...

Could you point me to an example of using on commit refresh materialized views for this type of problem?

Also, you alluded to other possible approaches for solving this, could you advise what they are please?

Thanks,
John.
Tom Kyte
March 01, 2010 - 9:27 am UTC

http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:21389386132607

shows a "refresh fast on commit" example.


those would be the two approaches, anything else would be rather inefficient as it would involve the lock table command to serialize access - or rather complicated trigger logic (which I am categorically "opposed" to)

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library