SQL Server 2016 accessed by ASP.NET 4.6.2 MVC web application
I have table "Building" and a Building can have multiple "Components". For example, Building1 has Component1 and Component2 ... etc
It was requested of me to be able to lock a building. Lock means that a component can no longer be modified (CREATE/UPDATE/DELETE). Well, as you can imagine, this is a huge application and a component can be modified in 100+ places. No one can even answer the question, "Where all do I need to lock?".
My thought is to lock everywhere I can think of and then as a safety net create a SQL Trigger that prevents all modifications if the column on the Component table "IsLocked BIT" is true. Currently, the only way I know if a component is locked is if the IsLocked column equals true.
So, I say all of that for this. How do I create a SQL Server Trigger that prevents a row of data from being modified if the row being modified has column IsLocked = 1?
Edit 1
In my opinion, this is not a duplicate. Using Instead of Delete or Instead of... will not work for me. If I do the instead of ... then inside of that I will need to provide commit logic. I don't want to provide commit logic. I just want to run a check prior to insert, update, delete.
Edit 2 - Instead of Update/Delete is best choice
If instead of... is my best choice than can someone rewrite what I have using the instead of update/delete? I don't know how to do it. Please keep in mind that requests will be coming from a web app. I won't know if they are updating one column or the entire entity or what they will passing in. I know that the way I have it written that it will catch any insert/update/delete and prevent it if locked. If there is a better way then please write it and explain why it is better.
Here is the solution I came up with:
ALTER TRIGGER [dbo].[PreventLockedModification]
ON [dbo].[Component]
FOR INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
--DETERMINE INSERT(I) UPDATE(U) OR DELETE(D)
----------------------------------------------------------------------------------------------------
DECLARE #action as char(1);
SET #action = 'I'; -- Set Action to Insert by default.
IF EXISTS(SELECT * FROM DELETED)
BEGIN
SET #action =
CASE
WHEN EXISTS(SELECT * FROM INSERTED) THEN 'U' -- Set Action to Updated.
ELSE 'D' -- Set Action to Deleted.
END
END
----------------------------------------------------------------------------------------------------
DECLARE #ErrorMsg nvarchar(100) = 'This row is locked and cannot be updated';
DECLARE #IsLocked bit;
DECLARE #BuildingId bigint;
DECLARE #UnitId bigint;
DECLARE #IsComplete_Building bit;
DECLARE #IsComplete_Unit bit;
----------------------------------------------------------------------------------------------------
IF #action = 'U' or #action = 'D'
BEGIN
SELECT #IsLocked = IsLocked FROM deleted;
IF #IsLocked = 1
BEGIN
RAISERROR (#ErrorMsg, 16, 1);
ROLLBACK TRANSACTION
END
END
----------------------------------------------------------------------------------------------------
ELSE IF #action = 'I'
BEGIN
SELECT #BuildingId = BuildingId FROM inserted;
SELECT #UnitId = UnitId FROM inserted;
SELECT #IsComplete_Building = IsComplete FROM Building WHERE BuildingId = #BuildingId
SELECT #IsComplete_Unit = IsComplete FROM Unit WHERE UnitId = #UnitId
IF #IsComplete_Building = 1 or #IsComplete_Unit = 1
BEGIN
RAISERROR (#ErrorMsg, 16, 1);
ROLLBACK TRANSACTION
END
END
----------------------------------------------------------------------------------------------------
END
Related
All our CRUD operations have a CommitTransaction() at the end before returning the results of a Method. For instance, we have a method Create() that creates a record if there is no existing record with a Status: 'Float'. Otherwise, it selects the record and updates its contents.
This is working as checked in the Database. The problem is, the method is returning its previous data and not the updated one:
public class Sample
{
public int ID {get; set;}
public string Code {get;set;}
public decimal Total {get; set;}
}
In Create(), Code and Total is entered by the user which should update the data in the database.
E.g.
ID | Code | Total
1 | CodeName | 100
The user updates
Code : 'Code1'
Total : 200
But the Method still returns
Code : 'CodeName '
Total : 100
But if checked in the Database, it is already
ID | Code | Total
1 | Code1 | 200
I have observed that this only happens when a CommitTransaction() is used which is not advisable to remove but a correct data return is also needed.
Here is a simplified replica of Create():
private string procCRUD = "procCreate #ID={0},#Code={1},#Total={2}";
public Sample Create(Sample data)
{
IQueryable<ComponentType> query = contextBase.Samples.FromSql(procCRUD,"create",data.ID, data.Code, data.Total); // contextBase.Samples is the DbContext
commit(); // this commits the transaction
return query;
}
procCRUD is a stored procedure:
DROP PROCEDURE IF EXISTS procCreate
GO
CREATE PROCEDURE procCreate
#proc varchar(20) = 'create',
#ID bigint = 0,
#Code varchar(20)= NULL,
#Total int= 0
AS
BEGIN
SET NOCOUNT ON;
DECLARE #SQL varchar(MAX)
SET #SQL = ''
IF (#proc='create') BEGIN
IF (NOT EXISTS(SELECT ID FROM Sample WHERE Status='Float'))
BEGIN
INSERT INTO Sample(Code, Total) VALUES( #Code, #Total)
SET #ID = ##IDENTITY;
END
ELSE
BEGIN
SELECT #ID=ID FROM Sample WHERE Status='Float'
UPDATE Sample SET
Code = #Code
,Total = #Total
WHERE ID=#ID
END
END
SET #SQL = 'SELECT TOP 1 * FROM Sample '
EXEC SP_EXECUTESQL #SQL
END
GO
I have tried manually setting the return value e.g. query.Code = data.Code before the return but it is time-consuming if I have more than 10 Class properties. Also, some properties are not entered by the user but retrieved from the database depends on another property entered.
I wonder if some of you also encountered this problem. Any help will be appreciated. Thank you.
(edited)
You need to better explain what you are trying to do and what is the actual code behind your
contextBase.Samples.FromSql
At least, the following seems to be relevant, based on your (not complete) description.
Copying from base to parent.
I have tried manually setting the return value e.g. query.Code = data.Code before the return but it is time-consuming if I have more than 10 Class properties.
Please read the solution already suggested here.
You will end up with defining a Map extension and using
query.Map(data);
The above should resolve your actual problem, but there are other inconsistencies in your question, that might affect the result.
SQL
procCRUD is a stored procedure:
SET #SQL = 'SELECT TOP 1 * FROM Sample '
EXEC SP_EXECUTESQL #SQL
END
GO
In the end of your stored procedure you are managing a select to return the correct value, but - as far as I can see - you are missing the correct where condition, something like a WHERE ID=#ID (or Status='Float', I don't know your app logic).
By the way, you should only Commit() after a BeginTransaction, but that is not shown in your question.
Nothing in your description really makes sense. According to you, you:
do a select
commit a transaction
return the values
Wonder that the value returned and the one in the Database do not match?
If you want to retreive values as a Update or Insert operation is running, the only reliable syntax in SQL is OUTPUT: https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql
Returns information from, or expressions based on, each row affected by an INSERT, UPDATE, DELETE, or MERGE statement. These results can be returned to the processing application for use in such things as confirmation messages, archiving, and other such application requirements. The results can also be inserted into a table or table variable. Additionally, you can capture the results of an OUTPUT clause in a nested INSERT, UPDATE, DELETE, or MERGE statement, and insert those results into a target table or view.
If you - for example - want to get teh Primary Key of something you just inserted, OUTPUT Inserted.ID is the only way that will reliably work. Unfortunatily, most non-MSSQL DBMS do not have such a feature.
I have 2 tables: dbo.Videos and dbo.Checkouts.
The dbo.Videos table contains a list of videos while the dbo.Checkouts table is keeping track of the videos that have been checked out.
The goal in my TSQL command is to insert a new row in the dbo.Checkouts table including VideoId, UserId, CheckoutDate.
Once this is successful I then want to update the dbo.Videos and decrement the TotalCopies column value based on the VideoID selected only if the value is greater than 0.
If less than 0 I want to throw an exception.
The VideoID in both tables are linked by foreign key.
However, the IF statement I have included in my statement below throws an error.
INSERT INTO dbo.Checkouts (VideoId, UserId, CheckoutDate)
VALUES (32, 'b0281f0d-8398-4a27-ba92-828bfaa9f90e', CURRENT_TIMESTAMP)
IF (SELECT TotalCopies FROM dbo.Videos WHERE VideoId = 32) > 0
UPDATE dbo.Videos SET
TotalCopies = TotalCopies - 1
WHERE VideoID = 32
You've got it backwards.
Instead of adding a record to Checkouts and then test if you have the video in Videos, You need to first check you have a copy you can check out.
It's like when you go to buy something from any shop - first you get the product off the shelf, and only then you pay for it.
If the product isn't on the shelf, there's no need for you to pay.
first version
You need at least three steps to do it right:
First, you check if you have a copy to check out.
If not, you don't do anything, just return a message that there are no free copies to checkout.
If there is a copy, you need to update the Video table (TotalCopies -= 1)
And last - you need to insert the record to checkouts.
The most important thing here is that if any of these steps fails, all of them fails - For instance, if for some reason you failed to insert the row to checkouts, you must revert the update you did on the Video table, since you can't complete the process.
This is the first reason why you need to wrap the entire process in a transaction.
The second reason you need a transaction is to avoid a race condition between the test if there are copies to checkout and the update of the video table. You can read more about it on Dan Guzman's blog post about Conditional INSERT/UPDATE Race Condition.
So, having said all that, let's show some code:
CREATE PROCEDURE VideoCheckout
(
#VideoId int,
#UserId uniqueIdentifier,
#Success bit OUTPUT
)
AS
SET XACT_ABORT ON
SET #Success = 0
BEGIN TRANSACTION
BEGIN TRY
DECLARE #NumberOfCopies int
SET #NumberOfCopies = ISNULL(
(
SELECT TotalCopies
FROM dbo.Videos WITH (UPDLOCK, HOLDLOCK)
WHERE VideoId = #VideoId
)
, 0)
IF #NumberOfCopies > 0
BEGIN
UPDATE dbo.Videos
SET TotalCopies = TotalCopies - 1
WHERE VideoId = #VideoId;
INSERT INTO dbo.Checkouts (VideoId, UserId, CheckoutDate)
VALUES (#VideoId, #UserId, CURRENT_TIMESTAMP)
SET #Success = 1
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
GO
update - the using ##rowcount version:
SQL Server's ##Rowcount global variable returns the number of rows effected (generally. there are some exceptions that are documented in the link) - using that you can unify the test part with the update part - having SQL Server report back if the update effected any rows on not. This enables you to write simpler SQL and will probably have better performace.
CREATE PROCEDURE VideoCheckout
(
#VideoId int,
#UserId uniqueIdentifier,
#Success bit OUTPUT
)
AS
SET XACT_ABORT ON
SET #Success = 0
BEGIN TRANSACTION
BEGIN TRY
UPDATE dbo.Videos
SET TotalCopies = TotalCopies - 1
WHERE VideoId = #VideoId
AND TotalCopies > 0;
IF ##ROWCOUNT > 0
BEGIN
INSERT INTO dbo.Checkouts (VideoId, UserId, CheckoutDate)
VALUES (#VideoId, #UserId, CURRENT_TIMESTAMP)
SET #Success = 1
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
GO
I have a C# application using Entity Framework and I created a trigger in the database to fire after insert and update.
I notice that the trigger does not fire when I insert or update using EF code, on the other side the trigger fires when I insert or update through SQL Server
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[updateLoanFinishing]
ON [dbo].[Installments_Paids]
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
DECLARE #totalLoan decimal, #totalpaid decimal, #balance decimal
SELECT #totalLoan = l.Total_Amount
FROM dbo.Loans l
SELECT #totalpaid = ISNULL(SUM(ip.Paid_Amount), 0)
FROM dbo.Installments_Paids ip
SELECT #balance = #totalLoan - #totalpaid
IF #balance = 0
BEGIN
UPDATE dbo.Loans
SET dbo.Loans.IsFinish = 1 -- bit
FROM dbo.Loans l
INNER JOIN INSERTED i ON i.Loan_ID = l.Loan_ID
END
ELSE IF #balance > 0
BEGIN
UPDATE dbo.Loans
SET dbo.Loans.IsFinish = 0 -- bit
FROM dbo.Loans l
INNER JOIN INSERTED i ON i.Loan_ID = l.Loan_ID
END
END
I believe your trigger is being called - it's just flawed and cannot execute properly.
Have a look at it:
DECLARE #totalLoan decimal, #totalpaid decimal, #balance decimal
SELECT #totalLoan = l.Total_Amount
FROM dbo.Loans l
What are you trying to do here?? You have a single variable called #TotalLoan and you're assigning to it the whole Loans table, Total_Amount column? Is there only one row in that table?? Otherwise, this is not a valid assignment - you cannot assign a whole table to a single variable.....
Same goes for the next statement - again you're assigning basically a whole table to a single variable - that just cannot possibly work!
What are you really trying to do here?? You might need to add a WHERE clause to your statements, or did you plan to use SUM(....) for those assignment, or do something else to make them work - but I'm pretty sure this trigger is called, it just cannot do what you expect it to do since it's flawed
I have a coworker working on an application who's run into a problem. He fires off a stored procedure using SqlCommand.ExecuteNonQuery. This stored procedure, in the same table, updates one row and inserts another. Meanwhile his application goes on and reads from the table. A race condition occurs where the read happens in between the update and the insert.
The data in question is records of access levels. When an access level changes it terminates (updates) the old access level and then instantiates (inserts) the new access level. Not infrequently the read will get in between the update and insert and find only terminated access levels--a bit of a problem.
What's the best solution to my coworker's problem?
I got a hold of the stored procedure he's trying to fix:
BEGIN
SELECT OBJECT_ACCESS_ID, PERSON_AUTH_LEVEL
INTO lAccessID, lExistingAccessLevel
FROM SHPAZ.SH_PAZ_OBJECT_ACCESS
WHERE
USER_ID = pUserID
AND (GRGR_ID = pGroupID OR (GRGR_ID IS NULL AND pGroupID IS NULL))
AND SYSDATE BETWEEN OBJECT_ACCESS_EFF_DATE AND OBJECT_ACCESS_END_DATE
FOR UPDATE;
-- If the new access level is the same as the existing, then do nothing.
IF lExistingAccessLevel = pLevel THEN
RETURN;
END IF;
-- Terminate the existing record.
UPDATE SHPAZ.SH_PAZ_OBJECT_ACCESS
SET OBJECT_ACCESS_END_DATE = SYSDATE
WHERE OBJECT_ACCESS_ID = lAccessID;
-- Create the new record.
SELECT CASE WHEN pGroupID IS NULL THEN 'Broker' ELSE 'Employer' END
INTO lSource
FROM DUAL;
INSERT INTO SHPAZ.SH_PAZ_OBJECT_ACCESS (USER_ID, GRGR_ID, SOURCE, PERSON_AUTH_LEVEL, OBJECT_ACCESS_EFF_DATE, OBJECT_ACCESS_END_DATE)
VALUES (pUserID, pGroupID, lSource, pLevel, SYSDATE, TO_DATE('12/31/2199', 'MM/DD/YYYY'));
COMMIT;
EXCEPTION
-- If there is no record, then just create a new one.
WHEN NO_DATA_FOUND THEN
SELECT CASE WHEN pGroupID IS NULL THEN 'Broker' ELSE 'Employer' END
INTO lSource
FROM DUAL;
INSERT INTO SHPAZ.SH_PAZ_OBJECT_ACCESS (USER_ID, GRGR_ID, SOURCE, PERSON_AUTH_LEVEL, OBJECT_ACCESS_EFF_DATE, OBJECT_ACCESS_END_DATE)
VALUES (pUserID, pGroupID, lSource, pLevel, SYSDATE, TO_DATE('12/31/2199', 'MM/DD/YYYY'));
END SHSP_SET_USER_ACCESS;
The solution is to remove the commit from inside your procedure, and have it
done after procedure returns. Let's say you create your procedure with name my_procedure:
SQL> exec my_procedure(my_in_arg, my_out_arg);
SQL> commit;
There should be no race at all when atomic functional operations are wrapped inside a transaction.
I have a table with sequential numbers (think invoice numbers or student IDs).
At some point, the user needs to request the previous number (in order to calculate the next number). Once the user knows the current number, they need to generate the next number and add it to the table.
My worry is that two users will be able to erroneously generate two identical numbers due to concurrent access.
I've heard of stored procedures, and I know that that might be one solution. Is there a best-practice here, to avoid concurrency issues?
Edit: Here's what I have so far:
USE [master]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[sp_GetNextOrderNumber]
AS
BEGIN
BEGIN TRAN
DECLARE #recentYear INT
DECLARE #recentMonth INT
DECLARE #recentSequenceNum INT
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- get the most recent numbers
SELECT #recentYear = Year, #recentMonth = Month, #recentSequenceNum = OrderSequenceNumber
FROM dbo.OrderNumbers
WITH (XLOCK)
WHERE Id = (SELECT MAX(Id) FROM dbo.OrderNumbers)
// increment the numbers
IF (YEAR(getDate()) > IsNull(#recentYear,0))
BEGIN
SET #recentYear = YEAR(getDate());
SET #recentMonth = MONTH(getDate());
SET #recentSequenceNum = 0;
END
ELSE
BEGIN
IF (MONTH(getDate()) > IsNull(#recentMonth,0))
BEGIN
SET #recentMonth = MONTH(getDate());
SET #recentSequenceNum = 0;
END
ELSE
SET #recentSequenceNum = #recentSequenceNum + 1;
END
-- insert the new numbers as a new record
INSERT INTO dbo.OrderNumbers(Year, Month, OrderSequenceNumber)
VALUES (#recentYear, #recentMonth, #recentSequenceNum)
COMMIT TRAN
END
This seems to work, and gives me the values I want. So far, I have not yet added any locking to prevent concurrent access.
Edit 2: Added WITH(XLOCK) to lock the table until the transaction completes. I'm not going for performance here. As long as I don't get duplicate entries added, and deadlocks don't happen, this should work.
you know that SQL Server does that for you, right? You can you a identity column if you need sequential number or a calculated column if you need to calculate the new value based on another one.
But, if that doesn't solve your problem, or if you need to do a complicated calculation to generate your new number that cant be done in a simple insert, I suggest writing a stored procedure that locks the table, gets the last value, generate the new one, inserts it and then unlocks the table.
Read this link to learn about transaction isolation level
just make sure to keep the "locking" period as small as possible
Here is a sample Counter implementation. Basic idea is to use insert trigger to update numbers of lets say, invoices. First step is to create a table to hold a value of last assigned number:
create table [Counter]
(
LastNumber int
)
and initialize it with single row:
insert into [Counter] values(0)
Sample invoice table:
create table invoices
(
InvoiceID int identity primary key,
Number varchar(8),
InvoiceDate datetime
)
Stored procedure LastNumber first updates Counter row and then retrieves the value. As the value is an int, it is simply returned as procedure return value; otherwise an output column would be required. Procedure takes as a parameter number of next numbers to fetch; output is last number.
create proc LastNumber (#NumberOfNextNumbers int = 1)
as
begin
declare #LastNumber int
update [Counter]
set LastNumber = LastNumber + #NumberOfNextNumbers -- Holds update lock
select #LastNumber = LastNumber
from [Counter]
return #LastNumber
end
Trigger on Invoice table gets number of simultaneously inserted invoices, asks next n numbers from stored procedure and updates invoices with that numbers.
create trigger InvoiceNumberTrigger on Invoices
after insert
as
set NoCount ON
declare #InvoiceID int
declare #LastNumber int
declare #RowsAffected int
select #RowsAffected = count(*)
from Inserted
exec #LastNumber = dbo.LastNumber #RowsAffected
update Invoices
-- Year/month parts of number are missing
set Number = right ('000' + ltrim(str(#LastNumber - rowNumber)), 3)
from Invoices
inner join
( select InvoiceID,
row_number () over (order by InvoiceID desc) - 1 rowNumber
from Inserted
) insertedRows
on Invoices.InvoiceID = InsertedRows.InvoiceID
In case of a rollback there will be no gaps left. Counter table could be easily expanded with keys for different sequences; in this case, a date valid-until might be nice because you might prepare this table beforehand and let LastNumber worry about selecting the counter for current year/month.
Example of usage:
insert into invoices (invoiceDate) values(GETDATE())
As number column's value is autogenerated, one should re-read it. I believe that EF has provisions for that.
The way that we handle this in SQL Server is by using the UPDLOCK table hint within a single transaction.
For example:
INSERT
INTO MyTable (
MyNumber ,
MyField1 )
SELECT IsNull(MAX(MyNumber), 0) + 1 ,
"Test"
FROM MyTable WITH (UPDLOCK)
It's not pretty, but since we were provided the database design and cannot change it due to legacy applications accessing the database, this was the best solution that we could come up with.