Related
Brief model overview:
I have a student and a course tables. As it's many to many relation there is also a junction table student_course (id_student, id_course), with unique constraint on both columns (composite).
The problem I want to solve:
On account of a mistake, there is no a unique constraint on the code column of the course table. It should as code column should uniquely identify a course. As a result there are two rows in the course table with the same value in the code column. I want to remove that duplicate, check that there is no other duplicates and add a unique constraint on the code column. Without loosing relations with student table.
My approach to solve the issue:
I have create a procedure that should do what I want.
CREATE OR REPLACE PROCEDURE REMOVE_COURSES
(
v_course_code IN VARCHAR2,
v_course_price IN VARCHAR2
)
AS
new_course_id NUMBER;
BEGIN
INSERT INTO course (CODE, PRICE) VALUES (v_course_code, v_course_price)
RETURNING ID INTO new_course_id;
FOR c_course_to_overwrite IN (SELECT *
FROM course
WHERE code = v_course_code AND id != new_course_id) LOOP
UPDATE student_course SET id_course = new_course_id WHERE id_course = c_course_to_overwrite.id;
DELETE FROM course WHERE id = c_course_to_overwrite.id;
END LOOP;
END REMOVE_COURSES;
/
Main problem I want to solve:
The procedure keeps giving me an error about unique constraint violation on student_course table. But I am really not sure how it's possible as I am using new_course_id, so there is no chance that in the junction table there are two rows with the same id_student, id_course. What do I need to fix ?
Miscellaneous:
I want to solve that issue using procedure only for learning purposes
EDITED:
CREATE TABLE student (
id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY,
name VARCHAR2(150) NOT NULL,
PRIMARY KEY (id)
);
ALTER TABLE student MODIFY ID
GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH LIMIT VALUE);
CREATE TABLE course (
id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY,
code VARCHAR2(255) NOT NULL,
PRIMARY KEY (id)
);
ALTER TABLE course MODIFY ID
GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH LIMIT VALUE);
CREATE TABLE student_course (
id_student NUMBER NOT NULL,
id_course NUMBER NOT NULL,
PRIMARY KEY (id_student, id_course),
CONSTRAINT student_fk FOREIGN KEY (id_student) REFERENCES student (id),
CONSTRAINT course_fk FOREIGN KEY (id_course) REFERENCES course (id)
);
insert into student (name) values ('John');
INSERT INTO course (ID, CODE) VALUES (1, 'C_13');
INSERT INTO course (ID, CODE) VALUES (2, 'C_13');
commit;
INSERT INTO STUDENT_COURSE (ID_STUDENT, ID_COURSE) VALUES (1, 1);
INSERT INTO STUDENT_COURSE (ID_STUDENT, ID_COURSE) VALUES (1, 2);
commit;
CALL REMOVE_COURSES('C_13');
[23000][1] ORA-00001: unique constraint (SYS_C0014983) violated ORA-06512: near "REMOVE_COURSES", line 8
Rather than removing one of the duplicate codes, you're creating a third course with the same code, and trying to move all students on either of the old courses onto the new one. The error suggests you have students who are already enrolled on both of the old courses.
Your cursor loop query is:
SELECT *
FROM course
WHERE code = v_course_code AND id != new_course_id
That will find all junction records for both old versions of the code, and the update then sets all of those junction records to the same new ID.
If there are any students listed against both old IDs for the code - which would be allowed by your composite unique key - then they will both be updated to the same new ID.
So say the courses you're looking at are [updated for your example code]:
ID CODE
-- ----
1 C_13
2 C_13
and you have junction records for a student for both courses, like:
ID_STUDENT ID_COURSE
---------- ---------
1 1
1 2
You are creating a new course:
ID CODE
-- ----
3 C_13
Your cursor loop looks for code = 'ABC' and ID != 3, which finds IDs 1 and 2. So in the first iteration of the loop up update the rows with ID 1, so now you have:
ID_STUDENT ID_COURSE
---------- ---------
1 3
1 2
Then in the second iteration you try to update the rows with ID 2, which would attempt to produce:
ID_STUDENT ID_COURSE
---------- ---------
1 3
1 3
which would break the unique constraint - hence the error.
You probably don't want to create a new course at all, but either way, you need to remove duplicate records from student_course - that is, rows which will become duplicates when updated. Basically you need to find students with entries for both existing course IDs, and delete either of them. If you don't care which this would do it:
delete from student_course sc1
where id_course in (
select id
from course
where code = 'C_13'
)
and exists (
select null
from student_course sc2
join course c on c.id = sc.id_course
where sc2.id_student = sc1.id_student
and sc2.id_course > sc1.id_course
and c.code = 'C_13'
);
but there are other (probably better) ways.
You then have the choice of updating all remaining junction records for both old IDs to your new ID; or to consolidate on one of the old IDs and remove the other.
(Your question implies you want to solve the overall task yourself, so I'll refrain from trying to provide a complete solution - this just hopefully helps you understand and resolve your main problem...)
I have experienced this bug in the past in various instances of Oracle 11g. Don't really have access personally to Oracle support as to know if it's an identified/resolved issue.
The bug causes a query to always return the default value of a column even when the record actually has a different value.
It manifests only in specific queries and it's caused by adding a not-nullable column with a default value in a previously existing table by using one single add sentence.
Please see below for a complete reproduction.
DROP TABLE testBugMain;
DROP TABLE testBugAdditional;
-- Create two tables related to one another through real or apparent FK.
CREATE TABLE testBugMain (
mainid NUMBER(1) NOT NULL,
str VARCHAR(200) NULL,
CONSTRAINT PK_testBugMain PRIMARY KEY (mainid)
);
CREATE TABLE testBugAdditional (
additionalid NUMBER(1) NOT NULL,
mainid NUMBER(1) NOT NULL,
CONSTRAINT PK_testBugAdditional PRIMARY KEY (additionalid)
);
-- Insert a couple of values on both.
INSERT INTO testBugMain (mainid, str) VALUES (1, 'test-with-1');
INSERT INTO testBugMain (mainid, str) VALUES (2, 'test-with-2');
INSERT INTO testBugMain (mainid, str) VALUES (3, 'test-with-3');
INSERT INTO testBugAdditional (additionalid, mainid) VALUES (1, 1);
INSERT INTO testBugAdditional (additionalid, mainid) VALUES (2, 2);
INSERT INTO testBugAdditional (additionalid, mainid) VALUES (3, 3);
-- Required for bug to manifest: add a new column, NOT NULL, with DEFAULT value (5 in this case).
-- It needs to be added through an ALTER and both DEFAULT and NOT NULL in the same sentence for the bug to manifest.
ALTER TABLE testBugAdditional ADD bug NUMBER(1) DEFAULT 5 NOT NULL;
-- Update the value on the column, so the query below should return these values instead of 5's.
UPDATE testBugAdditional SET bug = 1 WHERE mainid = 1;
UPDATE testBugAdditional SET bug = 2 WHERE mainid = 2;
UPDATE testBugAdditional SET bug = 3 WHERE mainid = 3;
-- Sanity check (returned values are correct).
SELECT ma.mainid, ma.str, ad.bug FROM testBugMain ma INNER JOIN testBugAdditional ad ON ma.mainid = ad.mainid;
-- Insanity check (returned values are default values).
SELECT mainid, str, bug FROM
(
SELECT ma.mainid, ma.str, ad.bug FROM testBugMain ma
LEFT JOIN -- Required for bug to manifest (left join).
testBugAdditional ad ON ma.mainid = ad.mainid
ORDER BY ma.mainid ASC -- Required for bug to manifest (any order by, perhaps?).
)
WHERE rownum < 6 -- Required for bug to manifest (pagination);
Although the query exposing the bug seems somewhat convoluted, I suppose is not actually that strange to have a sorted, paginated query with left joins (and, all things considered, having the bug manifest rarely is probably worse).
Questions
Is this an identified/resolved issue by Oracle?
Are there any workarounds for the issue?
Solution/Workaround
To get around this issue, always add not-nullable columns to existing tables in a way similar to the following:
-- Add the column as nullable with a default.
ALTER TABLE existingTable ADD newColumn NUMBER(1) DEFAULT 5;
-- Add the not-null constraint.
ALTER TABLE existingTable MODIFY newColumn NOT NULL;
The problem does not reproduce in 12.1 Here is my output from your test case
MAINID STR BUG
---------- -------------------- ----------
1 test-with-1 1
2 test-with-2 2
3 test-with-3 3
MAINID STR BUG
---------- -------------------- ----------
1 test-with-1 1
2 test-with-2 2
3 test-with-3 3
By the way, thank you for putting together an excellent test case. This is a great example of how to post.
In my vb application I want an autogenerated id of alphanumeric characters, like prd100. How can I increment it using Oracle as backend?
Any particular reason it needs to be alphanumeric? If it can just be a number, you can use an Oracle sequence.
But if you want just a random string, you could use the dbms_random function.
select dbms_random.string('U', 20) str from dual;
So you could probably combine these 2 ideas (in the code below, the sequence is called oid_seq):
SELECT dbms_random.string('U', 20) || '_' || to_char(oid_seq.nextval) FROM dual
There are two parts to your question. The first is how to create an alphanumeric key. The second is how to get the generated value.
So the first step is to determine the source of the alpha and the numeric components. In the following example I use the USER function and an Oracle sequence, but you will have your own rules. I put the code to assemble the key in a trigger which is called whenever a row is inserted.
SQL> create table t1 (pk_col varchar2(10) not null, create_date date)
2 /
Table created.
SQL> create or replace trigger t1_bir before insert on t1 for each row
2 declare
3 n pls_integer;
4 begin
5 select my_seq.nextval
6 into n
7 from dual;
8 :new.pk_col := user||trim(to_char(n));
9 end;
10 /
Trigger created.
SQL>
The second step requires using the RETURNING INTO clause to retrieve the generated key. I am using SQL*PLus for this example. I confess to having no idea how to wire this syntax into VB. Sorry.
SQL> var new_pk varchar2(10)
SQL> insert into t1 (create_date)
2 values (sysdate)
3 returning pk_col into :new_pk
4 /
1 row created.
SQL> print new_pk
NEW_PK
--------------------------------
APC61
SQL>
Finally, a word of warning.
Alphanumeric keys are a suspicious construct. They reek of "smart keys" which are, in fact, dumb. A smart key is a value which contains multiple parts. At somepoint you will find yourself wanting to retrieving all rows where the key starts with 'PRD', which means using SUBSTR() or LIKE. Even worse someday the definition of the smart key will change and you will have to cascade a complicated update to your table and its referencing foreign keys. A better ides is to use a surrogate key (number) and have the alphanumeric "key" defined as separate columns with a UNIQUE composite constraint to enforce the business rule.
SQL> create table t1 (id number not null
2 , alpha_bit varchar2(3) not null
3 , numeric_bit number not null
4 , create_date date
5 , constraint t1_pk primary key (id)
6 , constraint t1_uk unique (alpha_bit, numeric_bit)
7 )
8 /
Table created.
SQL>
Can a One-To-Constant Relationship be completely modeled in Oracle with constraints? In other words, the PARENT entity ALWAYS has EXACTLY n-CHILDREN of the child entity, and each child only has one parent.
Consider n to be a database constant.
Doing this so that it is sound and correct even when multiple sessions are doing updates is not easy. You will get yourself in a mess if you try this with triggers, and Oracle's declarative constraints are not powerful enough to express this.
It can be done as follows:-
Create a materialized view log on both the parent and the child tables
Create a materialized join view that joins them together and counts the number of children grouped by the parent. This must be REFRESH FAST ON COMMIT
Put a constraint on the materialized join view that the count of child records must equal "n" (your database constant)
You can then do a series of insert/update/delete statements. When you commit, the materialized view will refresh and if the condition is not met you will get a constraint violation error at that point.
A bonus bit of trickery is to only include rows that fail the constraint into the materialized view (HAVING count(ChildId) <> 5) so you do not waste any storage space.
Building upon the earler "chicken + egg" points, you can create deferrable constraints which aren't validated until commit time... these might help?
e.g.
ALTER TABLE AGREEMENTS ADD CONSTRAINT name FOREIGN KEY (column) REFERENCES table (column) DEFERRABLE INITIALLY DEFERRED;
I don't see how. It is the old question "which came first, the chicken or the egg?". How can you constrain the parent when no children have been added yet, and how can you add children without a parent?
you could create a new table, called something like "ValidParents" that only has the parent IDs that have N children, and keep it in sync with triggers.
There is an alternative solution to force each parent to have exactly either 0 or n children without materialized views using just check, foreign key and uniqueness constraints. For this, one has to number the children and add a field containing the number of the next sibling. Here an example for n=5 that works in PostgreSQL, for other DBS one has to adapt probably the type serial:
create table Tree(
id serial,
parent_id integer not null references Tree(id),
child_nr integer check(child_nr between 1 and 5),
next_sibling_nr integer,
unique (parent_id, child_nr),
check(next_sibling_nr in (child_nr+1, child_nr-4)),
check(((parent_id is null) and (child_nr is null) and
(next_sibling_nr is null)) or ((parent_id is not null)
and (child_nr is not null) and (next_sibling_nr is not null))),
foreign key (parent_id, next_sibling_nr) references Tree(parent_id, child_nr),
primary key (id)
);
The last (long) check constraint ensures that the fields parent_id, child_nr and next_sibling_nr are all null or all not null. The uniqueness constraint and the check for the child_nr field take care that a parent has at most 5 children. The other check constraint and the foreign key constraint on the pair (parent_id, next_sibling_nr) ensure that there are not less than 5 children.
After inserting a root with automatically generated id 1 with the command
insert into Tree (parent_id)
values (null);
one can add children always in packs of 5:
insert into Tree (parent_id, child_nr, next_sibling_nr)
values (1, 1, 2),
(1, 2, 3),
(1, 3, 4),
(1, 4, 5),
(1, 5, 1);
This solution is derived from the answers to a similar question I asked some weeks ago.
This may not be what you want, but I do have one method that does something similar.
The usual arrangement for one-to-many is something like this:
Primary Table:
primary_id (PK)
primary_stuff
Secondary Table:
secondary_id (PK)
primary_id (FK)
secondary_stuff
The alternative, to model a strict one-to-one would be:
Primary Table:
primary_id (PK)
secondary_id (FK, non-null)
primary_stuff
Secondary Table:
secondary_id (PK)
secondary_stuff
It might be a bit strange, but it works. A variation of this may be useful where there's a one-to-many with a one-to-one in it, such as having multiple addresses for a customer, but exactly one billing address.
An alternative solutionb to the chicken and egg problem is to use INSERT ALL. Because it is a single statement it obviates the need for deferrable foreign key constraints. It also provides a mechanism for inserting an exact number of dependent rows. Additional constraints prevent the insertion of additional rows. But we need a subsidiary table with foreign keys to prevent the accidental deletion of the rows of interest.
In this example, n = 3.
SQL> create table parent
2 ( pk_col number not null
3 , col1 varchar2(20)
4 , constraint par_pk primary key (pk_col)
5 )
6 /
Table created.
SQL>
SQL> create table child
2 ( pk_col number not null
3 , seqno number(1,0) not null
4 , col2 date
5 , constraint ch_pk primary key
6 (pk_col, seqno)
7 , constraint ch_par_fk foreign key
8 (pk_col) references parent (pk_col)
9 , constraint ch_ck check (seqno between 1 and 3)
10 )
11 /
Table created.
SQL>
SQL> create table child_lock
2 ( pk_col_1 number not null
3 , seqno_1 number(1,0) not null
4 , pk_col_2 number not null
5 , seqno_2 number(1,0) not null
6 , pk_col_3 number not null
7 , seqno_3 number(1,0) not null
8 , constraint chlk_pk primary key
9 (pk_col_1, seqno_1, pk_col_2, seqno_2, pk_col_3, seqno_3)
10 , constraint chlk_par_1_fk foreign key
11 (pk_col_1, seqno_1) references child (pk_col, seqno)
12 , constraint chlk_par_2_fk foreign key
13 (pk_col_2, seqno_2) references child (pk_col, seqno)
14 , constraint chlk_par_3_fk foreign key
15 (pk_col_3, seqno_3) references child (pk_col, seqno)
16 )
17 /
Table created.
SQL>
SQL> insert all
2 into parent values (pk_val, val_1)
3 into child values (pk_val, 1, sysdate)
4 into child values (pk_val, 2, sysdate+1)
5 into child values (pk_val, 3, sysdate+2)
6 into child_lock values (pk_val, 1, pk_val, 2, pk_val, 3)
7 select 999 as pk_val
8 , 'APPLE PIE' as val_1
9 from dual
10 /
5 rows created.
SQL>
SQL> insert into child values (999, 4, sysdate+4)
2 /
insert into child values (999, 4, sysdate+4)
*
ERROR at line 1:
ORA-02290: check constraint (APC.CH_CK) violated
SQL> insert into child values (999, 3, sysdate+4)
2 /
insert into child values (999, 3, sysdate+4)
*
ERROR at line 1:
ORA-00001: unique constraint (APC.CH_PK) violated
SQL> insert into child values (999, 2.5, sysdate+4)
2 /
insert into child values (999, 2.5, sysdate+4)
*
ERROR at line 1:
ORA-00001: unique constraint (APC.CH_PK) violated
SQL> delete from child
2 /
delete from child
*
ERROR at line 1:
ORA-02292: integrity constraint (APC.CHLK_PAR_1_FK) violated - child record found
SQL>
I accept the solution is a trifle contrived and also inflexible, but then so is the original requirement. It is also far from bulletproof - delete the row from CHILD_LOCK and you can delete one or more CHILD records.
You can create your tables as normal with a 1:M relationship, then on the child table have a count column with a check constraint that determines how many children can exist for a parent, as well as a unique constraint over the Parent ID + count column. e.g.:
CREATE TABLE Parent (PID NUMBER PRIMARY KEY);
CREATE TABLE Child (
PID NUMBER NOT NULL,
Count NUMBER(1) NOT NULL,
CONSTRAINT count_check CHECK (Count BETWEEN 1 AND 5),
CONSTRAINT parent_child_fk FOREIGN KEY (PID) REFERENCES Parent (PID),
CONSTRAINT count_unique UNIQUE (PID, Count));
The only thing this doesn't guarantee is that for each parent there are AT LEAST five children; to get around this you might create a materialized view with a constraint as WW suggests, or build something extra in the application (e.g. an "error" report).
I have an Oracle database table that I want to apply a unique constraint on. The issue is that I only want to apply the constraint if a column in that table is null,
ie. if a row does not have a deleted_date column, then apply the constraint, otherwise ignore it. This will allow for soft deleting records and ignoring constraints on them.
Any thoughts on how to do this?
Cheers,
Mark
Just create a multi column constraint - the column you want to be unique plus the deletion date. All not deleted rows will have a unique value and the deletion date null. All deleted rows will be unique because of the deletion date (assuming it is a time stamp and the resolution is good enough to separate all deletions). If deleted rows cannot be separated by the deletion date, one could think about creating a new column and adding it to the constraint to uniquify the deletion date - but this would be quite an inelegant solution.
And if the resolution is not good enough, then you can create a unique function based index.
An example:
SQL> create table t (id,col,deleted_date)
2 as
3 select 1, 99, null from dual union all
4 select 2, 99, date '2009-06-22' from dual
5 /
Tabel is aangemaakt.
SQL> alter table t add constraint t_pk primary key (id)
2 /
Tabel is gewijzigd.
SQL> alter table t add constraint t_uk1 unique (col,deleted_date)
2 /
Tabel is gewijzigd.
This is the solution described by Daniel. If there is ever a possibility that two rows are deleted at the exact same time (I'm using only the date part here), this solution is not good enough:
SQL> insert into t values (3, 99, date '2009-06-22')
2 /
insert into t values (3, 99, date '2009-06-22')
*
FOUT in regel 1:
.ORA-00001: unique constraint (RWK.T_UK1) violated
In that case use a unique function based index:
SQL> alter table t drop constraint t_uk1
2 /
Tabel is gewijzigd.
SQL> create unique index i1 on t (nvl2(deleted_date,null,col))
2 /
Index is aangemaakt.
SQL> insert into t values (3, 99, date '2009-06-22')
2 /
1 rij is aangemaakt.
Regards,
Rob.