Skip to Main Content
  • Questions
  • Maintaining a mandatory 1-1 relationship: deleting child removes parent

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, Don.

Asked: May 13, 2019 - 8:24 pm UTC

Last updated: June 04, 2019 - 4:44 pm UTC

Version: 12c

Viewed 1000+ times

You Asked

Our system has a table that is a nominal parent to several other tables. Each of these relationships are 1-1, and there will only be one child per parent. We need to ensure that when deleting a child row that the parent is always deleted as well. Is there a reasonable alternative to a trigger in this case?

(Live SQL is currently unavailable)
create table MY_PARENT_TABLE (
    PK number(19) primary key, 
    CUSTOMER_ID number(19), 
    CREATE_DATETIME timestamp(6), 
    DESCRIPTION varchar2(500 char), 
    ACTION_TYPE varchar2(40 char) 
    );

create table MY_CHILD_TABLE_DOCUMENT_ATTACHMENT (
    PK number(19) primary key, 
    DOCUMENT_ID number(19), 
    foreign key (PK) references MY_PARENT_TABLE (PK) 
    );

create table MY_CHILD_TABLE_CALENDAR_ALERT (
    PK number(19) primary key, 
    CALENDAR_ID number(19), 
    APPOINTMENT date, 
    foreign key (PK) references MY_PARENT_TABLE (PK) 
    );

create table MY_CHILD_TABLE_ORDER_REVIEW (
    PK number(19) primary key, 
    ORDER_ID number(19), 
    RESULTS varchar2(10 char), 
    foreign key (PK) references MY_PARENT_TABLE (PK) 
    );

and Chris said...

You by adding a foreign key from the child to the parent. Which is:

- on delete cascade
- deferrable initially deferred

create table MY_PARENT_TABLE (
  PK number(19) primary key, 
  CUSTOMER_ID number(19), 
  CREATE_DATETIME timestamp(6), 
  DESCRIPTION varchar2(500 char), 
  ACTION_TYPE varchar2(40 char) 
);

create table MY_CHILD_TABLE_DOCUMENT_ATTACHMENT (
  PK number(19) primary key, 
  DOCUMENT_ID number(19), 
  foreign key (PK) references MY_PARENT_TABLE (PK) 
    on delete cascade
);

alter table MY_PARENT_TABLE
  add constraint fk 
  foreign key ( pk ) 
  references MY_CHILD_TABLE_DOCUMENT_ATTACHMENT ( pk )
  on delete cascade
  deferrable initially deferred;
  
insert into MY_PARENT_TABLE 
  values ( 1, 1, sysdate, null, null );
insert into MY_CHILD_TABLE_DOCUMENT_ATTACHMENT 
  values ( 1, 1 );
commit;

select count(*) from my_parent_table;

COUNT(*)   
          1 

delete MY_CHILD_TABLE_DOCUMENT_ATTACHMENT;

select count(*) from my_parent_table;

COUNT(*)   
          0 


Of course, this only works if there's only one child. Or the parent row has a row in every child.

You can get around this by adding:

- A TYPE column to the parent (& child) tables
- A virtual column to the parent for each child table. This inspects the TYPE column and returns the PK as appropriate. Otherwise null
- An on delete cascade, deferrable, initially deferred foreign key on these VCs to the appropriate child table

Which looks like:

drop table MY_PARENT_TABLE cascade constraints purge;
drop table MY_CHILD_TABLE_DOCUMENT_ATTACHMENT cascade constraints purge;
drop table MY_CHILD_TABLE_CALENDAR_ALERT cascade constraints purge;

create table MY_PARENT_TABLE (
  PK number(19) primary key, 
  tab_type varchar2(10) not null,
  CUSTOMER_ID number(19), 
  CREATE_DATETIME timestamp(6), 
  DESCRIPTION varchar2(500 char), 
  ACTION_TYPE varchar2(40 char),
  unique ( tab_type, pk ) 
);

create table MY_CHILD_TABLE_DOCUMENT_ATTACHMENT (
  PK number(19) primary key, 
  tab_type varchar2(10) not null
    check ( tab_type = 'DOCUMENT' ),
  DOCUMENT_ID number(19), 
  foreign key (PK) references MY_PARENT_TABLE (PK) 
    on delete cascade, 
  foreign key (PK, tab_type) references MY_PARENT_TABLE (PK, tab_type) 
    on delete cascade,
  unique ( tab_type, pk ) 
);

create table MY_CHILD_TABLE_CALENDAR_ALERT (
  PK number(19) primary key, 
  tab_type varchar2(10) not null
    check ( tab_type = 'CALENDAR' ),
  CALENDAR_ID number(19), 
  APPOINTMENT date, 
  foreign key (PK) references MY_PARENT_TABLE (PK) 
    on delete cascade, 
  foreign key (PK, tab_type) references MY_PARENT_TABLE (PK, tab_type) 
    on delete cascade,
  unique ( tab_type, pk ) 
);

alter table MY_PARENT_TABLE
  add ( 
    doc_vc as ( case when tab_type = 'DOCUMENT' then pk end ),
    cal_vc as ( case when tab_type = 'CALENDAR' then pk end ),
    constraint doc_fk 
      foreign key ( doc_vc ) 
      references MY_CHILD_TABLE_DOCUMENT_ATTACHMENT ( pk )
      on delete cascade
      deferrable initially deferred,
    constraint cal_fk
      foreign key ( cal_vc ) 
      references MY_CHILD_TABLE_CALENDAR_ALERT ( pk )
      on delete cascade
      deferrable initially deferred
  );
  
insert into MY_PARENT_TABLE ( pk, tab_type, customer_id, create_datetime )
  values ( 1, 'DOCUMENT', 1, sysdate );
insert into MY_CHILD_TABLE_DOCUMENT_ATTACHMENT 
  values ( 1, 'DOCUMENT', 1 );
  
insert into MY_PARENT_TABLE ( pk, tab_type, customer_id, create_datetime )
  values ( 2, 'CALENDAR', 1, sysdate );
insert into MY_CHILD_TABLE_CALENDAR_ALERT 
  values ( 2, 'CALENDAR', 1, sysdate );
commit;

select count(*) from my_parent_table;

COUNT(*)   
          2 

delete MY_CHILD_TABLE_DOCUMENT_ATTACHMENT;
select count(*) from my_parent_table;

COUNT(*)   
          1

delete MY_CHILD_TABLE_CALENDAR_ALERT;
select count(*) from my_parent_table;

COUNT(*)   
          0 


As a side bonus, you've now also got constraints enforcing the exclusive arc relationship. I.e. that each parent row has an entry in only one child table.

You can read more about enforcing the exclusion relationship at:

https://blogs.oracle.com/sql/implementing-a-mutually-exclusive-relationship-in-the-database
https://blogs.oracle.com/sql/protecting-peoples-privacy%3a-implementing-cross-table-constraints-part-ii

Rating

  (2 ratings)

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

Comments

Don Simpson, June 03, 2019 - 5:06 pm UTC

Ah, I see. That's quite elegant. Too bad our data model isn't.
Chris Saxon
June 04, 2019 - 10:38 am UTC

:( If you can't add the TYPE columns, you may be stuck with triggers to manage this...

Don Simpson, June 04, 2019 - 3:19 pm UTC

I just meant that we do not have an elegant data model. Your solution works fine.
Chris Saxon
June 04, 2019 - 4:44 pm UTC

That's what I understood you meant! ;)

More to Explore

SQL

The Oracle documentation contains a complete SQL reference.