Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alternatives to FK SET NULL design that causes cycles [duplicate]

Possible Duplicate:
Foreign key constraint may cause cycles or multiple cascade paths?

I have a database of contact details that contains four primary tables:

  • Organisations
    • Departments
      • Sections
        • People

Here is a diagram (the arrows indicate foreign key constraints):

IMPORTANT: I accidentally marked the green arrow as pointing to per_PerID. It should point to per_SecID

enter image description here

I have created constraints that ensure each department record falls under an organisation, each section falls under a department, and so forth. These foreign key constraints have their action set to CASCADE, so that deleting an organisation will delete all corresponding departments etc.

The problem is that each organisation needs to have a person in charge. This translates to a foreign key constraint on the field that will contain the ID of the person in charge. Creating the constraint was easy, but when I tried to set the ON DELETE action to SET NULL, I got the following error:

Introducing FOREIGN KEY constraint ... on table ... may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.

My reasoning was that cycles would only occur if I set the action to CASCADE, not SET NULL, but evidently this was incorrect.

  1. Person record deleted
  2. org_incharge_PerID set to null in corresponding organisation record
  3. No further propagation occurs. If the organisation had been deleted instead there would be a problem, since departments also refer to organisations. As things stand there shouldn't be an issue.

Is there another way to ensure referential integrity, or should I rethink my table structure, in a way that can avoid the cycles problem entirely?

What I want is for org_incharge_PerID to be constrained to the set of per_PerIDs in the persons table. Additionally, I want for the value to be set to NULL if that person is deleted.

like image 274
Asad Saeeduddin Avatar asked Dec 09 '12 07:12

Asad Saeeduddin


People also ask

Can foreign key have duplicate values?

Foreign keys in SQL are used to create a link between two tables. It is a column that creates a relationship between the tables by taking a reference from another table in the same database using the primary key. The foreign key may contain null and duplicate values.

How can avoid foreign key constraint in SQL?

Use SQL Server Management StudioIn Object Explorer, expand the table with the constraint and then expand the Keys folder. Right-click the constraint and select Modify. In the grid under Table Designer, select Enforce Foreign Key Constraint and select No from the drop-down menu. Select Close.

How do I fix foreign key constraint failure?

The error message itself showing there is a foreign key constraint error, which means you are deleting a parent table where the child table contains the Primary table identifier as a foreign key. To avoid this error, you need to delete child table records first and after that the parent table record.

Can foreign keys be null?

A table can have many foreign keys. A foreign key is nullable if any part is nullable. A foreign key value is null if any part is null.


1 Answers

Once you have run into the cycles warning, the only other way to ensure referential integrity without changing your database structure is to use a trigger for one of the constraints. The problem triggering the warning is twofold:

  1. The Sections table shares two opposite-direction foreign key constraints with the Persons table.
  2. All the four tables have a full cycle that progresses through all of them in the same direction, Person->Section->Department->Organization.

Additionally, there is an issue with the table design: the way it is constructed, a person could be in charge of a section that he is not a member of.

You can get around two these three problems by removing InCharge column from Sections and introducing another table, SectionInCharge, containing columns PerID and SecID, with a clustered index/unique constraint on SecID, and a composite foreign key to the Persons table on both columns. You will need to add a UNIQUE constraint to the two columns in the Persons table so the FK can relate to them.

Now you can set the new FK to ON DELETE CASCADE and delete away.

It is often a superior database design pattern to use the presence or absence of rows to signify something rather than to use a nullable column.

Now that I think about it, the other tables with a "person in charge" column have a similar problem in regards to allowing the assignment of someone who is not properly a member. The only way I know to solve this using FKs (which in my mind is best practice over triggers) is by propagating columns through tables and including them in composite keys. This has the unfortunate side effect of making things a little awkward, but it is not too bad as you can put the most-specific column first and join only on it when possible. So you would put OrgID in the Section table, with a composite FK to the Dept table on (DeptID, OrgID), and then put DeptID and OrgID also in the Persons table. You will then need DeptInCharge and OrgInCharge tables.

One more note about this (that's not really about solving the core problem you presented) that you might be interested in is the possibility of combining your three bottom tables into a single table, OrgUnit. Add an OrgUnitType column and a ParentOrgID column. Either add columns that will be NULL when the thing does not possess that property (as in Sections not having a fax number), or make the existing Organizations, Departments, and Sections tables subtypes of the OrgUnit (example of DB supertype/subtype). The advantage of this is that then you can refer to any one of the three types anywhere you want to: if the FK is to OrgUnit, it can be any one of them. But if it is only allowed to be a Section, then you would FK to that table instead. In fact, that is then perfect for a single OrgUnitInCharge table, with an FK to OrgUnit (OrgUnitID, OrgUnitTypeID).

I know there are some loose ends here that I haven't tied up completely into a pretty package for you, but I hope that what I've described so far is helpful.

P.S. I hope you will forgive me for adding a stylistic consideration. To me it is a lot of extra needless decoration to put a table-signifying prefix on every column in each table. I type 90-110 wpm depending on how good I feel, but I still hate typing any more than I have to. Also, I believe very strongly in keeping a high signal-to-noise ratio in my code, and for me, the prefixes exploding all over the place drastically reduce that. In SQL queries, best practice is to use table aliases and then use those aliases on each column, as in SELECT P.Name FROM dbo.Person P. To resolve the issue of multiple tables having the same column names, I have started making the typical WidgetName column in my Widget table to also be Widget. As one example of why a high signal-to-noise is so important: vrbauxCan proYou verRead artThis nouSentence advQuickly conAnd advEasily? True, the additional prefixes contain useful information, but that useful information is only a little useful, and a lot distracting. If you're designing your database from scratch now, I'd like to request, as someone who could by a strange coincidence some day end up maintaining it (stranger things have happened), that you not use those prefixes. :)

P.P.S. I like PascalCase over under_scores because it's easier to type. The underscore is on the top row and hit by the weakest finger--so it's slower and error prone.

P.P.P.S. I also prefer singular table names, but that's neither here nor there is it? I apologize in advance for hitting you up with style critique.

like image 186
ErikE Avatar answered Sep 25 '22 13:09

ErikE