How to conduct a fully synchronous multi-step database operation? - asp.net

Current project:
DotNet 4.7
MVC 5
C# 7.1
Repository pattern that uses Linq Lambda for CRUD operations
I have a bit of a problem with regards to what might seem like a concurrency issue, but really isn’t.
You see, the system I am building allows a user to register for a class. Each class has a certain capacity, and there needs to be the ability to have people sitting in both Enrolled and Waitlisted states.
The Registration table does this by having an Enrolled boolean, true for Enrolled, False for waitlisted. The problem is, during the registration process, I need a query-count that brings up the number of existing Registered users, sees how many open spots there still are by comparing the Enrolled=yes count to the capacity value of the class (a different table), and if there are any open spots, allows a user to register with the Enrolled flag set to true. If there are no spots left, the user is registered with the Enrolled flag is set to False.
The problem exists when there is one spot left and two users sign up simultaneously (or close enough for the system to be working on one operation at the same time as the other). I have seen timestamps on the existing Registration table, and there are times when two users are very, very close together in having their data entered.
I need a system, either in SQL or in MVC, that will only do one user registration at a time. That there will be no chance whatsoever that it starts a second user registration (the query to see how many Enrolled=yes there are in the Registrations table) until the saving of the first user's registration is done. In no case should the number of Enrolled=yes registrations ever exceed the capacity of the class that the user is enrolling in.
In other words, the actual query-count-compare-decide-record process (at least two touches of the database, one to query, one to record) needs to be absolutely SYNCHRONOUS, and essentially block all other registration attempts until the process is done. Because this will be done in one block of code, I can safely say that the table will never be “locked” for any longer than it actually takes to run the code; this process is after all relevant user interaction. But since this is a read (counting all the current registrations where Enrolled = yes, to determine if current write needs to be Enrolled=yes or =no) followed by a write, I am worried about a second read occurring between the read and the write of the first, and that the second write occurs after the first, leaving the Enrolled=true count in an incompliant state.
Since the number of users who are signing up will only ever be small(ish), I am not overly concerned about performance, but I am stumped on how to actually implement this.
Suggestions?

IMO, the most important thing is, implement the validation in the database, at least. Don't allow the data to violate a rule like this.
I think you should just use an insert/update trigger and block a statement if it violates your constraint. It's the same as implementing a check constraint, but gives you the flexibility to check the class capacity, count the number of students in the class, and throw an error (rolling back the transaction) if you ever exceed the student limit in a course.
The trigger executes in the same transaction as the DML statement that triggers it, so if you THROW an error, you roll it all back. Something like this should work:
CREATE TABLE class (class_id INT, capacity INT)
GO
CREATE TABLE registration (class_id INT, student_id INT, enrolled BIT)
GO
CREATE TRIGGER i_registration
ON registration
FOR INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT * FROM (
SELECT SUM(CASE WHEN enrolled = 1 THEN 1 ELSE 0 END) OVER (PARTITION BY r.class_id) enrolled, c.capacity
FROM registration r
INNER JOIN class c ON r.class_id = c.class_id
)sq WHERE enrolled > capacity
)
THROW 51000, 'Class is full!', 1
END
GO
And some sample DML statements:
insert into class values (1, 5)
insert into registration values (1, 1, 1)
insert into registration values (1, 2, 1)
insert into registration values (1, 3, 0)
insert into registration values (1, 4, 0)
insert into registration values (1, 5, 1)
insert into registration values (1, 6, 1)
insert into registration values (1, 7, 1)
insert into registration values (1, 8, 1) -- blocked!
update registration set enrolled = 1 where student_id = 3 -- blocked!
When you call SaveChangesAsync() in your client app, it's going to raise an exception (System.Data.Entity.Infrastructure.DbUpdateException), and you'll be able to see your exception message & number by cascading through Inner Exceptions until you find a System.Data.SqlClient.SqlException. You can use these to determine how to present the error message to the user:

Related

Oracle APEX PL/SQL process to insert multi-select items into association table for m:m relationship failing silently

I am implementing a form on table that allows the end-user to create a new project. This form contains a shuttle that allows the user to select the disposal site(s)(1+) that the project pertains to. I would like to use the output of the shuttle values to populate an association table between projects and disposal sites which is a many to many relationship.
This is my approach so far:
Created an additional VARCHAR2(4000)in the projects table to store the shuttle output (called 'Shuttle'). The shuttle output in this column looks something like 'CA-AT-D109Z2:CA-AT-D115:CA-AT-D174Z2'.
Created a process to take separate based on ':' and then add the values to the association table using the PL/SQL code:
Declare
Cursor c_values
is
Select
t.column_value As disposal_sites
From
Table ( apex_string.split(:P28_SHUTTLE, ':') ) t
Where
t.column_value Is Not Null;
Begin
for c in c_values loop
insert into MP_MDB_PROJECT_2_DSITE (PROJECTIDFK,DISPOSALSITEIDFK)
values (:P28_PROJECTNUMBER,c.disposal_sites);
end loop;
End;
The process/code enters the values from the shuttle into the association table in a loop as expected for the the disposal site but remains blank for projectidfk (the key that is '1' in the 1:m relationship). The code doesn't throw an error so I am having trouble debugging.
I think perhaps the problem I am having is that project number is computed after submission based on the users selections.Therefore, when the process run it finds :P28_PROJECTNUMBER to be null. Is there a way to ensure the computation to determine :P28_PROJECTNUMBER takes places first and is then followed by the PL/SQL process?
All help appreciated
If the form you're implementing is a native apex form, then you can use the attribute "Return Primary Key(s) after Insert" in the "Automatic Row Processing - DML" process to ensure that the primary key page item contains inserted value for any processes execute after this process.
Just make sure the process that handles the shuttle data is executed after the DML process.

Can't delete from a table inside a trigger

I'm building this DB about the University for one of my course classes and I'm trying to create a trigger that doesn't allow for a professor to be under 21yo.
I have a Person class and then a Professor subclass.
What I want to happen is, you create a Person object, then a Professor object using that Person object's id, but, if the Person is under 21yo, delete this Professor object, then delete the Person object.
Everything works fine up until the "delete the Person object" part where this doesn't happen and I'm not sure why. Any help?
This is the sqlite code I have:
AFTER INSERT ON Professor
FOR EACH ROW
WHEN strftime('%J', 'now') - strftime('%J', (SELECT dateOfBirth from Person WHERE personId = NEW.personId)) < 7665 -- 21 years in days
BEGIN
SELECT RAISE(ROLLBACK, 'Professor cant be under 21');
DELETE FROM Person WHERE (personId= new.personId);
END;```
One common issue is that there many not be a current transaction scope to rollback to, which would result in this error:
Error: cannot rollback - no transaction is active
If that occurs, then the trigger execution will be aborted and the delete never executed.
If ROLLBACK does succeed, then this creates a paradox, by rolling back to before the trigger was executed in a strictly ACID environment it would not be valid to continue executing the rest of this trigger, because the INSERT never actually occurred. To avoid this state of ambiguity, any call to RAISE() that is NOT IGNORE will abort the processing of the trigger.
CREATE TRIGGER - The RAISE()
When one of RAISE(ROLLBACK,...), RAISE(ABORT,...) or RAISE(FAIL,...) is called during trigger-program execution, the specified ON CONFLICT processing is performed and the current query terminates. An error code of SQLITE_CONSTRAINT is returned to the application, along with the specified error message.
NOTE: This behaviour is different to some other RDBMS, for instance see this explanation on MS SQL Server where execution will specifically continue in the trigger.
As OP does not provide calling code that demonstrates the scenario it is worth mentioning that in SQLite on RAISE(ROLLBACK,)
If no transaction is active (other than the implied transaction that is created on every command) then the ROLLBACK resolution algorithm works the same as the ABORT algorithm.
Generally, if you wanted to Create a Person and then a Professor as a single operation, you would Create a Stored Procedure that would validate the inputs first, preventing the original insert at the start.
To maintain referential integrity, even if an SP is used, you could still add a check constraint on the Professor record or raise an ABORT from a BEFORE trigger to prevent the INSERT from occurring in the first place:
BEFORE INSERT ON Professor
FOR EACH ROW
WHEN strftime('%J', 'now') - strftime('%J', (SELECT dateOfBirth from Person WHERE personId = NEW.personId)) < 7665 -- 21 years in days
BEGIN
SELECT RAISE(ABORT, 'Professor can''t be under 21');
END
This way it is up to the calling process to manage how to handle the error. The ABORT can be caught in the calling logic and would effectively result in rolling back the outer transaction, but the point is that the calling logic should handle negative side effects. As a general rule triggers that cascade logic should only perform positive side effects, that is to say they should only affect data if the inserted row succeeds. In this case we are rolling back the insert, so it becomes hard to identify why the Person would be deleted.

How to assign an ID but then delete if not used

I am unsure on how to do this 'best practice' wise.
I have a web application (asp.net VB) that connects to an MS SQL server 2012. Currently when the page loads the app connects to a DB table and gets the last ID and adds 1 to it and displays this to the user. When the user submits the form the new ID is saved to the DB.
The problem being the app may be opened by 2 users at the same time and therefore they will get assigned the same ref number which will cause problems when the data is saved.
How can I assign different numbers to different users if the app is opened at the same time without saving unnecessary data?
You have multiple solutions for this, I'll try to outline a few approaches. (I'll assume that you need to insert things into a DB that I'll call "orders".)
First of all, you can move the ID-generation to the moment when the order is actually inserted, and not at the moment when the user start to enter the data. That way, you do not generate an ID for a user that never completes the form. Also this scenario is easy to accomplish using autoincrementing values in sql server. You can, for example do:
-- create a table with an identity column
create table Orders (
ID int identity(1,1) not null,
Description nvarchar(max) not null
);
-- insert values, without specifying the ID column
insert into Orders (Description) values ()
-- select the row back
-- returns 1, 'My First Order'
select * from Orders;
Another way to do this is to use SQL Server Sequences. These are things that do nothing except generate numbers in a row. They guarantee that the numbers won't be repeated, and always keep count of the current value, i.e.
-- create a sequence
create sequence OrderIdSequence
start with 1
increment by 1;
-- get the next sequence value
select next value for OrderIdSequence

Web Form Next Number Generation Scenario

I have a web form with several fields first field is Employee Number which is having "EMP - 0001" Format.i'm generating next Employee number by considering the last Emp Number added and converting the latter part to integer and add one
ex: split EMP - 0001 -> get 0001 -> convert it to integer -> add one -> generate next no as EMP - 0002
Next Employee Number should be visible to the user.my issue is when there are multiple users using the system.imagine that one user opens the web Form and doesn't save the record.his Employee Number is EMP - 0002.another user opens the web form he also sees the EMP no as 0002 because last record is not saved yet.2nd user saves the records he gets the Number 0002.1st user then saves the record.so at last i've got duplicate EMP Numbers in my database.what kind of scenario should i follow to over come this situation
The only way to accurately predict their ID is to put in a blank record, get the ID used, then when they enter the form, update the record with their information. However, if they quit the form, you're left with a blank record.
Insert the record on the DB and get the ID it returns. You won't have concurrency issues there if you're opening and disclosing your connection correctly.
Your current approach is prone to concurrency issues as you pointed out & I will not recommend it you. You have the following options.
Use an Identity column in your database table as a serial column (The database automatically increments the identity column on every insert row operation- you don't have to specify it through code)SQL Server Identity
Use a database sequence (Depeding on your database version & its support - A database sequence returns a unique integer value - this can also be cached, presented on form once the form loads - once generated, same sequence is never generated again)
Use a database trigger to automatically update the Id column on every row insert
Depeding on your requirement, you can pick one option.
After saving the record you can give the user message tah record is saved and your Employee numnber is "EMP - 0002".

Column 'AuctionStatus' cannot be used in an IF UPDATE clause because it is a computed column

I am developing an Auction site in asp.net3.5 and sql server 2008R2, My Database has an Auction Table that has a calculated column "AuctionStatus" -
(case when [EndDateTime] < getdate() then '0' else '1' end)
that gives auction status Active or inactive based on End Date.
Now I want to call a stored procedure that sends email notifications to buyers and sellers as soon as AuctionStatus becomes '0'. For that i tried to create a after update trigger that could call the email notification sp, but i am not able to do so.
I am getting the following error message :-
Msg 2114, Level 16, State 1, Procedure trgAuctionEmailNotification,
Line 6 Column 'AuctionStatus' cannot be used in an IF UPDATE clause
because it is a computed column.
The trigger is:
CREATE TRIGGER trgAuctionEmailNotification ON SE_Auctions
AFTER UPDATE
AS
BEGIN
IF (UPDATE (AuctionStatus))
BEGIN
IF EXISTS (SELECT * FROM inserted WHERE currentbidderid > 0
AND AuctionStatus='0' )
BEGIN
DECLARE #ID int
SELECT #ID = AuctionID from inserted
EXEC spSelectSE_AuctionsByAuctionID #ID
END
END
END
You could just replace AuctionStatus with the corresponding expression :
IF EXISTS (SELECT * FROM inserted WHERE currentbidderid > 0 AND [EndDateTime] < getdate() )
But, the point is I don't see how your trigger will be "triggered" as [AuctionStatus] is never "updated". Its Value is just calculated whenever you need it.
You could go for a sql job that runs every x minutes and send a notification for each auction which ended during the last x minutes.
You need to add a real column containing a flag to indicate whether the notifications have been sent, and then implement a polling technique to scan the table for rows where the status is inactive and notifications haven't been sent.
The computed column doesn't really transition from one state to another, so it's not like an UPDATE has occurred. Even if SQL Server did implement this, it would be hideously expensive, since it would have to query the entire table for transitioning rows every 3ms. (or even more frequently if you're using datetime2 with a higher precision)
Whereas you can pick a suitable polling interval yourself. This could be an SQL agent job, or in some service code somewhere, whatever best fits the rest of your architecture.

Resources