触发器中的条件无法回滚

Condition in trigger failing to rollback

我正在尝试在以下数据库的 [user_dance_style] table 中创建插入触发器。

[user_dance_style] table 有两列 [id_user][style_ref]。有 3 种样式可用,每个 id_user 可以有一到三种样式。 在插入时,我想检查 id_user 是否已经有三种样式,如果是的话 ROLLBACK

我还想检查 id_user 是否已经有要在这种情况下插入的样式 ROLLBACK。

很明显,如果 id_user 还没有所有 3 种样式并且没有我要插入的样式,我只想能够进行新的插入。

我的触发器如下所示:

DROP TRIGGER IF EXISTS update_style
GO

CREATE TRIGGER update_style 
ON [user_dance_style] 
AFTER INSERT
AS
BEGIN
    DECLARE @id_user int, @number_of_style int, @style INT

    SELECT @style = style_ref FROM inserted
    SELECT @id_user = id_user FROM inserted

    SELECT @number_of_style = COUNT(*)
    FROM [user_dance_style]
    WHERE id_user = @id_user

    IF @number_of_style > 3
        ROLLBACK

    IF @style IN (SELECT [user_dance_style].[style_ref]
                  FROM [user_dance_style]
                  WHERE [user_dance_style].[id_user] = @id_user)
        ROLLBACK
END

看来问题出在这里:

IF @style IN (SELECT [user_dance_style].[style_ref]
              FROM [user_dance_style]
              WHERE [user_dance_style].[id_user] = @id_user)
        ROLLBACK

table的结构是:

CREATE TABLE [user]
(
    [id_user] INT PRIMARY KEY NOT NULL IDENTITY(1,1),
    [user_name] VARCHAR(45)  NOT NULL UNIQUE,
    [User_Sex] CHAR(1) NOT NULL,
    [date_of_birth] DATE NOT NULL,
    [account_type] INT NOT NULL,
    [id_address] INT NOT NULL,
);

CREATE TABLE [address]
(
    [id_address] INT PRIMARY KEY NOT NULL IDENTITY(1,1),
    [street] VARCHAR(255) NOT NULL,
    [number] INT NOT NULL,
    [locality] VARCHAR(255) NOT NULL,
    [city] VARCHAR(255) NOT NULL,
    [country_code] CHAR(2) NOT NULL
);

CREATE TABLE [membership]
(
    [account_type] INT PRIMARY KEY NOT NULL IDENTITY(1,1),
    [membership_name] VARCHAR(45) UNIQUE NOT NULL,
    [membership_price] DECIMAL(4,2) NOT NULL
);

CREATE TABLE [style]
(
    [style_ref] INT PRIMARY KEY NOT NULL IDENTITY(1,1),
    [style_name] VARCHAR(45) UNIQUE NOT NULL
);

CREATE TABLE [dance]
(
    [id_dance] INT NOT NULL IDENTITY(1,1),
    [dancer_1_id_user] INT,
    [dancer_2_id_user] INT,
    [dance_dtg] DATETIME NOT NULL,
    [style_ref] INT NOT NULL,
    FOREIGN KEY (dancer_1_id_user) REFERENCES [user] (id_user),
    FOREIGN KEY (dancer_2_id_user) REFERENCES [user] (id_user),
    FOREIGN KEY (style_ref) REFERENCES [style] (style_ref)  
);

CREATE TABLE [user_dance_style]
(
    [id_user] INT,
    [style_ref] INT NOT NULL
    FOREIGN KEY (id_user) REFERENCES [user] (id_user),
    FOREIGN KEY (style_ref) REFERENCES [style] (style_ref)
)

ALTER TABLE [user]
     ADD CONSTRAINT fk_user_memebership 
         FOREIGN KEY (account_type) REFERENCES membership (account_type),
         CONSTRAINT fk_user_address 
         FOREIGN KEY (id_address) REFERENCES address (id_address);

-- disable all constraints
EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"

INSERT INTO [membership] ([membership_name], [membership_price])
VALUES ('free', '0'), ('regular', '15'), ('premium', '30')
GO

INSERT INTO [style]([style_name])
VALUES ('Salsa'), ('Bachata'), ('Kizomba') 
GO

INSERT INTO [user] ([user_name], [User_Sex], [date_of_birth], [account_type], [id_address]) 
VALUES ('sara', 'f', '1990-04-23', '1', '1'),
       ('elenor', 'f', '1989-02-18', '1', '2'),
       ('eva', 'f', '1987-01-04','1','3'),
       ('mike', 'm', '1985-05-02', '1', '4'),
       ('phil', 'm', '1985-03-01', '1', '5'),
       ('laurent', 'm', '1986-02-14', '2', '6'),
       ('nidia', 'f', '1985-01-16', '2', '7'),
       ('franz', 'm', '1990-03-17', '2', '8'),
       ('stephan', 'm', '1991-05-23', '2', '9'),
       ('sandra', 'f', '1993-03-25', '3', '10'),
       ('virginie', 'f', '1999-05-03', '3', '11'),
       ('claire', 'f', '1992-02-24', '3', '12'),
       ('laurence', 'f', '1991-04-26', '3', '13'),
       ('pierre', 'm', '1987-02-14', '3', '14'),
       ('thierry', 'm', '1989-01-04', '3', '15'),
       ('nancy', 'f', '1950-04-15', '1', '16'),
       ('cédric', 'm', '1980-02-02', '1', '17')
GO

INSERT INTO [address] ([street], [number], [locality], [city], [country_code])
VALUES ('av de l''exposition', '13', 'laeken', 'bruxelles', 'be'),
       ('rue cans', '2', 'ixelles', 'bruxelles', 'be'),
       ('rue goffart', '32', 'ixelles', 'bruxelles', 'be'),
       ('ch de haecht', '17', 'schaerbeek', 'bruxelles', 'be'),
       ('rue metsys', '108', 'schaerbeek', 'bruxelles', 'be'),
       ('rue du pré', '223', 'jette', 'bruxelles', 'be'),
       ('rue sergent sorenser', '65', 'ganshoren', 'bruxelles', 'be'),
       ('rue d''aumale', '38', 'anderlecht', 'bruxelles', 'be'),
       ('av de fré', '363', 'uccle', 'bruxelles', 'be'),
       ('rue de lisbonne', '52', 'saint gilles', 'bruxelles', 'be'),
       ('av neptune', '24', 'forest', 'bruxelles', 'be'),
       ('av mozart', '76', 'forest', 'bruxelles', 'be'),
       ('rue emile delva', '92', 'laeken', 'bruxelles', 'be'),
       ('av de la chasse', '68', 'etterbeek', 'bruxelles', 'be'),
       ('rue leopold 1', '42', 'laeken', 'bruxelles', 'be'),
       ('av charle woeste', '68', 'jette', 'bruxelles', 'be'),
       ('ch de boondael', '12', 'ixelles', 'bruxelles', 'be')
GO

INSERT INTO [user_dance_style] ([id_user], [style_ref])
VALUES (1, 1), (1, 2), (1, 3),
       (2, 1), (2, 2), (2, 3),
       (3, 1), (3, 2),
       (4, 1), (4, 2), (4, 3),
       (5, 2), (5, 3),
       (6, 1), (7, 3), (8, 3),
       (9, 1), (9, 2), (9, 3),
       (10, 1), (10, 2), (10, 3),
       (11, 3), (12, 2), (13, 2),
       (14, 1), (15, 3), (16, 1)
GO

INSERT INTO [dance]([dancer_1_id_user], [dancer_2_id_user], [dance_dtg], [style_ref])
VALUES (1, 2, convert(datetime, '2019-11-24 10:34:09 PM',20), 3),
       (4, 2, convert(datetime, '2019-11-24 10:50:00 PM',20), 3),
       (3, 5, convert(datetime, '2019-11-24 10:35:00 PM',20), 2),
       (6, 1, convert(datetime, '2019-11-24 10:37:00 PM',20), 1),
       (7, 2, convert(datetime, '2019-11-24 10:37:00 PM',20), 3),
       (8, 1, convert(datetime, '2019-12-03 11:20:03 PM',20), 3),
       (9, 3, convert(datetime, '2019-12-23 10:45:00 AM',20), 1),
       (10, 12, convert(datetime, '2019-12-26 11:20:00 AM',20), 2),
       (11, 4, convert(datetime, '2020-01-02 08:45:00 AM',20), 3),
       (12, 5, convert(datetime, '2020-01-02 11:10:04 AM',20), 2),
       (13, 12, convert(datetime, '2020-02-04 09:25:00 PM',20), 2),
       (14, 10, convert(datetime, '2020-02-25 10:45:00 AM',20), 1),
       (2, 14, convert(datetime, '2020-02-25 08:45:00 PM',20), 1),
       (5, 10, convert(datetime, '2020-03-01 11:15:06 AM',20), 2),
       (17, 2, convert(datetime, '2020-03-04 03:15:06 AM',20), 1)
GO

如果我运行:

SELECT
    [user_dance_style].[id_user], 
    STRING_AGG([user_dance_style].[style_ref], ', ') AS style
FROM  
    [user_dance_style]
GROUP BY  
    [user_dance_style].[id_user]

我得到这些结果:

id_user 1 尝试插入所有 3 种样式:

insert into [user_dance_style]([id_user], [style_ref])
values (1,1)

这会引发错误

Msg 3903, Level 16, State 1, Procedure update_style, Line 16
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.

但错误应该是

The transaction ended in the trigger. The batch has been aborted.

id_user3 只有样式 1 和样式 2 所以我应该可以插入样式 3 但是当我尝试时:

insert into [user_dance_style]([id_user], [style_ref])
values (3,3)

我收到错误:

The transaction ended in the trigger. The batch has been aborted.

如何修改代码以获得所需的结果?

如评论中所述,inserted 伪 table 中可能有 0-N 行,因此您不能假设只有一行。与所有 T-SQL 一样,您应该在使用程序操作之前寻找基于集合的操作。

您报告的实际错误是因为即使您调用了 ROLLBACK 但您没有调用 RETURN 因此触发器继续 运行 并尝试了第二个 ROLLBACK是什么导致了你的错误。

在下面的基于集合的解决方案中,我正在考虑更新中涉及的所有用户,按他们分组,然后使用 having 子句排除具有 3 行或更少行的任何用户(第一次测试)。

在第二次测试中,我做了同样的事情,但现在也按 style_ref 分组,并检查超过 1 行 - 因为此时在触发器中,INSERT 触发触发器已经发生,所以任何重复的行都已经在 table 中,因此为什么要检查不止一行。

这是您更正的触发器,使用基于集合的逻辑:

CREATE TRIGGER update_style 
ON [user_dance_style] 
AFTER INSERT
AS
BEGIN
    SET NOCOUNT ON;

    -- Check for more then 3 records
    -- Not actually required, because already covered by the next check
    /*if exists (
        SELECT 1
        FROM [user_dance_style]
        WHERE id_user = (select id_user FROM inserted)
        group by id_user
        having count(*) > 3
    ) BEGIN
        ROLLBACK;
        RETURN;
    END;*/

    -- Check whether a user has multiple identical records
    -- Because by this point the INSERT has taken place
    if exists (
        SELECT 1
        FROM [user_dance_style]
        WHERE id_user = (select id_user FROM inserted)
        group by id_user, style_ref
        having count(*) > 1
    ) BEGIN
        ROLLBACK;
        -- As mentioned by Smor in a real example you would throw an error here (and remove the RETURN as its no longer needed).
        -- THROW 51000, 'Style failed to be inserted as it already exists.', 1; 
        RETURN;
    END;
END

注意:重新阅读问题后,我认为您只需要第二次检查,因为它会自动覆盖第一次。正如其他人所说,unique constraint 将是完成此任务的最佳方式 - 假设它允许您进行分配。

注意!您只询问了 INSERT,它很好地使用了触发器。由于我无法将其添加为评论,因此我将其添加为 "answer" 因为它很重要,也许这正是您真正需要的

如果您想涵盖 table 中的任何更改数据,而不仅仅是 INSERT(例如 UPDATE),则没有理由使用触发器。触发器有多个未强制执行的情况(例如批量插入)

在这种情况下,您只需要简单的 2 约束(假设我了解您的需求)

-- On insert, I want to check if the id_user has already three styles, if so ROLLBACK
-- I also want to check if the id_user has already the style to be inserted in that case ROLLBACK.
ALTER TABLE user_dance_style
    ADD CHECK (style_ref = 1 or style_ref = 2 or style_ref = 3);
GO
ALTER TABLE user_dance_style
    ADD CONSTRAINT UC_Person UNIQUE (id_user, style_ref);
GO

因为您只能有值 1、2、3,并且每个用户都有 UNIQUE,这确认每个用户只能有 1、或、2 或 3,并且每个用户都可以出现仅此一次。