防止基于用户定义函数的结果更新列的触发器
Trigger that prevents update of column based on result of the user defined function
我们有 DVD 出租公司。在此特定场景中,我们仅考虑会员、租赁和会员资格 tables.
任务是编写一个触发器,防止客户收到 DVD
如果他们使用该功能达到会员合同规定的每月 DVD 租赁限额。
我的触发器导致无限循环。它可以在没有 While 循环的情况下工作,但是如果我考虑对 Rental table 进行多次更新,它就无法正常工作。我哪里错了?
-- do not run, infinite loop
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE @MemberId INT
DECLARE @RentalId INT
SELECT * INTO #TempTable FROM inserted
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
IF UPDATE(RentalShippedDate)
BEGIN
IF (SELECT TotalDvdLeft FROM dvd_numb_left(@MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
END;
DELETE FROM #TempTable WHERE RentalID = @RentalId
END;
END;
我的函数如下所示:
CREATE OR ALTER FUNCTION dvd_numb_left(@member_id INT)
RETURNS @tab_dvd_numb_left TABLE(MemberId INT, Name VARCHAR(50), TotalDvdLeft INT, AtTimeDvdLeft INT)
AS
BEGIN
DECLARE @name VARCHAR(50)
DECLARE @dvd_total_left INT
DECLARE @dvd_at_time_left INT
DECLARE @dvd_limit INT
DECLARE @dvd_rented INT
DECLARE @dvd_at_time INT
DECLARE @dvd_on_rent INT
SET @dvd_limit = (SELECT Membership.MembershipLimitPerMonth FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = @member_id))
SET @dvd_rented = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE CONCAT(month(Rental.RentalShippedDate), '.', year(Rental.RentalShippedDate)) = CONCAT(month(GETDATE()), '.', year(GETDATE())) AND Rental.MemberId = @member_id)
SET @dvd_at_time = (SELECT Membership.DVDAtTime FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = @member_id))
SET @dvd_on_rent = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE Rental.MemberId = @member_id AND Rental.RentalReturnedDate IS NULL)
SET @name = (SELECT CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) FROM Member WHERE Member.MemberId = @member_id)
SET @dvd_total_left = @dvd_limit - @dvd_rented
SET @dvd_at_time_left = @dvd_at_time - @dvd_on_rent
IF @dvd_total_left < 0
BEGIN
SET @dvd_total_left = 0
SET @dvd_at_time_left = 0
INSERT INTO @tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(@member_id, @name, @dvd_total_left, @dvd_at_time_left)
RETURN;
END
INSERT INTO @tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(@member_id, @name, @dvd_total_left, @dvd_at_time_left)
RETURN;
END;
很高兴收到任何建议。
您的主要问题是,即使您填充了 #TempTable
,您也永远不会从中提取任何值。
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE @MemberId INT, @RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 @RentalID = RentalId, @MemberId = MemberId FROM #TempTable;
-- Select our values to its working
-- SELECT @RentalID, @MemberId;
IF (SELECT TotalDvdLeft FROM dvd_numb_left(@MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = @RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
END;
调试像这样的简单触发器的一种简单方法是复制 T-SQL,然后创建一个 @Inserted table 变量,例如
DECLARE @Inserted table (RentalId INT, MemberId INT);
INSERT INTO @Inserted (RentalId, MemberId)
VALUES (1, 1), (2, 2);
DECLARE @MemberId INT, @RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
-- IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM @inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 @RentalID = RentalId, @MemberId = MemberId FROM #TempTable;
-- Select our values to its working
SELECT @RentalID, @MemberId;
-- IF (SELECT TotalDvdLeft FROM dvd_numb_left(@MemberId)) <= 0
-- BEGIN
-- ROLLBACK
-- RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
-- END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = @RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
注意:throw
是 抛出 错误的推荐方法,而不是 raiserror
。
另一件需要考虑的事情是,由于一些副作用,您必须尝试将 UDF 转换为内联 TVF。
喜欢这个:
CREATE OR ALTER FUNCTION dvd_numb_left(@member_id INT)
RETURNS TABLE
AS
RETURN
(
WITH
TM AS
(SELECT Membership.MembershipLimitPerMonth AS dvd_limit,
Membership.DVDAtTime AS dvd_at_time,
CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) AS [name]
FROM Membership AS MS
JOIN Member AS M
ON MS.MembershipId = M.MembershipId
WHERE M.MemberId = @member_id
),
TR AS
(SELECT COUNT(Rental.MemberId) AS dvd_rented
FROM Rental
WHERE YEAR(Rental.RentalShippedDate ) = YEAR(GETDATE)
AND MONTH(Rental.RentalShippedDate ) = MONTH(GETDATE)
AND Rental.MemberId = @member_id
)
SELECT MemberId, [Name],
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_limit - dvd_rented END AS TotalDvdLeft,
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_at_time - dvd_on_rent END AS AtTimeDvdLeft
FROM TM CROSS JOIN TR
);
GO
哪个效率会高很多。
获得性能的绝对规则是:尽量保持在"SET BASED"代码中而不是迭代代码。
上述功能可以通过优化器进行优化,而您的功能不能而且将需要 4 次访问相同的表。
我们有 DVD 出租公司。在此特定场景中,我们仅考虑会员、租赁和会员资格 tables.
任务是编写一个触发器,防止客户收到 DVD 如果他们使用该功能达到会员合同规定的每月 DVD 租赁限额。
我的触发器导致无限循环。它可以在没有 While 循环的情况下工作,但是如果我考虑对 Rental table 进行多次更新,它就无法正常工作。我哪里错了?
-- do not run, infinite loop
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE @MemberId INT
DECLARE @RentalId INT
SELECT * INTO #TempTable FROM inserted
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
IF UPDATE(RentalShippedDate)
BEGIN
IF (SELECT TotalDvdLeft FROM dvd_numb_left(@MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
END;
DELETE FROM #TempTable WHERE RentalID = @RentalId
END;
END;
我的函数如下所示:
CREATE OR ALTER FUNCTION dvd_numb_left(@member_id INT)
RETURNS @tab_dvd_numb_left TABLE(MemberId INT, Name VARCHAR(50), TotalDvdLeft INT, AtTimeDvdLeft INT)
AS
BEGIN
DECLARE @name VARCHAR(50)
DECLARE @dvd_total_left INT
DECLARE @dvd_at_time_left INT
DECLARE @dvd_limit INT
DECLARE @dvd_rented INT
DECLARE @dvd_at_time INT
DECLARE @dvd_on_rent INT
SET @dvd_limit = (SELECT Membership.MembershipLimitPerMonth FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = @member_id))
SET @dvd_rented = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE CONCAT(month(Rental.RentalShippedDate), '.', year(Rental.RentalShippedDate)) = CONCAT(month(GETDATE()), '.', year(GETDATE())) AND Rental.MemberId = @member_id)
SET @dvd_at_time = (SELECT Membership.DVDAtTime FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = @member_id))
SET @dvd_on_rent = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE Rental.MemberId = @member_id AND Rental.RentalReturnedDate IS NULL)
SET @name = (SELECT CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) FROM Member WHERE Member.MemberId = @member_id)
SET @dvd_total_left = @dvd_limit - @dvd_rented
SET @dvd_at_time_left = @dvd_at_time - @dvd_on_rent
IF @dvd_total_left < 0
BEGIN
SET @dvd_total_left = 0
SET @dvd_at_time_left = 0
INSERT INTO @tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(@member_id, @name, @dvd_total_left, @dvd_at_time_left)
RETURN;
END
INSERT INTO @tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(@member_id, @name, @dvd_total_left, @dvd_at_time_left)
RETURN;
END;
很高兴收到任何建议。
您的主要问题是,即使您填充了 #TempTable
,您也永远不会从中提取任何值。
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE @MemberId INT, @RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 @RentalID = RentalId, @MemberId = MemberId FROM #TempTable;
-- Select our values to its working
-- SELECT @RentalID, @MemberId;
IF (SELECT TotalDvdLeft FROM dvd_numb_left(@MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = @RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
END;
调试像这样的简单触发器的一种简单方法是复制 T-SQL,然后创建一个 @Inserted table 变量,例如
DECLARE @Inserted table (RentalId INT, MemberId INT);
INSERT INTO @Inserted (RentalId, MemberId)
VALUES (1, 1), (2, 2);
DECLARE @MemberId INT, @RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
-- IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM @inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 @RentalID = RentalId, @MemberId = MemberId FROM #TempTable;
-- Select our values to its working
SELECT @RentalID, @MemberId;
-- IF (SELECT TotalDvdLeft FROM dvd_numb_left(@MemberId)) <= 0
-- BEGIN
-- ROLLBACK
-- RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
-- END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = @RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
注意:throw
是 抛出 错误的推荐方法,而不是 raiserror
。
另一件需要考虑的事情是,由于一些副作用,您必须尝试将 UDF 转换为内联 TVF。
喜欢这个:
CREATE OR ALTER FUNCTION dvd_numb_left(@member_id INT)
RETURNS TABLE
AS
RETURN
(
WITH
TM AS
(SELECT Membership.MembershipLimitPerMonth AS dvd_limit,
Membership.DVDAtTime AS dvd_at_time,
CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) AS [name]
FROM Membership AS MS
JOIN Member AS M
ON MS.MembershipId = M.MembershipId
WHERE M.MemberId = @member_id
),
TR AS
(SELECT COUNT(Rental.MemberId) AS dvd_rented
FROM Rental
WHERE YEAR(Rental.RentalShippedDate ) = YEAR(GETDATE)
AND MONTH(Rental.RentalShippedDate ) = MONTH(GETDATE)
AND Rental.MemberId = @member_id
)
SELECT MemberId, [Name],
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_limit - dvd_rented END AS TotalDvdLeft,
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_at_time - dvd_on_rent END AS AtTimeDvdLeft
FROM TM CROSS JOIN TR
);
GO
哪个效率会高很多。
获得性能的绝对规则是:尽量保持在"SET BASED"代码中而不是迭代代码。
上述功能可以通过优化器进行优化,而您的功能不能而且将需要 4 次访问相同的表。