Monday, October 16, 2006

Mimicking Deferrable Constraints With Persisted Computed Columns.

In Oracle you can utilize deferrable constraints (when RI checks are delayed until commit time) - this is very convenient in some cases.
In SQL Server deferrable constraints are not supported, but you can mimick this behaviour using persisted computed columns.
For instance, suppose two columns in your child table reference one and the same primary key as follows:

CREATE TABLE Employee(EmployeeName VARCHAR(30) NOT NULL PRIMARY KEY, Extension CHAR(4))
go
CREATE TABLE Task(CompletedBy VARCHAR(30) NOT NULL, ReviewedBy VARCHAR(30) NULL, Description VARCHAR(30))
go
ALTER TABLE Task ADD CONSTRAINT TaskCompletedFK FOREIGN KEY(CompletedBy) REFERENCES Employee(EmployeeName)
go
ALTER TABLE Task ADD CONSTRAINT TaskReviewedByFK FOREIGN KEY(ReviewedBy) REFERENCES Employee(EmployeeName)
go

Also suppose that your primary key is not immutable and you need to cascade the changes in the parent's primary key down to the child table

ALTER TABLE Task DROP CONSTRAINT TaskCompletedFK
go
ALTER TABLE Task DROP CONSTRAINT TaskReviewedByFK
go

You can create one foreign key with cascading updates all right:

ALTER TABLE Task ADD CONSTRAINT TaskCompletedFK FOREIGN KEY(CompletedBy) REFERENCES Employee(EmployeeID)
ON UPDATE CASCADE
go

But the second constraint cannot be not created with ON UPDATE CASCADE ON DELETE CASCADE option:

ALTER TABLE Task ADD CONSTRAINT TaskReviewedByFK FOREIGN KEY(ReviewedBy) REFERENCES Employee(EmployeeID)
ON UPDATE CASCADE
go

Msg 1785, Level 16, State 0, Line 2
Introducing FOREIGN KEY constraint 'TaskReviewedByFK' on table 'Task' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Msg 1750, Level 16, State 0, Line 2
Could not create constraint. See previous errors.

You have no choice but implement cascading updates manually.
Note that dealing with mutating primary keys in triggers is very inconvenient to put it mildly. However, you can add a couple of columns to your table and easily cascade the changes without troubling yourself too terribly much. Add two columns and a constraint as follows:

ALTER TABLE Task ADD InTransientState CHAR(1) CHECK(InTransientState IN('Y', 'N'))
go
ALTER TABLE Task ADD ReviewedByNotTransient AS CASE WHEN InTransientState = 'N' THEN ReviewedBy END PERSISTED
go
ALTER TABLE Task ADD CONSTRAINT TaskReviewedByFK FOREIGN KEY(ReviewedByNotTransient) REFERENCES Employee(EmployeeName)
go

Let us populate the tables

INSERT Employee VALUES('Doe, John', '1234')
INSERT Employee VALUES('Doe, Jane', '1235')
INSERT Employee VALUES('Jill, Ruben', '1236')
go
INSERT Task(CompletedBy, ReviewedBy, InTransientState) VALUES('Doe, John', 'Doe, Jane', 'N')
INSERT Task(CompletedBy, ReviewedBy, InTransientState) VALUES('Doe, Jane', 'Jill, Ruben', 'N')
go

Note that a straightforward UPDATE will not work, because it is not cascaded down to ReviewedBy column:

UPDATE Employee SET EmployeeName = 'Doe, Janette' WHERE EmployeeName = 'Doe, Jane'
/*
Msg 547, Level 16, State 0, Line 1
The UPDATE statement conflicted with the REFERENCE constraint "TaskReviewedByFK". The conflict occurred in database "Sandbox", table "dbo.Task", column 'ReviewedByNotTransient'.
The statement has been terminated.
*/

But a slightly more complex script will work all right (error handling skipped for brevity):

BEGIN TRAN
--- Turn off TaskReviewedByFK temporarily for Doe, Jane only
UPDATE Task SET InTransientState = 'Y' WHERE ReviewedBy = 'Doe, Jane'
--- update the PK
UPDATE Employee SET EmployeeName = 'Doe, Janette' WHERE EmployeeName = 'Doe, Jane'
--- change the FK accordingly and get the rows back into RI scope
UPDATE Task SET ReviewedBy = 'Doe, Janette', InTransientState = 'N' WHERE ReviewedBy = 'Doe, Jane'
COMMIT
-- verify that it worked. Note that changes to CompletedBy cascaded automatically.
SELECT * FROM Task

As you have seen, a persisted computed column allowed you to postpone RI verification.

1 Comments:

At 4:28 AM, Anonymous Anonymous said...

that is scarry as hell.
I think I will stick to triggers to inforce 'possible' cyclic casdes

 

Post a Comment

<< Home