Foreign Key Constraints in Oracle - sql

I have Entity Relationship Model (ERD) where entities IndividualCategory and TeamCategory relate to entity Category. Now I want to create tables in Oracle DB. I started like this:
CREATE TABLE Category(
category_id INT PRIMARY KEY,
...
);
CREATE TABLE Individual_category(
category_id INT CONSTRAINT fk_cat_indivcat REFERENCES Category(category_id),
...,
CONSTRAINT pk_indivgamecat PRIMARY KEY (category_id)
);
CREATE TABLE Team_category(
category_id INT CONSTRAINT fk_cat_teamcat REFERENCES Category(category_id),
...,
CONSTRAINT pk_teamcat PRIMARY KEY (category_id)
);
This combination of Foreign key and Primary key constraints assures that for every Individual_category there will be corresponding record in Category "super" table (or "parent" table ?). And there will be only one IndividualCategory record for particular Category record. Same for Team_category.
To enforce inheritance I need one more constraint: A constraint that assures that for every record in Category there will be either record in IndividualCategory (X)OR a record in TeamCategory but not both.
How do I create such constraint ?
EDIT: This is what I meant by 'inheritance in E-R model'. This is from my DB teacher's slides (they call it "Entity sub-type" there but they sometimes call it just inheritance):

A completely different way to do this using deferrable constraints:
CREATE TABLE Category(
category_id INT PRIMARY KEY,
team_category_id INT,
individual_category_id INT,
...
);
CREATE TABLE Individual_category(
individual_category_id INT PRIMARY KEY,
category_id INT NOT NULL,
...,
);
CREATE TABLE Team_category(
team_category_id INT PRIMARY KEY,
category_id INT NOT NULL,
...,
);
Make sure a Category is a TeamCategory xor an IndividualCategory:
alter table Category add constraint category_type_check check
( (team_category_id is null and individual_category_id is not null)
or (team_category_id is not null and individual_category_id is null)
);
Create deferrable integrity constraints so that one can insert a Category and Team/Individual_Category within the same transaction; otherwise, you couldn't insert a Category before the TeamCategory/IndividualCategory, and vice-versa. A catch-22.
alter table category add constraint category_team_fk
foreign key (team_category_id)
references team_category (team_category_id)
deferrable initially deferred;
alter table category add constraint category_individual_fk
foreign key (individual_category_id)
references individual_category (individual_category_id)
deferrable initially deferred;
alter table individual_category add constraint individual_category_fk
foreign_key (category_id)
references category (category_id)
deferrable initially deferred;
alter table team_category add constraint team_category_fk
foreign_key (category_id)
references category (category_id)
deferrable initially deferred;

How one may do this is, using a simplified example:
CREATE TABLE Category(
category_id INT PRIMARY KEY,
category_type varchar2(300) not null,
...
[list of required attributes for only individual category, but nullable],
[list of required attributes for only team category, but nullable]
);
alter table category add constraint check_category_individual check
( category_type <> 'INDIVIDUAL'
or ( category_type = 'INDIVIDUAL'
and [list of individual category attributes IS NOT NULL]
)
);
alter table category add constraint check_category_team check
( category_type <> 'TEAM'
or ( category_type = 'TEAM'
and [list of team category attributes IS NOT NULL]
)
);
You could then create views, like:
create view individual_category as
select category_id, [base category attributes], [individual category attributes]
from category
where category_type = 'INDIVIDUAL;
You can even put an INSTEAD OF trigger on the view so it would be appear to the application to be a table like any other.

Another way to implement complex constraints in the database is using materialized views (MVs).
For this example an MV could be defined as follows:
create materialized view bad_category_mv
refresh complete on commit
as
select c.category_id
from category c
left outer join individual_category i on i.category_id = c.category_id
left outer join team_category i on t.category_id = c.category_id
where ( (i.category_id is null and t.category_id is null)
or (i.category_id is not null and t.category_id is not null)
);
alter table bad_category_mv
add constraint bad_category_mv_chk
check (1=0) deferrable;
So the MV is populated only for categories that break the rule, but then the check constraint ensures that any transaction that results in a row in the MV will fail (since 1=0 is never true).
I have blogged about this approach in the past here.
CAUTION: Although I am interested in this approach I have never used it "in anger" in a production database. Careful benchmarking is needed to ensure that the overhead of the full MV refresh whenever the data is changed is not too high.

ERD inheritance is a classic example of the gen-spec design pattern. There are numerous articles on how to design gen-spec in a relational DBMS like Oracle. you can find some of them by doing a Google search on "generalization specialization relational modeling".
Much of what you will see in these articles has already been outlined by other responses to this question. This topic has surfaced many times in SO. For a sample prior discussion, click here.
The main feature of the classic solution is that the specialized tables have an id column that is both a primary key and a foreign key that references the id column of the generalized table. In this manner, the subentities do not acquire an identity of their own. The feature you really need to watch out for is the constraint that implements disjunction. Not all of the articles enforce this rule in their presented solution.

Related

How can I prevent a table not having references pointing to it?

I have these tables:
CREATE TABLE s_users (
user_id serial PRIMARY KEY,
username text
);
CREATE TABLE s_projects (
project_id serial PRIMARY KEY,
projectname text
);
CREATE TYPE s_MEMBERSHIP_TIER AS ENUM ('pending', 'member', 'admin');
CREATE TABLE s_memberships (
user_id INT NOT NULL,
project_id INT NOT NULL,
membership_tier MEMBERSHIP_TIER,
CONSTRAINT s_one_membership_type
UNIQUE(user_id, project_id)
);
I want to ensure that it will be impossible to get to a situation where:
(1) a membership refers to a project or user that doesn't exist, and
(2) a project is without members.
I think I can achieve (1) by changing the first rows in CREATE TABLE s_memberships ( to
user_id INT NOT NULL REFERENCES s_users ON DELETE CASCADE ON UPDATE CASCADE,
project_id INT NOT NULL REFERENCES s_projects ON DELETE CASCADE ON UPDATE CASCADE,
But how can I avoid a project without members?
I.e.
INSERT INTO s_users (username) values ('mickey');
INSERT INTO s_users (username) values ('donald');
INSERT INTO s_projects (projectname) values ('p1'); -- this should not succeed to create a project without any members.
I've tried something like:
ALTER TABLE s_projects ADD CONSTRAINT pid FOREIGN KEY(project_id) REFERENCES s_memberships(project_id);
But I'm getting an error:
ERROR: there is no unique constraint matching given keys for referenced table "s_memberships"
Is there some other constraint I can add? Or, alternatively, is there a better way to organise my tables?
There are two options I can think of:
You create a foreign key from s_projects to s_membership that identifies a "special member" (project leader?) that must always be there.
You have a column member_count in s_project that is maintained by a trigger on s_membership, so that it always contains the number of members in that project. Then you place a check constraint on s_project that forces that number to be greater than 0.
A project without members is quite tricky. You can't insert a membership unless there is a project. And you want to requite that a project has members.
One solution is to use a deferrable constraint so you can insert both rows at the same time. You can alter the table to defer the constraint check, insert the rows, and then undefer the constraint.
However, I prefer other solutions.
One is to include a member count in projects. Maintaining this requires triggers -- which are yucky -- but you can then get "active" projects using a where clause: where num_members > 0.
Or just create a view:
create view active_projects as
select p.*
from projects p
where exists (select 1 from memberships m where m.project_id = p.project_id);
In other words, these solutions allow "inactive" projects, but then just hide them when desired.

"Multiple" Foreign Key

I have tables:
MUSICIANS (musician_id, ...)
PROGRAMMERS (programmer_id, ...)
COPS (cop_id, ...)
Then I'm going to have a specific table
RICH_PEOPLE (rich_person_id, ...)
where rich_person_id is either musician_id, programmer_id or cop_id. (Assume that all the musician_ids, programmer_ids, cop_ids are different.)
Is it possible to directly create a Foreign Key on the field rich_person_id?
P.S. I would like the database to
ensure that there is a record of either MUSICIANS, PROGRAMMERS or COPS with the same id as the new RICH_PEOPLE record's rich_person_id before inserting it into RICH_PEOPLE
deleting from either MUSICIANS, PROGRAMMERS or COPS would fail (or require cascade deletion) if there a RICH_PEOPLE record with the same id
P.P.S. I wouldn't like
creating an extra table like POSSIBLY_RICH_PEOPLE with the only field possibly_rich_person_id
creating triggers
You can create three nullable foreign keys, one to each foreign table. Then use a CHECK constraint to ensure only one value is not null at any given time.
For example:
create table rich_people (
rich_person_id int primary key not null,
musician_id int references musicians (musician_id),
programmer_id int references programmers (programmer_id),
cop_id int references cops (cop_id),
check (musician_id is not null and programmer_id is null and cop_id is null
or musician_id is null and programmer_id is not null and cop_id is null
or musician_id is null and programmer_id is null and cop_id is not null)
);
This way, referential integrity will be ensured at all times. Deletions will require cascade deletion or other strategy to keep data integrity.
You do this in a somewhat different way:
Create a table people with a person_id.
Use this key as the primary key (and foreign key) for each of your occupation tables.
Use this key as the primary key (and foreign key) for your rich_people table.
Postgres supports a concept called "inheritance", which facilitates this type construct. Your occupation tables can "inherit" columns from people.

What is the simplest way to delete a child row when its parent is deleted, without knowing what its parent is?

Given multiple entity types:
Cluster
Hypervisor
VirtualMachine
and given properties that could belong to any one of them (but no more than one per row):
CpuInfo
CpuSpeed
CpuTotal
...
DataStore
...
What is the simplest way to delete a property with its parent?
Attempted Solutions
ON DELETE CASCADE
ON DELETE CASCADE seems to require a nullable foreign key for each possible parent, which strikes me as a poor design:
CREATE TABLE CpuInfo
(
-- Properties
Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
CpuSpeed INT,
AllocatedTotal INT,
CpuTotal INT,
AvailableTotal INT,
-- Foreign keys for all possible parents
ClusterId INT,
HypervisorId INT,
VirtualMachineId INT,
FOREIGN KEY (ClusterId) REFERENCES Cluster(Id) ON DELETE CASCADE,
FOREIGN KEY (HypervisorId) REFERENCES Hypervisor(Id) ON DELETE CASCADE,
FOREIGN KEY (VirtualMachineId) REFERENCES VirtualMachine(Id) ON DELETE CASCADE
);
Junction Tables with Triggers
Parents are related to properties through junction tables. For example:
CREATE TABLE HypervisorCpuInfo
(
HypervisorId INT NOT NULL,
CpuInfoId INT NOT NULL,
FOREIGN KEY (HypervisorId) REFERENCES Hypervisor(Id),
FOREIGN KEY (CpuInfoId) REFERENCES CpuInfo(Id) ON DELETE CASCADE
);
There is then a DELETE trigger for each entity type. The trigger selects the IDs of the entity's properties and deletes them. When the properties are deleted, the child junction rows are then deleted also, via ON CASCADE DELETE.
This doesn't model the business rules very well, though, since it allows the same CpuInfo to belong to multiple entities. It also adds a lot of tables to the design.
Is there a simpler solution?
I think a "junction table" might be fitting for DRYness (it isn't a real junction because of the 1:n relation)
You could call your "junction table" a "super table" (something like "machine" [sorry I'm not native]):
In this table you put all the keys to your properties (make each foreign key column unique to ensure 1:1*). The very type of your "machine" (Cluster,Hypervisor,VirtualMachine) is in the "triple key" you already tried - also in the super-table.
To ensure "machine" is only of one entity add a constraint:
ALTER TABLE CpuInfo WITH CHECK ADD CONSTRAINT [CK_keyIDs] CHECK (
(ClusterId IS NULL AND HypervisorId IS NULL AND VirtualMachineId IS NOT NULL)
OR (ClusterId IS NULL AND HypervisorId IS NOT NULL AND VirtualMachineId IS NULL)
OR (ClusterId IS NOT NULL AND HypervisorId IS NULL AND VirtualMachineId IS NULL)) GO
The good thing is you are quite free with your entities, you could allow a PC to be a Cluster at the same time.
*the key-column! the ID already has to be unique

Updating primary keys in POSTGRESQL

I have a database from previous project that I want to use in another project, from security reasons I need to update the IDs of one of the table. Problem is that the table is heavily referenced by foreign keys from other tables:
CREATE TABLE "table_table" (
"id" serial NOT NULL PRIMARY KEY,
"created" timestamp with time zone NOT NULL,
);
CREATE TABLE "table_photo" (
"id" serial NOT NULL PRIMARY KEY,
"table_id" integer NOT NULL REFERENCES "table_table" ("id") DEFERRABLE INITIALLY DEFERRED,
);
Now if I change the id on table_table the reference from table_photo won't work.
I will probably use something like this to change the IDs:
UPDATE table_table SET id = id + 15613;
I have read somewhere that I could use ON UPDATE CASCADE constraints to do this but I am not very sure how to use it.
btw: I am using Django ORM.
Get the constraint name with \d "table_photo", which shows:
Foreign-key constraints:
"table_photo_table_id_fkey" FOREIGN KEY (table_id) REFERENCES table_table(id) DEFERRABLE INITIALLY DEFERRED
Then replace it with a constraint that has on update cascade:
ALTER TABLE "table_photo"
DROP CONSTRAINT "table_photo_table_id_fkey",
ADD CONSTRAINT "table_photo_table_id_fkey"
FOREIGN KEY ("table_id")
REFERENCES "table_table"
ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED;
Now when you do your UPDATE, referenced row IDs are automatically updated. Adding an index on "table_photo"."table_id" will help a lot.
This can be slow for big tables though. An alternative if you have large tables is to do it in a couple of stages. For table A with field id that's referenced by table B's field A_id:
Add a new column, new_id, to A, with a UNIQUE constraint. Leave it nullable.
Add a new column, A_new_id to table B, giving it a foreign key constraint to A(new_id).
Populate A.new_id with the new values
Do an
UPDATE B
SET A_new_id = A.new_id
FROM A
WHERE B.A_id = A.id;
to do a joined update, setting the new ID values in B.A_new_id to match.
Drop the column B.A_id and rename B.A_new_id to B.A_id.
Drop the column A.id and rename A.new_id to A.id
Create a PRIMARY KEY constraint on the renamed A.id, USING the index created automatically before.
It's a lot more complicated, especially since for big tables you usually want to do each of these steps in batches.
If this seems too complicated, just do it with a cascading foreign key constraint like above.

How do I create a composite foreign key going backwards down a relationship?

Sorry about the title, couldn't think of a better way to write it.
Here's my problem...
I have 2 tables in my database [Drawings] and [Revisions];
[Drawings] 1-----* [Revisions]
ProjectId(pk) ProjectId(pk)(fk)
DrawingNo(pk) DrawingNo(pk)(fk)
RevisionNo(pk)
LatestRevision
There is a foreign key in [revisions] referencing [drawings] on [ProjectId] and [DrawingNo].
I need to implement a way of enforcing that the drawings latest revision number equals a corresponding revision number in the revisions table:
... WHERE [Drawings].[LatestRevision] NOT IN (
SELECT [RevisionNo]
FROM [Revisions]
WHERE [Drawings].[ProjectId] = [Revisions].[ProjectId]
AND [Drawings].[DrawingNo] = [Revisions].[DrawingNo])
How would I put something like this into a foreign key?
I need this to work on sql server 2008 express onwards.
Thanks in advance for any help!
Schema:
TABLE Drawings
( ProjectId varchar,
DrawingNo varchar,
LatestRevisions varchar,
...other columns
PRIMARY KEY(ProjectId, DrawingNo)
)
TABLE Revisions
( ProjectId varchar,
DrawingNo varchar,
RevisionNo varchar,
...other columns
PRIMARY KEY(ProjectId, DrawingNo, RevisionNo)
FOREIGN KEY(ProjectId, DrawingNo) REFERENCES (Drawings(ProjectId, DrawingNo))
)
Drawing 'A' can have revision '1', and Drawing 'B' can have a different revision '1',
Revision number by itself is not unique
I will take the schema as follows:
TABLE drawings
( projectid integer,
drawingno integer,
latestRevision integer,
primary key (projectid, drawingno)
)
TABLE revisions
( revisionno integer primary key,
projectid integer,
drawingno integer,
foreign key (projectid, drawingno)
references (drawings (projectid, drawingno))
)
In this case, I would issue:
ALTER TABLE drawings
ADD FOREIGN KEY (latestRevision)
REFERENCES (revisions(revisionNo))
This would mean that every revisions.revisionNo is unique and the column drawings.latestRevision is a foreign key that references the primary key of revisions table, that is, revisionNo.
Please let me know if there are any changes to the schema you have.
Also, the foreign key is enforced only if it is referencing a primary key of another table. If revisions.revisionno is not a primary key or if the primary key constraint is disabled on this column, then the ALTER TABLE .. ADD FOREIGN KEY statement is bound to return an error.
The following structures replaces your tables with views and looks similar to what you describe, except it's instead maintained behind the scenes rather than being an explicit foreign key. I don't know what operations you'd want to support on Revisions, at the moment I only support INSERT:
create table dbo._Drawings (
ProjectId int not null,
DrawingId int not null,
constraint PK_Drawings PRIMARY KEY (ProjectID,DrawingID)
)
go
create table dbo._Revisions (
ProjectID int not null,
DrawingID int not null,
RevisionNo int not null,
_PreviousRevision as CASE WHEN RevisionNo > 1 THEN RevisionNo - 1 END persisted,
_NextRevision int null,
constraint PK_Revisions PRIMARY KEY (ProjectID,DrawingID,RevisionNo),
constraint FK_Revisions_Drawings FOREIGN KEY (ProjectID,DrawingID)
references _Drawings (ProjectID,DrawingID),
constraint CK_RevisionNos CHECK (RevisionNo >= 1),
constraint UK_Revisions_Previous UNIQUE (ProjectID,DrawingID,_PreviousRevision),
constraint UK_Revisions_Next UNIQUE (ProjectID,DrawingID,_NextRevision),
constraint FK_Revisions_Previous FOREIGN KEY (ProjectID,DrawingID,_PreviousRevision)
references _Revisions (ProjectID,DrawingID,RevisionNo),
constraint FK_Revisions_Next FOREIGN KEY (ProjectID,DrawingID,_NextRevision)
references _Revisions (ProjectID,DrawingID,RevisionNo)
)
The above two tables are the "backing store" for the data. The _Revisions table ensures that the revision sequences are strictly monotonically increasing from 1. Each row maintains a foreign key to its immediate preceding and succeeding revisions, except the first and last, for which NULLs substitute (but the unique constraints ensure only one of each exists for each ProjectID,DrawingID combination.
create view dbo.Drawings
with schemabinding
as
select
d.ProjectID,
d.DrawingID,
r.RevisionNo as LatestRevision
from
dbo._Drawings d
left join
dbo._Revisions r
on
d.ProjectId = r.ProjectID and
d.DrawingId = r.DrawingID and
r._NextRevision is null
The above view mimics your asked for Drawings table and would be used for any actual data access. If you wanted to enforce an invariant that each drawing must have at least one revision, you could switch the left join to an inner join and make this an indexed view. You'd need to add a trigger to support INSERTs, in much the same way as the below does for Revisions, which then populates both tables.
create view dbo.Revisions
with schemabinding
as
select
ProjectID,
DrawingID,
RevisionNo
from
dbo._Revisions
This view creates the impression that Revisions is as simple as in your query
create trigger T_Revisions_I
on dbo.Revisions
instead of insert
as
;with SplitData as (
select ProjectID,DrawingID,RevisionNo,RevisionNo-1 as Prev, Seq
from inserted cross join (select 1 union all select 2) t(Seq)
)
merge into dbo._Revisions r
using SplitData s
on
r.ProjectID = s.ProjectID and
r.DrawingID = s.DrawingID and
(
(s.Seq = 1 and r.RevisionNo = s.Prev) or
(s.Seq = 2 and r.RevisionNo = s.RevisionNo)
)
when matched and s.Seq = 1
then update set _NextRevision = s.RevisionNo
when not matched and s.Seq = 2
then insert (ProjectID,DrawingID,RevisionNo) values (s.ProjectID,s.DrawingID,s.RevisionNo)
;
And finally, this trigger is responsible for maintaining the _Revisions structure in the way that the constraints I created above require. The trick is that we use a MERGE statement so that at the same time as we insert the new row, we also update the previous row so that it's _NextRevision column is no longer null and references the row that we're inserting.
More triggers can be added to support more advanced usage.