SQL Server 2005 - 编写插入和更新触发器以进行验证

时间:2014-12-15 12:09:35

标签: sql sql-server tsql sql-server-2005 triggers

我在SQL上非常糟糕,所以我需要有人检查我的触发器查询并告诉我它是否解决了问题以及它是否可以接受。这些要求有点令人费解,所以请耐心等待。

假设我有一个像这样声明的表:

CREATE TABLE Documents
(
    id int identity primary key,
    number1 nvarchar(32),
    date1 datetime,
    number2 nvarchar(32),
    date2 datetime
);

对于此表,必须遵守以下约束:

  • 至少应填写一个数字日期对(数字和日期字段均不为空)。
  • 如果number1和date1都不为null,则该对唯一标识记录。如果两个字段都不为空,则不能有两个具有相同number1和date1的记录。
  • 如果number1或date1为null,则记录由number2-date2对唯一标识。

是的,存在标准化不佳的问题,但我对此无能为力。

据我所知,我无法在数字日期对上编写唯一索引来检查SQL Server 2005中的某些值是否为空。因此,我尝试使用触发器验证约束。

最后一个要求 - 触发器应该没有自己的插入,只有验证检查。这就是我想出的:

CREATE TRIGGER validate_requisite_uniqueness
ON [Documents]
FOR INSERT, UPDATE
AS
BEGIN
    DECLARE @NUMBER1 NVARCHAR (32)
    DECLARE @DATE1 DATETIME
    DECLARE @NUMBER2 NVARCHAR (32)
    DECLARE @DATE2 DATETIME
    DECLARE @DATETEXT VARCHAR(10)
    DECLARE inserted_cursor CURSOR FAST_FORWARD FOR SELECT number1, date1, number2, date2 FROM Inserted

    IF NOT EXISTS (SELECT * FROM INSERTED)
        RETURN

    OPEN inserted_cursor
    FETCH NEXT FROM inserted_cursor into @NUMBER1, @DATE1, @NUMBER2, @DATE2

    WHILE @@FETCH_STATUS = 0
    BEGIN   

        IF (@NUMBER1 IS NULL OR @DATE1 IS NULL)
        BEGIN
            IF (@NUMBER2 IS NULL OR @DATE2 IS NULL)
            BEGIN
                ROLLBACK TRANSACTION
                RAISERROR ('Either the first or the second number-date pair should be filled.', 10, 1)## Heading ##
            END
        END

        IF (@NUMBER1 IS NOT NULL AND @DATE1 IS NOT NULL)
        BEGIN
            IF ((SELECT COUNT(*) FROM Documents WHERE number1 = @NUMBER1 AND date1 = @DATE1) > 1)
            BEGIN
                ROLLBACK TRANSACTION

                SET @DATETEXT = CONVERT(VARCHAR(10), @DATE1, 104)
                RAISERROR ('A document with the number1 ''%s'' and date1 ''%s'' already exists.', 10, 1, @NUMBER1, @DATETEXT)
            END
        END
        ELSE IF (@NUMBER2 IS NOT NULL AND @DATE2 IS NOT NULL) /*the DATE2 check is redundant*/
        BEGIN
            IF ((SELECT COUNT(*) FROM Documents WHERE number2 = @NUMBER2 AND date2 = @DATE2) > 1)
            BEGIN
                ROLLBACK TRANSACTION

                SET @DATETEXT = CONVERT(VARCHAR(10), @DATE2, 104)
                RAISERROR ('A document with the number2 ''%s'' and date2 ''%s'' already exists.', 10, 1, @NUMBER2, @DATETEXT)
            END
        END

        FETCH NEXT FROM inserted_cursor 
    END

    CLOSE inserted_cursor
    DEALLOCATE inserted_cursor
END

请告诉我这个解决方案的写作和效率如何。 我可以提出几个问题:

  • 如果批量修改,此触发器是否会针对现有行和新插入/更新的行正确验证?它应该,因为修改已经应用于本交易范围内的表,对吧?
  • 是否正确处理了约束违规?意思是,我是否正确使用回滚事务和raiserror对?
  • “IF NOT EXISTS(SELECT * FROM INSERTED)RETURN”语句是否正确使用?
  • 是否使用COUNT来检查可接受的约束,或者我应该使用其他方式来检查数字对日期的唯一性?
  • 此解决方案可以在执行速度方面进行优化吗?我应该在两个数字日期对上添加非唯一索引吗?

感谢。

修改

使用检查约束和索引视图的解决方案,基于Damien_The_Unbeliever的答案:

CREATE TABLE dbo.Documents
(
    id int identity primary key,
    number1 nvarchar(32),
    date1 datetime,
    number2 nvarchar(32),
    date2 datetime,
    constraint CK_Documents_AtLestOneNotNull CHECK (
        (number1 is not null and date1 is not null) or
        (number2 is not null and date2 is not null)
    )
);
go
create view dbo.UniqueDocuments
with schemabinding
as
    select
        CASE WHEN (number1 is not null and date1 is not null)
            THEN CAST(1 AS BIT)
            ELSE CAST(0 AS BIT)
        END as first_pair_filled,
        CASE WHEN (number1 is not null and date1 is not null)
            THEN number1
            ELSE number2
        END as number,
        CASE WHEN (number1 is not null and date1 is not null)
            THEN date1
            ELSE date2
        END as [date]
    from
        dbo.Documents
go
create unique clustered index IX_UniqueDocuments on dbo.UniqueDocuments(first_pair_filled,number,[date])
go

2 个答案:

答案 0 :(得分:1)

我会避开触发器,并使用检查约束和indexed view

CREATE TABLE dbo.Documents
(
    id int identity primary key,
    number1 nvarchar(32),
    date1 datetime,
    number2 nvarchar(32),
    date2 datetime,
    constraint CK_Documents_AtLestOneNotNull CHECK (
        (number1 is not null and date1 is not null) or
        (number2 is not null and date2 is not null)
    )
);
go
create view dbo.UniqueDocuments
with schemabinding
as
    select
        COALESCE(number1,number2) as number,
        COALESCE(date1,date2) as [date]
    from
        dbo.Documents
go
create unique clustered index IX_UniqueDocuments on dbo.UniqueDocuments(number,[date])
go

其优点在于,虽然有一些类似触发器的"由于索引视图的行为,它经过良好测试的代码已经深入集成到SQL Server中。

答案 1 :(得分:0)

我会使用这个逻辑(我没有输入它,因为它需要很长时间),并且肯定在IF EXISTS()语句中使用SELECT 1 FROM ...因为它有助于提高性能。同样删除像marc_s所说的游标。

CREATE TRIGGER trg_validate_requisite_uniqueness
ON dbo.[Documents]
AFTER INSERT, UPDATE
AS

 DECLARE @Number1 NVARCHAR(100) = (SELECT TOP 1 number1 FROM dbo.Documents ORDER BY Id DESC)
 DECLARE @Date1 DATETIME        = (SELECT TOP 1 date1 FROM dbo.Documents ORDER BY Id DESC)
 DECLARE @Number2 NVARCHAR(100) = (SELECT TOP 1 number2 FROM dbo.Documents ORDER BY Id DESC)
 DECLARE @Date2 DATETIME        = (SELECT TOP 1 date2 FROM dbo.Documents ORDER BY Id DESC)
 DECLARE @DateText NVARCHAR(100)

IF EXISTS (SELECT 1 FROM dbo.Documents AS D 
INNER JOIN INSERTED AS I ON D.id = I.id WHERE I.Number1 IS NULL AND I.number2 IS  NULL)       
BEGIN   
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second number pair should be filled.', 10, 1) 
END  

ELSE IF EXISTS (SELECT 1 FROM dbo.Documents AS D 
       INNER JOIN INSERTED AS I ON D.id = I.id WHERE I.Date1 IS NULL AND I.Date2 IS NULL)
BEGIN     
  ROLLBACK TRANSACTION
  RAISERROR ('Either the first or the second date pair should be filled.', 10, 1) 
END


ELSE IF EXISTS (SELECT 1 FROM dbo.Documents AS D 
           GROUP BY D.number1, D.date1 HAVING COUNT(*) >1
          )

BEGIN 
  ROLLBACK TRANSACTION
  SET @DateText = (SELECT CONVERT(VARCHAR(10), @Date1, 104))
  RAISERROR ('Cannot have duplicate values', 10, 1, @Number1, @DateText )
END