【问题标题】:MSSQL: Update statement avoiding the CHECK constraintMSSQL:避免 CHECK 约束的更新语句
【发布时间】:2012-08-17 02:20:46
【问题描述】:

在 MS2000 中工作时,我有一个名为 JobOwners 的表,它将工作 (JPSID) 映射到拥有它们的员工 (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)

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

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

谢谢你,

【问题讨论】:

  • 不要为此使用检查约束。您可以在索引视图上使用唯一索引(尽管在 2000 年,您可能需要做更多工作以确保连接使用的 SET 选项与这些兼容)
  • @MartinSmith:不需要索引视图。常规的部分索引就足够了:create unique index idx_active_jpsid on jobowners (jpsid) where isActive = 1 但我认为部分索引仅在 2008 年及以后可用
  • @a_horse_with_no_name 2008 及更高版本,问题标记为 2000
  • @MartinSmith:是的,我知道它被标记为 2000,但不是索引视图在 2000 中也不可用吗?
  • @a_horse_with_no_name - 索引视图在 2000 年推出。只是设置选项的要求有点繁琐(需要开启 arithabort)

标签: sql sql-server-2000 constraints


【解决方案1】:

存在一个已知问题,即某些操作会导致调用 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

【讨论】:

  • 哈,至少我并没有因为一些基本的事情而失去理智。非常感谢您的帮助亚伦。我会给你的解决方法和 Connect Item 中的解决方法,看看结果如何。
  • 如果 2 个并发连接进行更新,这看起来像是存在竞争条件。
  • @Martin 是的,您可以通过交易更好地保护它。
  • Aaron,鉴于您的凭据(感谢您知道什么是“twofer”,因为我有加拿大亲戚,哈哈)我希望您对我的回答以及您是否同意我的“编辑”提出意见并警告或仍然觉得我应该删除我的答案。毕竟,正如马丁所指出的(并且您同意),即使您的回答也有可能成为“问题”。我喜欢过滤索引(由上面的 a_horse_with_no_name 建议),但这需要额外的 IO 和存储来维护索引,并且仍然可能导致 INSERT/UPDATE 失败。如果我的答案有我没有想到的危险,尽量不要成为“积分猎犬”
【解决方案2】:

编辑: 巨大的警告。请参阅 Aaron 对 this SO question 的评论,了解您可能希望避免组合 UDF 和 CHECK CONSTRAINTS 的原因。但是,由于(即使在阅读并理解了 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-20
    • 2021-07-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多