5

在 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)

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

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

谢谢你,

4

2 回答 2

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

于 2012-08-21T17:59:32.780 回答
0

编辑: 巨大的警告。请参阅 Aaron 对此SO 问题的评论,了解您可能希望避免组合 UDF 和 CHECK CONSTRAINTS 的原因。但是,由于(即使在阅读并理解了 Aaron 的担忧之后)我的答案在我们的系统中仍然可行,因为 1)我们的系统如何工作,以及 2)我们实际上希望 UPDATE 语句在他描述的场景中失败,所以我将答案留在这里. 一如既往,由您来确保您了解在此答案中使用脚本的后果。你被警告了

我按照亚伦(已接受)答案中的链接进行操作。在描述中有一段特定的文字引起了我的注意“(检查未作为参数传递的值)”。

这给了我一个想法。我有一个表,其中包含 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
于 2016-05-09T19:51:07.587 回答