I understand that perhaps the problem is that I use a select on the same table that I update or insert a record, but this trigger throws an exception in most cases. Then what should I rewrite?
The purpose of the trigger is to block inserting or updating entries if the room is already occupied on a certain date, i.e. the dates overlap
CREATE TABLE [dbo].[settlements]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[client_id] [int] NOT NULL,
[checkin_date] [date] NOT NULL,
[checkout_date] [date] NOT NULL,
[room_id] [int] NOT NULL,
[employee_id] [int] NULL
);
ALTER TRIGGER [dbo].[On_Hotel_Settlement]
ON [dbo].[settlements]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #room_id int
DECLARE #checkin_date Date, #checkout_date Date
DECLARE cursor_settlement CURSOR FOR
SELECT room_id, checkin_date, checkout_date
FROM inserted;
OPEN cursor_settlement;
FETCH NEXT FROM cursor_settlement INTO #room_id, #checkin_date, #checkout_date;
WHILE ##FETCH_STATUS = 0
BEGIN
IF EXISTS (SELECT 1
FROM settlements AS s
WHERE s.room_id = #room_id
AND ((s.checkin_date >= #checkin_date AND s.checkin_date <= #checkout_date)
OR (s.checkout_date >= #checkin_date AND s.checkout_date <= #checkout_date)))
BEGIN
RAISERROR ('Room is not free', 16, 1);
ROLLBACK;
END;
FETCH NEXT FROM cursor_settlement INTO #room_id, #checkin_date, #checkout_date;
END;
CLOSE cursor_settlement;
DEALLOCATE cursor_settlement;
RETURN
I tried to test the code by removing the condition with dates and leaving only the room _id, but the trigger does not work correctly in this case either.
I tried query like
IF EXISTS (SELECT 1
FROM settlements AS s
WHERE s.room_id = 9
AND ((s.checkin_date >= '2022-12-10' AND s.checkin_date <= '2022-12-30')
OR (s.checkout_date >= '2022-12-10' AND s.checkout_date <= '2022-12-30')))
BEGIN
RAISERROR ('Room is not free', 16, 1);
END;
and it worked correctly. Problem is that is not working in my trigger
As noted by comments on the original post above, the cursor loop is not needed and would best be eliminated to improve efficiency.
As for the date logic, consider a new record with a check-in date that is the same as the checkout date from a prior record. I believe that your logic will consider this an overlap and throw an error.
My suggestion is that you treat the check-in date as inclusive (that night is in use) and the checkout date as exclusive (that night is not in use).
A standard test for overlapping dates would then be Checkin1 < Checkout2 AND Checkin2 < Checkout1. (Note use of inequality.) It may not be obvious, but this test covers all overlapping date cases. (It might be more obvious if this condition is inverted and rewritten as NOT (Checkin1 >= Checkout2 OR Checkin2 >= Checkout1).)
Also, if you are inserting multiple records at once, I would suggest that you also check the inserted records for mutual conflicts.
Suggest something like:
ALTER TRIGGER [dbo].[On_Hotel_Settlement]
ON [dbo].[settlements]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS(
SELECT *
FROM inserted i
JOIN settlements s
ON s.room_id = i.room_id
AND s.checkin_date < i.checkout_date
AND i.checkin_date < s.checkout_date
AND s.id <> i.id
)
BEGIN
RAISERROR ('Room is not free', 16, 1);
ROLLBACK;
END;
RETURN;
END
One more note: Be careful with an early rollback of a transaction. If your overall logic could potentially execute additional DML after the error is thrown, that would now execute outside the transaction and there would be no remaining transaction to roll back.
Related
There are 2 tables:
Wallets;
Transactions.
There is a stored procedure that handles (I think with ACID operation):
updating on Wallet table
inserting one row into Transactions table
every time it is called.
The issue occurs when there are many calls to the SP at same time, infact the value of PreviousBalance is not correct (sequentially wrong), cause in the SP read old value, meantime another process of call is running.
To understand better look the following screenshot.
There are 3 Transaction with same DT (IDs 1289, 1288, 1287), in all of those PreviouseBalance is equal, but is not correct, because the value for :
Trx ID 1288 should be 180,78 as Balance of previous row;
Trx ID 1289 should be 168,07 = 180,78 - 12,08
I think that the issue is in the SET of #OLDBalance var; at same time those 3 thread read same value, so when the SP goes to INSERT loads same value of PreviousBalance.
How can I do in order to read #OLDBalance correct after commit of one operation?
I tried to set several type of Isolation Levet into SP, the result was the same and sometime went in error for deadlock.
I have the following stored procedure:
Stored Procedure
ALTER PROCEDURE [dbo].[upsMovimenta_internal]
#AccountID int,
#Amount money,
#TypeTransactionID int,
#ProductID int,
#notes nvarchar(max)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #OLDBalance MONEY;
DECLARE #PreviousBalance MONEY;
DECLARE #CurrentBalance MONEY;
DECLARE #Molt float;
BEGIN TRANSACTION;
IF NOT EXISTS( SELECT * FROM Accounts WHERE AccountID = #AccountID)
BEGIN
RaisError(N'Account not found ', 10, 1, N'number', 3);
return (1)
END
SELECT #Molt = Moltiplicatore
FROM TypeTransactions
where TypeTransactionID = #TypeTransactionID
;
IF (#Molt is null )
BEGIN
RaisError(N'Error transaction', 10, 1, N'number', 3);
return (1)
END
SET #Amount = #Amount * #Molt;
--SELECT * FROM Wallets
SELECT TOP 1 #OLDBalance = TotalAmount
FROM Wallets
where AccountID = #AccountID
;
SET #CurrentBalance = #OLDBalance + #Amount;
IF (#ProductID = 1 )
BEGIN
UPDATE Wallets
SET TotalAmount+=#Amount,
Cash+=#Amount
FROM Wallets where AccountID = #AccountID
;
END
IF (#ProductID = 2 )
BEGIN
UPDATE Wallets
SET TotalAmount+=#Amount,
Fun+=#Amount
FROM Wallets where AccountID = #AccountID
;
END
INSERT INTO Transactions
( AccountID, ProductID, DT, TypeTransactionID, Amout, Balance, PreviousBalance, Notes )
VALUES
( #AccountID, #ProductID, GETDATE(), #TypeTransactionID, #Amount, #CurrentBalance, #OLDBalance, #notes)
;
COMMIT TRANSACTION;
return (0)
END
Thank you so much guys
Generally, one way of managing locks on records, is to apply a dummy update on the rows you want to work on, right after starting transaction.
In this case SQL Server guarantees that those rows will be locked and no other transactions can access the rows. So you can change your design to something like this:
begin tran
update myTable
set Field1 = Field1
where someKeyField = 212
-- do the same for other tables that you want to protect against change
-- at this moment all working rows will be locked, other procedure calls will be on hold
-- do your main operations here
commit tran
The issue with this will be the other proc calls will wait and this may degrade performance or even time-out if the traffic is high and your operation in this proc is lengthy
If you are working on high transaction environment, you need to change your design.
Update: Design Suggestion
I don't get why you have PreviousBalance and Balance in your transaction (it is against the design rules, however you can override rule in special case).
Probably you have that to speed up your calculations or make your queries simpler. But it is not good practice in OLTP database.
Rules say you keep the Amount column and calculate PreviousBalance and Balance somewhere else.
You should drop PreviousBalance but keep the Balance column, and every time you insert a transaction, you update (increase/decrease) the Balance column. Plus you need to initialize the Balance column at the first transaction.
This is what I can think of. If I knew your whole system, I would be able to have better ideas though.
I have a trigger that works (it fires when it has to) but I still get an error.
I understand the error but I don't know how to resolve it.
I tried to put some BEGIN TRANSACTION with all the code who go with it but I think my grammar is wrong because I always get a timeout!
So my question is, where exactly do I have to put my BEGIN TRANSACTION statements in my code?
Also, do I need 3 BEGIN TRANSACTION statements since I have 3 ROLLBACK?
Thank you in advance!
My code:
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #IdVol INT, #IdTranche INT,
#AgeMinInserted DATE, #AgeMaxInserted DATE
SELECT #AgeMinInserted = t.TRA_Age_Min
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
SELECT #AgeMaxInserted = t.TRA_Age_Max
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
DECLARE CR_TrancheVol CURSOR FOR
SELECT t.TRA_Vol_Id,t.TRA_Id
FROM Tranche t
JOIN inserted AS i ON t.TRA_Vol_Id = i.TRA_Vol_Id;
OPEN CR_TrancheVol
FETCH CR_TrancheVol INTO #IdVol, #IdTranche
WHILE( ##FETCH_STATUS = 0)
BEGIN
DECLARE #AgeMin DATE, #AgeMax DATE
SELECT #AgeMin = t.TRA_Age_Min
FROM Tranche t
WHERE t.TRA_Id = #IdTranche
SELECT #AgeMax = t.TRA_Age_Max
FROM Tranche t
WHERE t.TRA_Id = #IdTranche
IF #AgeMinInserted > #AgeMin AND #AgeMinInserted < #AgeMax
BEGIN
PRINT 'Trans1'
RAISERROR('Overlap: Date de naissance minimum déjà couverte', 1, 420)
ROLLBACK TRANSACTION
END
IF #AgeMaxInserted > #AgeMin AND #AgeMaxInserted < #AgeMax
BEGIN
PRINT 'Trans2'
RAISERROR('Overlap: Date de naissance maximum déjà couverte', 1, 421)
ROLLBACK TRANSACTION
END
IF #AgeMinInserted < #AgeMin AND #AgeMaxInserted > #AgeMax
BEGIN
PRINT 'Trans3'
RAISERROR('Overlap: Tranche déjà couverte complètement', 1, 422)
ROLLBACK TRANSACTION
END
FETCH CR_TrancheVol INTO #IdVol, #IdTranche
END
CLOSE CR_TrancheVol
DEALLOCATE CR_TrancheVol
END
EDIT:
Okay, so I tried your answer without cursor (I understand that my way was clearly not the best!) but for now it doesn't work.
My goal: I have a DB to book a flight. In this DB, i have a table "Tranche" who contains some dates and some prices (depending when the flight is).
I need to prevent and avoid any overlap of birthdate, for example:
1y-17y: 80€
18y-64y: 120€
So my trigger has to fire when I try to insert 17y-63y: xx € (because I already have a price for those ages).
Sorry if my English is not perfect btw!
Here's my table "Tranche":
https://i.stack.imgur.com/KuQH8.png
TRA_Vol_ID is a foreign key of another table "Vol" who contain the flights
Here's the code I have atm:
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
/*
Some SQL goes here to get the value of Minimum age.
I assuming that it doesn't vary by entry, however,
I don't really have enough information to go on to tell
*/
SET NOCOUNT ON;
DECLARE #MinAge DATE, #MaxAge DATE
SELECT #MinAge = t.TRA_Age_Min
FROM Tranche t
JOIN Vol AS v ON v.VOL_Id = t.TRA_Vol_Id
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
WHERE t.TRA_Id = i.TRA_Id
SELECT #MaxAge = t.TRA_Age_Max
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
JOIN Vol AS v ON v.VOL_Id = t.TRA_Vol_Id
WHERE t.TRA_Id = i.TRA_Id
IF (SELECT COUNT(CASE WHEN i.TRA_Age_Min > #MinAge AND i.TRA_Age_Min < #MaxAge THEN 1 END) FROM inserted i) > 0
BEGIN
RAISERROR('Overlap: Birthday min reached',1,430);
ROLLBACK
END
ELSE IF (SELECT COUNT(CASE WHEN i.TRA_Age_Max > #MinAge AND i.TRA_Age_Max < #MaxAge THEN 1 END) FROM inserted i) > 0
BEGIN
RAISERROR('Overlap: Birthday max reached',1,430);
ROLLBACK
END
END
I don't really know what the OP's goals are here. However, I wanted to post a small example how to do a dataset approach, and how to check all the rows in one go.
At the moment, the trigger the OP has will only "work" if the user is inserting 1 row. Any more, and things aren't going to work properly. Then we also have the problem of the CURSOR. I note that the declaration of the cursors aren't referencing inserted at all, so I don't actually know what their goals are. It seems more like the OP is auditing the data already in the table when a INSERT occurs, not the data that is being inserted. This seems very odd.
Anyway, this isn't a solution for the OP, however, I don't have enough room in a comment to put all this. Maybe it'll push the OP in the right direction.
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
/*
Some SQL goes here to get the value of Minimum age.
I assuming that it doesn't vary by entry, however,
I don't really have enough information to go on to tell
*/
IF (SELECT COUNT(CASE WHEN i.Age < #MinAge THEN 1 END) FROM inserted i) > 0 BEGIN
RAISERROR('Age too low',1,430);
ROLLBACK
END
ELSE
IF (SELECT COUNT(CASE WHEN i.Age > #MaxAge THEN 1 END) FROM inserted i) > 0 BEGIN
RAISERROR('Age too high',1,430);
ROLLBACK
END
END
The question at hand seems to very much be an xy question; the problem isn't the CURSOR or the ROLLBACK, the problems with this trigger are much more fundamental. I'd suggest revising your question and actually explaining your goal of what you want to do with your Trigger. Provide DDL to CREATE your table and INSERT statements for any sample data. You might want to also provide some INSERT statements that will have different results for your trigger (make sure to include ones that have more than one row to be inserted at a time).
I realise this is more commenting, however, again, there is definitely not enough room in a comment for me to write all this. :)
I'm very new to triggers and I can't seem to wrap my head around them. Let's assume I have two tables here:
CREATE TABLE Project
(
id INT NOT NULL IDENTITY(1, 1),
startDate DATETIME,
endDate DATETIME
);
CREATE TABLE Work
(
date DATETIME,
projectId INT
);
I insert some data:
INSERT INTO Project VALUES ('2017-04-18', '2017-05-01'); /*id = 1*/
INSERT INTO Work VALUES ('2017-04-17', 1);
Assuming there's only 1 project with id = 1, this should go well. However, it doesn't make much sense that my work starts the day before the project starts (not in this case). How would I create a trigger that basically says date cannot be < startDate OR > endDate?
Something like this should work:
CREATE TRIGGER t_CheckInterval ON dbo.Work
AFTER UPDATE, INSERT
AS
IF NOT EXISTS (
--if no records are returned then work date lies outside the project
-- (start, end) interval
SELECT 1
FROM inserted AS i
JOIN Project AS p
ON p.Id = i.projectId AND i.[date] BETWEEN p.startDate AND p.endDate
)
BEGIN
RAISERROR ('Error: Your error message here.', 16, 1)
ROLLBACK TRANSACTION
END
GO
In your case instead of using trigger for these kind of checkings, I suggest to use a CHECK CONSTRAINT, something like this:
CREATE FUNCTION dbo.ufn_CheckWorkDate
(
#WorkDate DateTime,
#ProjectID INT
)
RETURNS BIT
AS
BEGIN
DECLARE #Result BIT
IF EXISTS (SELECT * FROM Project WHERE id = #ProjectID AND #WorkDate BETWEEN startdate AND endDate)
SET #Result = 1
ELSE
SET #Result = 0
RETURN #Result
END
GO
ALTER TABLE Work
WITH CHECK ADD CONSTRAINT CK_CheckWorkDate
CHECK (dbo.ufn_CheckWorkDate(date, projectid) = 1)
I am working with Sage Evolution and do a lot of the back end stuff to customize it for our company.
I need to write a query where, when a user enters a negative quantity the system must not allow the transaction, however when the user enters a negative quantity and the product belongs to the "chemicals" group it needs to process the transaction.
Here is my code I have written so far.
DECLARE
#iAfterfQuantity Int;
#iAfteriStockCodeID Int;
#iAfterStockItemGroup VarChar
SELECT
#iAfterfQuantity = fQuantity,
#iAfteriStockCodeID = iStockCodeID
FROM
INSERTED
SELECT
#iAfterStockItemGroup = ItemGroup
FROM
dbo.stkItem
WHERE
StockLink = #iAfteriStockCodeID
BEGIN
IF #iAfterfQuantity < 0 AND #iAfterStockItemGroup <> 'chemicals'
BEGIN
RAISERROR ('',16,1)
ROLLBACK TRANSACTION
END
END
This is a task better suited for a check constraint then for a trigger, especially considering the fact that you are raising an error.
First, create the check function:
CREATE FUNCTION fn_FunctionName
(
#iAfterfQuantity Int,
#iAfteriStockCodeID Int
)
RETURNS bit
AS
BEGIN
DECLARE #iAfterStockItemGroup VarChar(150) -- Must specify length!
SELECT #iAfterStockItemGroup = ItemGroup FROM dbo.stkItem WHERE StockLink=#iAfteriStockCodeID
IF #iAfterfQuantity < 0 AND #iAfterStockItemGroup <> 'chemicals'
RETURN 0
RETURN 1 -- will be executed only if the condition is false...
END
Then, alter your table to add the check constraint:
ALTER TABLE YourTableName
ADD CONSTRAINT ck_ConstraintName
CHECK (dbo.fn_FunctionName(fQuantity, iStockCodeID) = 1)
GO
I am creating an agent job, using SQL server. In my database there are 2 tables.
The columns in the first table are:
Idproduct, number
The columns in the second tables are:
Idproduct, start (datatime), duration (time)
I need to increment the field number of a product when its (start+duration)<= getdate() and then i need delete this record.
How can i do?
Create table product(
Idproduct int primary key,
Number int default 0)
Create table production(
Idproduct int primary key,
Start datetime not null,
Times time not null)
One method is with the OUTPUT clause of a DELETE statement to insert the produced products into a table variable. Then use the table variable to increment the count. The example below uses SQL 2012 and above features but you retrofit the error handling for earlier versions if needed.
SET XACT_ABORT ON;
DECLARE #ProducedProducts TABLE(
Idproduct int
);
BEGIN TRY
BEGIN TRAN;
--delete produced products
DELETE FROM dbo.production
OUTPUT deleted.Idproduct INTO #ProducedProducts
WHERE
DATEADD(millisecond, DATEDIFF(millisecond, '', Times), Start) <= GETDATE();
--increment produced products count
UPDATE dbo.product
SET Number += 1
WHERE Idproduct IN(
SELECT pp.Idproduct
FROM #ProducedProducts AS pp
);
COMMIT;
END TRY
BEGIN CATCH
THROW;
END CATCH;