Tanuki
Tanuki

Reputation: 363

Condition in trigger failing to rollback

I'm trying to create a trigger on insert in the [user_dance_style] table of the following database.

The [user_dance_style] table has two columns [id_user] and [style_ref]. There are 3 styles available and each id_user can have one to three of the syles. 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.

In clear I just want to be able to make a new insert if the id_user hasn't all 3 styles already and doesn't have the style that I'm trying to insert.

My trigger looks like this:

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

It seems that the problem is here:

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

The tables' structure is:

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

If I run:

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]

I get these results:

result

The id_user 1 has all 3 styles trying to insert with:

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

This throws an error

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

but the error should be

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

The id_user 3 has only style 1 and 2 so I should be able to insert a style 3 but when I try:

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

I got the error:

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

How can I fix the code to achieve the desired result?

Upvotes: 0

Views: 546

Answers (2)

Dale K
Dale K

Reputation: 27201

As noted in the comments the inserted pseudo table could have 0-N rows in it, so you cannot assume a single row. And as with all T-SQL you should look for a set based operation before using a procedural one.

The actual error you reported was because even though you called ROLLBACK you didn't RETURN and therefore the trigger continued running and attempted a second ROLLBACK which is what caused your error.

In the set based solution below I am considering all users involved in the update, grouping by them, then using the having clause to exclude any users with 3 or less rows (first test).

And in the second test I am doing the same thing, but now grouping by style_ref as well, and checking for more than 1 row - because at this point in the trigger, the INSERT that fired the trigger has already taken place, so any duplicate row is already in the table, hence why checking for more than one row.

Here is your corrected trigger, using set based logic:

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

Note: Having re-read the question I think you only need the second check because it automatically covers the first. And as others have said a unique constraint would be the best way to accomplish this - assuming that it allowed for your assignment.

Upvotes: 1

Ronen Ariely
Ronen Ariely

Reputation: 2434

Note! You asked only about INSERT which is cover using triggers well. Since I cannot add this as comment, I will add it as "answer" since it is important and maybe it is what you actually need

If you want to cover any change data in the table and not only INSERT (UPDATE as well for example), then there is no reason to use triggers. Triggers has multiple cases which are not enforced (for example bulk insert)

In this case, all you need is simple 2 constraint (assuming I understood your need)

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

Since you can have only the values 1,2,3 and you have UNIQUE for each user, this confirm that you can only have 1, or , 2 , or 3 for each user and each one of these can appear once only.

Upvotes: 2

Related Questions