Implementing a “Both, Either-or, but Not Null” Requirement in a Database

后端 未结 4 1452
谎友^
谎友^ 2021-01-21 16:11

I have requirement for a web app that states that a user should be able to either upload an instruction document(s) (.pdf, .doc, .txt) or provide text for the instructions. The

相关标签:
4条回答
  • 2021-01-21 16:38

    If a user submitted text, could your application save it as a .txt file? This way you would only have to worry about dealing with files.

    0 讨论(0)
  • 2021-01-21 16:39

    Ypercube's answer is fine, except this can, in fact, be done purely through declarative integrity while keeping separate tables. The trick is to combine deferred circular FOREIGN KEYs with a little bit of creative denormalization:

    enter image description here

    CREATE TABLE Instruction (
        InstructionId INT PRIMARY KEY,
        TextId INT UNIQUE,
        DocumentId INT UNIQUE,
        CHECK (
            (TextId IS NOT NULL AND InstructionId = TextId)
            OR (DocumentId IS NOT NULL AND InstructionId = DocumentId)
        )
    );
    
    CREATE TABLE Text (
        InstructionId INT PRIMARY KEY,
        FOREIGN KEY (InstructionId) REFERENCES Instruction (TextId) ON DELETE CASCADE
    );
    
    CREATE TABLE Document (
        InstructionId INT PRIMARY KEY,
        FOREIGN KEY (InstructionId) REFERENCES Instruction (DocumentId) ON DELETE CASCADE
    );
    
    ALTER TABLE Instruction ADD FOREIGN KEY (TextId) REFERENCES Text DEFERRABLE INITIALLY DEFERRED;
    ALTER TABLE Instruction ADD FOREIGN KEY (DocumentId) REFERENCES Document DEFERRABLE INITIALLY DEFERRED;
    

    Inserting Text is done like this:

    INSERT INTO Instruction (InstructionId, TextId) VALUES (1, 1);
    INSERT INTO Text (InstructionId) VALUES (1);
    COMMIT;
    

    Inserting Document like this:

    INSERT INTO Instruction (InstructionId, DocumentId) VALUES (2, 2);
    INSERT INTO Document (InstructionId) VALUES (2);
    COMMIT;
    

    And inserting both Text and Document like this:

    INSERT INTO Instruction (InstructionId, TextId, DocumentId) VALUES (3, 3, 3);
    INSERT INTO Text (InstructionId) VALUES (3);
    INSERT INTO Document (InstructionId) VALUES (3);
    COMMIT;
    

    However, trying to insert Instruction alone fails on commit:

    INSERT INTO Instruction (InstructionId, TextId) VALUES (4, 4);
    COMMIT; -- Error (FOREIGN KEY violation).
    

    Attempting to insert the "mismatched type" also fails on commit:

    INSERT INTO Document (InstructionId) VALUES (1);
    COMMIT; -- Error (FOREIGN KEY violation).
    

    And of course, trying to insert bad values into Instruction fails (this time before commit):

    INSERT INTO Instruction (InstructionId, TextId) VALUES (5, 6); -- Error (CHECK violation).
    INSERT INTO Instruction (InstructionId) VALUES (7); -- Error (CHECK violation).
    
    0 讨论(0)
  • 2021-01-21 16:49

    Something feels a bit off here

    1. There is no UserID in this schema, so it should be added to the Instruction table.

    2. If a user does not upload anything, there will (should) be no entry for that user in the Instruction table.

    3. So the problem -- as stated -- is not about placing constraints on these three tables.

    4. When loading this structure, use a stored procedure and/or a transaction -- to make sure that at least one of the child record gets populated. Though, this has nothing to do with the business requirement that user has to upload something.

    0 讨论(0)
  • 2021-01-21 17:01

    I think that this cannot be done with Declarative Referential Integrity alone - not if your design has these 3 separate tables.

    You'll have to ensure that all Insert/Delete/Update operations are done within transactions (stored procedures) that enforce such a requirement - so no row is ever inserted or left in table Instruction without a relative row in either one of the 2 other tables.


    If you don't mind having nullable fields, you could merge the 3 tables into one and use a CHECK constraint:

    CREATE TABLE Instruction
    ( InstructionID INT          NOT NULL
    , Text          VARCHAR(255) NULL
    , Filepath      VARCHAR(255) NULL
    , PRIMARY KEY (InstructionID)
    , CONSTRAINT Instruction_has_either_text_or_document 
        CHECK (Text IS NOT NULL OR FilePath IS NOT NULL)
    ) ;
    
    0 讨论(0)
提交回复
热议问题