简体   繁体   中英

How to give a database constraint to ensure this behavior in a table?

I have a table with five columns: A, B, C, D and E.

And I need to comply with the following restrictions:

  • A is the primary key.
  • For a B there can only be one C, ie: 1-1 ; 2-1 ; 3-2 but not 1-2.
  • BC and D can take any value but can not be repeated, ie: 1-1 1 ; 1-1 2 ; not 1-1 1 again.
  • E can take any value.

So, considering the following order

| A | B | C | D | E |

| 1 | 1 | 1 | 1 | 1 | -> OK 

| 2 | 1 | 2 | 1 | 1 | -> Should fail, because there is a B with another C, 1-2 must be 1-1.

| 3 | 1 | 1 | 2 | 1 | -> OK

| 4 | 1 | 1 | 2 | 1 | -> Should fail, because relation between B-C and D is repeated.

| 5 | 2 | 1 | 1 | 1 | -> OK

Is there any way to comply with this behavior through some constraint in the database?

Thanks!

For B - C rule I would create a trigger

For the B - C - D rule looks like you want unique constraint

ALTER TABLE t ADD CONSTRAINT uni_BCD UNIQUE (B,C,D);

A and E are irrelevant to the question and can be ignored.

The BCD rule can be easily solved by creating a unique index on BCD.

If for every B there can be only one C then your DB is not normalized. Create a new table with B and C. Make B the primary key or create a unique index on B. Then remove C from the original table. (At which point the unique index on BCD becomes a unique index on BD.)

Without normalizing the tables, I don't think there's any way to do it with a constraint. You could certainly do it with a trigger or with code.

This condition is not trivial

For a B there can only be one C, ie: 1-1 ; 2-1 ; 3-2 but not 1-2.

, since Oracle does not support CREATE ASSERTION (soon, we hope!)

Therefore, you need to involve a second table to enforce this constraint, or else a statement-level AFTER INSERT/UPDATE trigger.

What I would do is create a second table and have it maintained via an INSTEAD OF trigger on a view, and ensure all my application DML happened via the view. (You could also just create a regular trigger on the table and have it maintain the second table. That's just not my preference. I find INSTEAD OF triggers to be more flexible and more visible.)

In case it's not clear, the purpose of the second table is that it allows you to enforce your constraint as a FOREIGN KEY constraint. The UNIQUE or PRIMARY KEY constraint on the second table ensures that each value of B appears only once.

Here's sample code for that approach:

--DROP TABLE table1_parent;
--DROP TABLE table1;


CREATE TABLE table1_parent
 ( b number NOT NULL,
   c number NOT NULL,
   constraint table1_parent_pk PRIMARY KEY (b),
   constraint table1_parent_u1 UNIQUE (b, c) );

CREATE TABLE table1
(
  a   NUMBER NOT NULL,
  b   NUMBER NOT NULL,
  c   NUMBER NOT NULL,
  d   NUMBER NOT NULL,
  e   NUMBER NOT NULL,
  CONSTRAINT table1_pk PRIMARY KEY (a),  -- "A is the primary key."
  CONSTRAINT table1_fk FOREIGN KEY ( b, c ) REFERENCES table1_parent ( b, c ), -- "For a B there can only be one C, ie: 1-1 ; 2-1 ; 3-2 but not 1-2."
  CONSTRAINT table1_u2 UNIQUE ( b, c, d ) -- "B-C and D can take any value bue can not be repeated, ie: 1-1 1 ; 1-1 2 ; not 1-1 1 again."
);

CREATE INDEX table1_n1 ON table1 (b,c); -- Always index foreign keys

CREATE OR REPLACE VIEW table1_dml_v AS SELECT * FROM table1;

CREATE OR REPLACE TRIGGER table1_dml_v_trg INSTEAD OF INSERT OR UPDATE OR DELETE ON table1_dml_v
DECLARE
  l_cnt NUMBER;
BEGIN
  IF INSERTING THEN
    BEGIN
      INSERT INTO table1_parent (b, c) VALUES ( :new.b, :new.c );
    EXCEPTION
      WHEN dup_val_on_index THEN
        NULL;  -- parent already exists, no problem
    END;

    INSERT INTO table1 ( a, b, c, d, e ) VALUES ( :new.a, :new.b, :new.c, :new.d, :new.e );
  END IF;

  IF DELETING THEN
    DELETE FROM table1 WHERE a = :old.a;

    SELECT COUNT(*) INTO l_cnt FROM table1 WHERE b = :old.b AND c = :old.c;

    IF l_cnt = 0 THEN
      DELETE FROM table1_parent WHERE b = :old.b AND c = :old.c;
    END IF;
  END IF;

  IF UPDATING THEN
    BEGIN
      INSERT INTO table1_parent (b, c) VALUES ( :new.b, :new.c );
    EXCEPTION
      WHEN dup_val_on_index THEN
        NULL;  -- parent already exists, no problem
    END;

    UPDATE table1 SET a = :new.a, b = :new.b, c = :new.c, d = :new.d, e = :new.d WHERE a = :old.a;

    SELECT COUNT(*) INTO l_cnt FROM table1 WHERE b = :old.b AND c = :old.c;

    IF l_cnt = 0 THEN
      DELETE FROM table1_parent WHERE b = :old.b AND c = :old.c;
    END IF;

  END IF;      
END;


insert into table1_dml_v ( a,b,c,d,e) VALUES (1,1,1,1,1);
insert into table1_dml_v ( a,b,c,d,e) VALUES (2,1,2,1,1);
insert into table1_dml_v ( a,b,c,d,e) VALUES (3,1,1,2,1);
insert into table1_dml_v ( a,b,c,d,e) VALUES (4,1,1,2,1);
insert into table1_dml_v ( a,b,c,d,e) VALUES (5,2,1,1,1);

If your system supports fast refreshed materialized views, please try the following.
Since I currently don't access to a this feature, I can't verify the solution.

create materialized view log on t with primary key;

create materialized view t_mv
refresh fast
as
select      b,c
from        t
group by    b,c
;

alter table t_mv add constraint t_mv_uq_b unique (b);

and off course:

alter table t add constraint t_uq_a_b_c unique (b,c,d);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM