Enforcing “zero-or-one to one” relationship on SQL database?

后端 未结 4 2094
一整个雨季
一整个雨季 2021-01-06 11:53

I have a Post entity and a FbPost entity.

Post.FbPost is either null or a FbPost, and no two Post en

4条回答
  •  一个人的身影
    2021-01-06 12:34

    Superclass It!

    I think the best solution is to make the Post table a superclass of the FBPost table. (Note: I'm going to use PostID and FBPostID here to make it absolutely clear what I'm referring to.) That is, remove the FBPostID column from your Post table entirely, after updating the FBPost.FBPostID column to match the corresponding PostID. Instead of each FBPostID having its own unique value, it will share the same value as the PostID. In my professional opinion, this is the right way to model a one-to-zero-or-one relationship. It has a huge advantage of being foolproof, and not requiring any additional indexes, triggers, or constraints beyond what a simple FK already provides.

    Note: I'm assuming that we can just update the FBPostID column in FBPost once we drop the (presumed) PK on it. If it is an identity column, then more work will be required--just to add a new column that will become the new PK, and to rename the original column. If column order matters, the data will have to be moved to a new table in order to make a new column appear in the desired location.

    Be sure to think about concurrency while working on this so no data can be improperly read or modified while the change is taking place or before the altered data handling is updated.

    Why you would choose this

    When you're modeling an is-a relationship instead of a has-a relationship, and the two entities participate as zero-to-zero-or-one, then they should share the same surrogate key, because really they're the same entity (you're actually modeling an is-sometimes-a relationship).

    Even though changing to this database model will take a bit of work, it's totally worth it to repair a suboptimal design. Your relational database's design should always leverage the core relational functionality available. Why would you do anything else:

    • You would never write a trigger to enforce a foreign key relationship, since databases offer explicit foreign key constraints that do the job with 100% reliability.
    • You would never modify the system tables directly to create a new table, you'd issue a CREATE TABLE command.
    • And so on and so forth.

    Similarly, you would use a foreign key/primary key combination to model an is-sometimes-a relationship, without having to do weird NULL-allowing unique constraints or filtered indexes to accomplish that purpose.

    Example Change Script

    It's not even too bad! Here is some script that should get you pretty well started:

    BEGIN TRAN;
    ALTER TABLE dbo.Post DROP CONSTRAINT FK_Post_FBPostID; -- referencing FBPost
    -- Also remove all FKs from any other tables referencing FBPostID
    
    ALTER TABLE dbo.FBPost DROP CONSTRAINT PK_FBPost; -- FBPostID column not PK
    ALTER TABLE dbo.FBPost ADD OriginalFBPostID int;
    UPDATE dbo.FBPost WITH (HOLDLOCK, UPDLOCK) SET OriginalFBPostID = FBPostID;
    
    UPDATE F
    SET F.FBPostID = P.PostID
    FROM
       dbo.FBPost F
       INNER JOIN dbo.Post P
          ON F.OriginalFBPostID = P.FBPostID;
    
    -- Perform a similar update on all other tables referencing FBPostID
    
    -- Now, the two most important changes
    ALTER TABLE dbo.FBPost ADD CONSTRAINT PK_FBPost PRIMARY KEY CLUSTERED (FBPostID);
    ALTER TABLE dbo.FBPost ADD CONSTRAINT FK_FBPost_PostID FOREIGN KEY
       REFERENCES dbo.Post (PostID); -- This is where the magic happens!
    
    ALTER TABLE dbo.SomeTable ADD CONSTRAINT FK_SomeTable_FBPostID FOREIGN KEY
       REFERENCES dbo.FBPost (FBPostID); -- and all other tables referencing FBPostID
    
    EXEC sp_rename 'dbo.Post.FBPostID', 'OriginalFBPostID'; -- should stop using it
    ALTER TABLE dbo.Post DROP COLUMN FBPostID; -- Or even better, remove it.
    COMMIT TRAN;
    ALTER TABLE dbo.FBPost DROP COLUMN OriginalFBPostID; -- Meaningless now
    -- If you keep OriginalFBPostID and it is identity, please copy the values
    -- to a new non-identity column and drop it so you don't keep generating more
    

    Finally, modify your Post/FBPost insertion code to use the PostID as the FBPostID.

    New Query Appearance

    Just to drive home the point, your joins between the tables used to look like this:

    SELECT
       P.Something,
       F.SomethingElse
    FROM
       dbo.Post P
       INNER JOIN dbo.FBPost F
          ON P.FBPostID = F.FBPostID
    

    But now they will look like this:

    SELECT
       P.Something,
       F.SomethingElse
    FROM
       dbo.Post P
       INNER JOIN dbo.FBPost F
          ON P.PostID = F.FBPostID -- the important part
    

    The problem is completely solved now! Your tables even take up less space (losing the FBPostID column from Post). You don't have to monkey around with an FK that allows multiple NULLs. With a PK on FBPostID in the FBPost table it is obvious that you can only have one FBPost row per FBPostID.

提交回复
热议问题