jon
jon

Reputation: 31

How to enforce a one-many relationship between a column in one table and a column in two parent tables?

I have three tables in an existing database application. I don't have ERDs or other documentation about the table relations. There are no existing primary or foreign keys. I would like to model the relation and ideally enforce it with constraints.
The order table can have any number of orders for parts. The value of the Orders.part column must exist in either the Parts_inv table or the Parts_noninv table. I would think the original design should have had one parts table with a type column, but this what I have. Is there a way to model/enforce these relationships?

Orders

order     char(10)
part      char(20)

Parts_inv

part_num  char(50)

Parts_noninv

part_num  char(50)

Upvotes: 1

Views: 122

Answers (4)

Popeye
Popeye

Reputation: 35910

You can also create an insert and update Trigger on the ORDERS table to fulfill the requirement.

As per your comment, PART_NUM will be unique across two tables. So you can create the trigger as follows:

CREATE OR REPLACE TRIGGER ORDERS_TRG BEFORE
    INSERT OR UPDATE ON ORDERS
    FOR EACH ROW
DECLARE
    LV_COUNT   NUMBER;
BEGIN
    SELECT
        1
    INTO LV_COUNT
    FROM
        DUAL
    WHERE
        :NEW.PART IN (
            SELECT
                PART_NUM
            FROM
                PARTS_INV
            UNION ALL
            SELECT
                PART_NUM
            FROM
                PARTS_NONINV
        );

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE;
END ORDERS_TRG;
/

db<>fiddle demo

Update the exception-handling part according to your requirement.

Cheers!!

Upvotes: 0

user5683823
user5683823

Reputation:

Here's one way to achieve this, using a materialized view.

In my full working example, I create two tables, T1 and T2. Each has an ID column, and another column that is not the same in the two tables (NAME in one, PRICE in the other, not even the same data type). The task is to create a primary key from the UNION ALL of columns ID from both tables.

First, ID must be PK in each table. Then we create a materialized view (MV) from the UNION ALL of the ID columns from the two tables, and declare the column ID in the MV as the primary key of the MV. Then, in the last step, I create a child table T, with a column ID, foreign key pointing to the MV (not to T1 or T2, but the union all of the ID's from both tables).

You may want to test various things that you could do with the three tables (T1, T2 and T) to see how this all works.

Two things to keep in mind: (1) PK and FK constraints on tables are usually checked at INSERT / UPDATE / DELETE time (if the constraints are "immediate"); this will be the case for the PK constraints on ID, separately for T1 and T2. However, the joint PK constraint on the MV is only checked at COMMIT time. The FK constraint on table T is checked "immediate" (after each INSERT, UPDATE or DELETE - or MERGE - statement). (2) Checking the PK constraint on the MV adds marginal overhead to DML statements on T1 and T2.

So, here goes:

create table t1 (id number primary key, name varchar2(100));
create table t2 (id number primary key, price number not null);

create materialized view log on t1 with primary key, rowid including new values;
create materialized view log on t2 with primary key, rowid including new values;

create materialized view t12 (id, rid, source)
refresh fast on commit
as
select id, rowid, 't1' from t1 union all select id, rowid, 't2' from t2
;

alter materialized view t12 add constraint t12_pk primary key (id);

create table t (id number references t12, eff_date date not null);

Upvotes: 1

Given just the tables you have you can't do this. But that's not to say it can't be done. :-)

Ideally you should change ORDER.PART to CHAR(50) to match the PARTS_NUM fields on the other tables - otherwise you won't be able to store all the possible PARTS_NUM on an ORDER. Perhaps there are business reasons for it being as it is, and perhaps someone was just clueless. No matter - try to fix it up if you can. It won't stop you completely, though.

Create another table - let's call it PARTS_ALL. It has two fields - PARTS_NUM which should match the data type of the PARTS_NUM fields on PARTS_INV and PARTS_NONINV, and SOURCE which should be VARCHAR2(1). Make the combination of (PARTS_NUM, SOURCE) the primary key. So it looks like

CREATE TABLE ALL_PARTS
  (PARTS_NUM   CHAR(50),
   SOURCE      VARCHAR2(1) NOT NULL,
   CONSTRAINT PK_ALL_PARTS
     PRIMARY KEY (PARTS_NUM, SOURCE));

You then copy the data from PARTS_INV and PARTS_NONINV into ALL_PARTS, adding the appropriate SOURCE value:

INSERT INTO ALL_PARTS (PARTS_NUM, SOURCE)
  SELECT PARTS_NUM, 'I' AS SOURCE FROM PARTS_INV
  UNION ALL
  SELECT PARTS_NUM, 'I' AS SOURCE FROM PARTS_NONINV

Now you define a couple of triggers on PARTS_INV and PARTS_NONINV to propagate inserts and deletes on those tables to ALL_PARTS:

CREATE TRIGGER PARTS_INV_AID
  AFTER INSERT OR DELETE ON PARTS_INV
  FOR EACH ROW
BEGIN
  IF INSERTING THEN
    INSERT INTO ALL_PARTS (PARTS_NUM, SOURCE) VALUES (:NEW.PARTS_NUM, 'I');
  ELSIF DELETING THEN
    DELETE FROM ALL_PARTS WHERE PARTS_NUM = :OLD.PARTS_NUM;
  END IF;
END PARTS_INV_AID;
/

CREATE TRIGGER PARTS_NONINV_AID
  AFTER INSERT OR DELETE ON PARTS_NONINV
  FOR EACH ROW
BEGIN
  IF INSERTING THEN
    INSERT INTO ALL_PARTS (PARTS_NUM, SOURCE) VALUES (:NEW.PARTS_NUM, 'N');
  ELSIF DELETING THEN
    DELETE FROM ALL_PARTS WHERE PARTS_NUM = :OLD.PARTS_NUM;
  END IF;
END PARTS_NONINV_AID;
/

Now your application suite can continue to INSERT and DELETE rows from PARTS_INV and PARTS_NONINV as they've always done, and those changes will be propagated to ALL_PARTS.

NOW (finally!) you can define your foreign key from ORDERS to ALL_PARTS and get the validation you wanted:

ALTER TABLE ORDER
  ADD CONSTRAINT ORDER_FK1
    FOREIGN KEY (PART) REFERENCES ALL_PARTS (PARTS_NUM);

Is this ideal? No. IDEALLY you'd get rid of PARTS_INV and PARTS_NONINV, and replace them with ALL_PARTS, modifying all your applications and web apps and back-office software to use the new table - so probably not gonna happen - but given the situation you find yourself in it may be about as good as you can do.

dbfiddle here

Upvotes: 1

William Robertson
William Robertson

Reputation: 16001

Unfortunately not.

You can define two foreign key constraints, but they will both need to be valid for every row in the table.

The second problem is the choice of char datatype. No char(10) value in ORDERS will match a char(50) in either parent table. Probably the person who chose the data type was not familiar with it.

Upvotes: 0

Related Questions