MSSQL:避免CHECK约束的Update语句

时间:2012-08-21 17:21:53

标签: sql sql-server-2000 constraints

在MS2000中工作,我有一个名为JobOwners的表,它将Jobs(JPSID)映射到拥有它们的Employees(EmpID)。它还包含他们开始拥有该作业的日期(DateStarted),他们停止拥有该作业的日期(DateEnded)以及所有权是否处于活动状态(IsActive)。看起来像这样。

CREATE TABLE JobOwners
(
    LogID int NOT NULL IDENTITY(1,1) PRIMARY KEY,
    JPSID int NOT NULL FOREIGN KEY REFERENCES JobsPerShift(JPSID),
    EmpID int NOT NULL FOREIGN KEY REFERENCES Employees(EmpID),
    DateStarted datetime,
    DateEnded datetime,
    IsActive tinyint NOT NULL   
)

应该没有活动的JPSID重复项,尽管非活动重复项应该没问题。通过一些研究,我发现我可以使用CHECK约束上的函数来完成此操作。

CREATE FUNCTION CheckActiveCount(@JPSID INT) 
RETURNS INT AS 
BEGIN
    DECLARE @result INT
    SELECT @result = COUNT(*) FROM JobOwners WHERE JPSID = @JPSID AND IsActive = 1
    RETURN @result
END
GO

ALTER TABLE JobOwners 
 ADD CONSTRAINT CK_JobOwners_IsActive
 CHECK ((IsActive = 1 AND dbo.CheckActiveCount(JPSID) <= 1) OR (IsActive = 0))

这很好用。它将允许我插入带有IsActive 1的JPSID 2,因为没有其他活动的JPSID 2.它将允许我插入带有IsActive 0的JPSID 2,因为当IsActive为0时不应用检查。当我尝试时它拒绝但是,再次使用IsActive 1插入JPSID 2,因为它与约束冲突。见下文。

INSERT INTO JobOwners
 VALUES(2,2,NULL,NULL,1)

(1 row(s) affected)

INSERT INTO JobOwners
 VALUES(2,2,NULL,NULL,0)

(1 row(s) affected)

INSERT INTO JobOwners
 VALUES(2,3,NULL,NULL,1)

INSERT statement conflicted with COLUMN FOREIGN KEY constraint...

如果我尝试将其中一个非活动记录更新为活动状态,则会出现此问题。出于某种原因,它允许我。

UPDATE JobOwners SET IsActive = 1
 WHERE LogID = 3

(1 row(s) affected)

如果我再次运行相同的语句,那么它与约束冲突,但不是第一次。这个应用程序的前端永远不会将非活动记录更改为活动,它只会插入一个新记录,但它仍然不是我希望表允许的内容。

我想知道是否最好将活动的工作所有者分开,并为工作所有者历史记录提供单独的表格,但我不确定这里的最佳做法。 任何帮助将不胜感激。

谢谢你,

2 个答案:

答案 0 :(得分:7)

有一个已知问题,某些操作会导致调用绕过UDF的检查约束。该错误列在Connect上(在它被破坏之前,所有链接都是孤立的)并且已经被确认,但是因为无法修复而关闭。这意味着我们需要依赖于变通方法。

我的第一个解决方法可能是而不是更新触发器。感谢Martin让我保持诚实并让我进一步测试 - 我发现我没有防止在同一个声明中将两行更新为1。我已经纠正了逻辑,并添加了一个事务来帮助防止竞争条件:

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

(我的唯一原因是代替而不是触发后是你只更新你需要更新的行,而不是在事后回滚(这不会让你只回滚案例中的无效更新)多行更新))。

这里有很多关于这个问题的讨论:

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

答案 1 :(得分:0)

编辑:巨大的警告。有关您可能希望避免组合UDF和CHECK CONSTRAINTS的原因,请参阅Aaron对this SO question的评论。然而,因为(即使在阅读并理解了Aaron的担忧之后)我的答案在我们的系统中仍然可行,因为1)我们的系统如何工作; 2)我们实际上希望UPDATE语句在他描述的场景中失败,我在这里留下我的答案。因为它总是如此,你应该确保你理解在这个答案中使用脚本的后果。 您已被警告

我按照Aaron(已接受)答案中的链接。在描述中有一个特定的文本引起了我的注意“(检查未作为参数传递的值)”。

这给了我一个主意。我有一个表CustomerId,ContactId,ContactType都是“int”类型的表。 PK是CustomerId和ContactId。我需要能够限制每个CustomerId只有一个“主要”联系人(ContactType = 1),但是人们想要添加的“次要”和“其他”联系人数量。我设置了我的UDF只接受CustomerId作为参数。所以,我也添加了ContactType,但由于我只关心ContactType = 1,我只是在函数内部将ContactType参数硬编码为1。它适用于SQL2012,但我不知道其他版本。

这是一个测试脚本。我将一些语句“挤压”在一起以减少所需的滚动量。 注意:约束条件允许零主要联系人,因为如果您没有先删除现有的主要联系人,则无法将其他联系人设置为主要联系人。

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