Hi All,
I am trying to build a conditional unique key constraint where based on specific data availability, I need to allow duplicates or no duplicates.
Ex:
I get to insert data with deptno=30 and deptname =SALES; then I can have EMP=123 with more than one record whose DEPTNO=30.
emp table
EMPNO DEPTNO DNAME SAL
123 30 SALES 1000
123 30 SALES 1000
123 30 SALES 1000
But should not allow duplicate for any employee record whose DEPTNO'S deptname is not SALES.
EMPNO DEPTNO DNAME SAL
123 40 ACCOUNTING 1000 (No more than one record for a given EMPNO and DEPTNO should be present as the deptname <> SALES)
I created the below code, and it is failing due to a Mutating error. Appreciate your help here.
create table dept(
deptno number(2,0),
dname varchar2(14),
loc varchar2(13),
constraint pk_dept primary key (deptno)
);
-- 2
create table emp(
empno number(4,0),
ename varchar2(10),
job varchar2(9),
mgr number(4,0),
hiredate date,
sal number(7,2),
comm number(7,2),
deptno number(2,0),
constraint pk_emp primary key (empno),
constraint fk_deptno foreign key (deptno) references dept (deptno)
);
-- 3
insert into DEPT (DEPTNO, DNAME, LOC)
values(10, 'ACCOUNTING', 'NEW YORK')
;
-- 4
insert into dept
values(20, 'RESEARCH', 'DALLAS')
;
-- 5
insert into dept
values(30, 'SALES', 'CHICAGO')
;
-- 6
insert into dept
values(40, 'OPERATIONS', 'BOSTON')
;
-- 7
insert into emp
values(
7839, 'KING', 'PRESIDENT', null,
to_date('17-11-1981','dd-mm-yyyy'),
5000, null, 10
)
;
-- 8
insert into emp
values(
7698, 'BLAKE', 'MANAGER', 7839,
to_date('1-5-1981','dd-mm-yyyy'),
2850, null, 30
)
;
-- 9
insert into emp
values(
7782, 'CLARK', 'MANAGER', 7839,
to_date('9-6-1981','dd-mm-yyyy'),
2450, null, 10
)
;
-- 10
insert into emp
values(
7566, 'JONES', 'MANAGER', 7839,
to_date('2-4-1981','dd-mm-yyyy'),
2975, null, 20
)
;
-- 11
insert into emp
values(
7788, 'SCOTT', 'ANALYST', 7566,
to_date('13-JUL-87','dd-mm-rr') - 85,
3000, null, 20
)
;
-- 12
insert into emp
values(
7902, 'FORD', 'ANALYST', 7566,
to_date('3-12-1981','dd-mm-yyyy'),
3000, null, 20
)
;
-- 13
insert into emp
values(
7369, 'SMITH', 'CLERK', 7902,
to_date('17-12-1980','dd-mm-yyyy'),
800, null, 20
)
;
-- 14
insert into emp
values(
7499, 'ALLEN', 'SALESMAN', 7698,
to_date('20-2-1981','dd-mm-yyyy'),
1600, 300, 30
)
;
-- 15
insert into emp
values(
7521, 'WARD', 'SALESMAN', 7698,
to_date('22-2-1981','dd-mm-yyyy'),
1250, 500, 30
)
;
-- 16
insert into emp
values(
7654, 'MARTIN', 'SALESMAN', 7698,
to_date('28-9-1981','dd-mm-yyyy'),
1250, 1400, 30
)
;
-- 17
insert into emp
values(
7844, 'TURNER', 'SALESMAN', 7698,
to_date('8-9-1981','dd-mm-yyyy'),
1500, 0, 30
)
;
-- 18
insert into emp
values(
7876, 'ADAMS', 'CLERK', 7788,
to_date('13-JUL-87', 'dd-mm-rr') - 51,
1100, null, 20
)
;
-- 19
insert into emp
values(
7900, 'JAMES', 'CLERK', 7698,
to_date('3-12-1981','dd-mm-yyyy'),
950, null, 30
)
;
-- 20
insert into emp
values(
7934, 'MILLER', 'CLERK', 7782,
to_date('23-1-1982','dd-mm-yyyy'),
1300, null, 10
)
;
CREATE OR REPLACE TRIGGER fn_emp_trigger
FOR INSERT OR UPDATE
ON emp
COMPOUND TRIGGER
gv_dname VARCHAR2 (100);
gv_emp_rec emp%ROWTYPE;
/*
create global temporary table g_emp on commit preserve rows as select * from emp where 1=2;
*/
BEFORE STATEMENT
IS
BEGIN
NULL;
DBMS_OUTPUT.put_line ('BEFORE STATMENT');
END BEFORE STATEMENT;
BEFORE EACH ROW
IS
lv_dname VARCHAR2 (100);
ln_check NUMBER;
lb_ignore BOOLEAN := FALSE;
BEGIN
SELECT dname
INTO lv_dname
FROM dept
WHERE deptno = :new.deptno;
gv_dname := lv_dname;
IF lv_dname = 'SALES'
THEN
lb_ignore := TRUE;
END IF;
IF INSERTING
THEN
IF NOT lb_ignore
THEN
INSERT INTO g_emp
SELECT *
FROM emp
WHERE emp.deptno = :new.deptno
AND emp.ename = :new.ename;
END IF;
ELSIF UPDATING
THEN
IF NOT lb_ignore
THEN
NULL;
END IF;
END IF;
END BEFORE EACH ROW;
AFTER EACH ROW
IS
lv_dname VARCHAR2 (100);
ln_check NUMBER;
lb_ignore BOOLEAN := FALSE;
BEGIN
IF gv_dname = 'SALES'
THEN
lb_ignore := TRUE;
END IF;
IF INSERTING
THEN
IF NOT lb_ignore
THEN
SELECT COUNT (*)
INTO ln_check
FROM g_emp emp
WHERE emp.deptno = :new.deptno AND emp.ename = :new.ename;
DBMS_OUTPUT.put_line ('ln_check -> ' || ln_check);
IF ln_check >= 1
THEN
DBMS_OUTPUT.put_line ('AFTER ROW UNIQUE KEY ERROR');
raise_application_error (-20001, 'UNIQUE KEY VIOLATION');
END IF;
END IF;
ELSIF UPDATING
THEN
IF NOT lb_ignore
THEN
NULL;
END IF;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
BEGIN
NULL;
DBMS_OUTPUT.put_line ('AFTER STATMENT');
END AFTER STATEMENT;
END fn_emp_trigger;
/
SHOW ERRORS;
Using triggers to enforce cross-row/table constraints is always tricky. It's tough to get these correct when many sessions are writing to the tables at the same time.
An alternative is to use (fast refresh on commit) materialized views. If you can write a query that returns the data you DON'T want in the MV, you can create a check constraint on it to reject these.
In this case you need a query that counts the employee rows/department. Then you can check this is one for all departments except sales.
It's unclear to me whether you can duplicate employee IDs across departments - I've assumed you can for this example.
So the MV query would be something like:
select e.empno, d.deptno, d.dname, count(*) c
from emp e, dept d
where e.deptno = d.deptno
group by e.empno, d.deptno, d.dname;
Then you have a check constraint to check the count is one for non-sales departments and equal to itself for sales. Which could look something like:
check ( case
when dname = 'SALES' then c
else 1
end = c
)
Full demo below. I covered this topic in last month's Ask TOM Office Hours, so if you want more details on how this process works watch the recording:
create table dept(
deptno number(2,0),
dname varchar2(14),
loc varchar2(13),
constraint pk_dept primary key (deptno)
);
-- 2
create table emp(
empno number(4,0),
ename varchar2(10),
job varchar2(9),
mgr number(4,0),
hiredate date,
sal number(7,2),
comm number(7,2),
deptno number(2,0),
constraint fk_deptno foreign key (deptno) references dept (deptno)
);
create materialized view log
on dept with primary key, rowid ( dname )
including new values;
create materialized view log
on emp
with rowid ( empno, deptno )
including new values;
create materialized view dept_emps
refresh fast on commit
as
select e.empno, d.deptno, d.dname, count(*) c
from emp e, dept d
where e.deptno = d.deptno
group by e.empno, d.deptno, d.dname;
alter table dept_emps
add constraint emp_dups_c
check ( case
when dname = 'SALES' then c
else 1
end = c
);
insert into DEPT (DEPTNO, DNAME, LOC)
values(10, 'ACCOUNTING', 'NEW YORK');
insert into dept
values(30, 'SALES', 'CHICAGO');
/* Initial row */
insert into emp ( empno, deptno )
values ( 7839, 10 );
commit;
/* Can't duplicate in dept = 10 */
insert into emp ( empno, deptno )
values ( 7839, 10 );
commit;
/* Can duplicate in dept = 30 */
insert into emp ( empno, deptno )
values ( 7839, 30 );
insert into emp ( empno, deptno )
values ( 7839, 30 );
commit;
select * from dept_emps;