MSSQL: Update statement avoiding the CHECK constraint

前端 未结 2 568
感情败类
感情败类 2020-12-18 07:41

Working in MS2000, I have a table called JobOwners that maps Jobs (JPSID) to the Employees that own them (EmpID). It also contains the date they started owning that job (Dat

相关标签:
2条回答
  • 2020-12-18 08:13

    There is a known issue where certain operations will lead to a check constraint that calls a UDF to be bypassed. The bug was listed on Connect (before it was scuttled and all the links were orphaned) and it has been acknowledged, but closed as Won't Fix. This means we need to rely on workarounds.

    My first workaround would probably be an instead of update trigger. Thanks to Martin for keeping me honest and for making me test this further - I found that I did not protect against two rows being updated to 1 in the same statement. I've corrected the logic and added a transaction to help prevent a race condition:

    CREATE TRIGGER dbo.CheckJobOwners ON dbo.JobOwners
    INSTEAD OF UPDATE
    AS
    BEGIN
      SET NOCOUNT ON;
      BEGIN TRANSACTION;
    
      UPDATE j SET IsActive = 1 -- /* , other columns */
        FROM dbo.JobOwners AS j INNER JOIN inserted AS i
        ON i.LogID = j.LogID
        WHERE i.IsActive = 1 AND NOT EXISTS 
        (    -- since only one can be active, we don't need an expensive count:
          SELECT 1 FROM dbo.JobOwners AS j2
            WHERE j2.JPSID = i.JPSID
            AND j2.IsActive = 1 AND j2.LogID <> i.LogID
        )
        AND NOT EXISTS 
        (    -- also need to protect against two rows updated by same statement: 
          SELECT 1 FROM inserted AS i2
            WHERE i2.JPSID = i.JPSID
            AND i2.IsActive = 1 AND i2.LogID <> i.LogID
        );
    
      -- *if* you want to report errors:
      IF (@@ROWCOUNT <> (SELECT COUNT(*) FROM inserted WHERE IsActive = 1))
        RAISERROR('At least one row was not updated.', 11, 1);
    
      -- assume setting active = 0 always ok & that IsActive is not nullable
      UPDATE j SET IsActive = 0 -- /* , other columns */
        FROM dbo.JobOwners AS j INNER JOIN inserted AS i
        ON j.LogID = i.LogID
        WHERE i.IsActive = 0;
    
      COMMIT TRANSACTION;
    END
    GO
    

    (My only reason for an instead of instead of after trigger is that you only update the rows you need to update, instead of having to rollback after the fact (which won't let you only rollback the invalid updates in the case of a multi-row update)).

    There is a lot of good discussion about this issue here:

    https://web.archive.org/web/20171013131650/http://sqlblog.com/blogs/tibor_karaszi/archive/2009/12/17/be-careful-with-constraints-calling-udfs.aspx

    0 讨论(0)
  • 2020-12-18 08:28

    EDIT: HUGE caveat. See Aaron's comment on this SO question for reasons you probably want to avoid combining UDFs and CHECK CONSTRAINTS. However, since (even after reading and understanding Aaron's concerns) my answer is still viable in our system because of 1) how our system works and 2) we actually want UPDATE statements to fail in the scenarios he describes, I am leaving my answer here. As it ALWAYS is, it is up to you to make sure you understand the ramifications of using the script in this answer. YOU HAVE BEEN WARNED

    I followed the link in Aaron's (accepted) answer. In the description there was a specific piece of text that caught my attention "(to check values that are not passing as parameters)".

    That gave me an idea. I have a table with columns CustomerId, ContactId, ContactType all of type "int". The PK is CustomerId and ContactId. I needed to be able to limit each CustomerId to only have one "Primary" Contact (ContactType = 1) but as many "secondary" and "other" contacts as people wanted to add. I had setup my UDF to accept only CustomerId as a parameter. So, I added ContactType as well but since I only cared about ContactType = 1, I just hard-coded the ContactType parameter to 1 inside the function. It worked on SQL2012 but I have no idea about other versions.

    Here is a test script. I "squished" together some of the statements to reduce the amount of scrolling needed. Note: the constraint ALLOWS zero Primary Contacts because it would be impossible to set a different Contact as the Primary if you did not first remove an existing Primary.

    CREATE TABLE [dbo].[CheckConstraintTest](
        [CustomerId] [int] NOT NULL,
        [ContactId] [int] NOT NULL,
        [ContactType] [int] NULL,
    CONSTRAINT [PK_CheckConstraintTest] PRIMARY KEY CLUSTERED (
        [CustomerId] ASC,
        [ContactId] ASC
    ))
    GO
    
    CREATE FUNCTION dbo.OnlyOnePrimaryContact (
        @CustId int, @ContactType int ) RETURNS bit
    AS BEGIN
        DECLARE @result bit, @count int
        SET @ContactType = 1 --only care about "1" but needed parm to force SQL to "care" about that column
        SELECT @count = COUNT(*) FROM CheckConstraintTest WHERE [CustomerId] = @CustId AND [ContactType] = @ContactType
        IF @count < 2 SET @result = 1
        ELSE  SET @result = 0
        RETURN @result
    END
    GO
    
    ALTER TABLE [dbo].[CheckConstraintTest] WITH CHECK ADD CONSTRAINT [SinglePrimaryContact] CHECK  (([dbo].[OnlyOnePrimaryContact]([CustomerId],[ContactType])=(1)))
    GO
    
    ALTER TABLE [dbo].[CheckConstraintTest] CHECK CONSTRAINT [SinglePrimaryContact]
    GO
    
    INSERT INTO [CheckConstraintTest] (CustomerId, ContactId, ContactType) 
    VALUES (1,1,1), (1,2,2), (1,3,2), (1,4,2), (2,1,1)
    
    INSERT INTO [CheckConstraintTest] (CustomerId, ContactId, ContactType) 
    VALUES (1,5,1) --This should fail
    
    UPDATE [CheckConstraintTest] --This should fail
    SET ContactType = 1
    WHERE CustomerId = 1 AND ContactId = 2
    
    UPDATE [CheckConstraintTest] --This should work
    SET ContactType = 2
    WHERE CustomerId = 1 AND ContactId = 1
    
    INSERT INTO [CheckConstraintTest] (CustomerId, ContactId, ContactType) 
    VALUES (1,5,1) --This should work now since we change Cust 1, Contact 1, to "secondary" in previous statement
    
    0 讨论(0)
提交回复
热议问题