Webapp - should a constraint be checked at database or application, or both - sql

Simplified context:
let say I have 2 table in my database: Room(id, maxContract) and Contract(id, roomid, status)
let say I have a room 17 which allow max 2 clients, now I would search the Contract table of row roomid = 17 and status = active, if the more more than max (in this case 2) rows, I would prevent further INSERT until a contract expire.
Question:
Now I see 2 ways of doing this, first is in the database itself, maybe on a TRIGGER, and the second is doing this in my webapp DAO: query the Contract table to get the count, if-else to check the logic and only run the insert if true
But I am just a newbie, I don't know what is the best (or common) approach, which way I should do it? If this was my personal app, I would do both for max security, but designing a web I had to also take performance into consideration.
In case of frontend - backend, I know that validation is mandatory at backend and optional at the frontend, but between backend-database I don't know exactly
(In case this is opinion-based and there is no best-practice, I would like to know the pros and cons of both implementation)
EDIT:
to be more exact: user click JOIN ROOM => call an insertToRoom() method
+solution 1:
insertToRoom(){
if (roomIsAvailable()){
execute INSERT query;
}
else alert: "room is full";
}
roomIsAvailable() is a method to query and count how many contracts are bound to the room
+solution 2:
insertToRoom(){
execute INSERT query;
}
database:
CREATE TRIGGER before INSERT
if (some code to count the rooms)
ROLLBACK TRANSACTION;
in this case, if an unavailable room is join, database will return error, which in turn cause the execute INSERT query in the application to return false.
Either way, the falsy data is not inserted end the end user will get an error alert

I expect that more than one user can work with your application.
If you will get the current status from DB to the application, evaluate the condition there and then you will run the insert into DB table, then the condition can be violated. Let's imagine this sequence of steps:
User A read the current status from DB.
User B read the current status from DB.
User A evaluate the condition with result "there is a space for one client".
User B evaluate the condition with result "there is a space for one client".
User A update DB -> the room is full.
User B update DB -> in the room is more than allowed number of clients.
So that is not an option.
You can use triggers (as you mentioned), if your DB has such possibility.
You can also create one SQL statement which will check conditions and update the record in DB in one step (atomically). It depends on your DB engine, whether it is possible.
Update 2022-05-27
I was asked to explain in detail the atomic insert solution.
I'm not MySQL guru and I'm pretty sure, that there is more elegant way, how to do it, but something like this works too:
Let's create required DB objects first:
create table Room(
id INT,
maxContract INT);
create table Contract(
id INT AUTO_INCREMENT,
roomid INT,
status VARCHAR(30),
PRIMARY KEY(id)
);
create view UsedSpace as
select r.id as roomid, count(c.id) as used
from Room r
left outer join Contract c on c.roomid = r.id and status='active'
group by r.id;
Then we can use this statement to insert new row to Contract table:
insert into Contract(roomid, status)
select r.id, 'active'
from Room r
inner join UsedSpace us on r.id = us.roomid and r.maxContract > us.used
where id = 17;
When there is too much active contracts, then new row is not inserted.
You can check if the row was inserted or not via
select row_count();
Here is a fiddle to show results quickly.

Related

Oracle SQL update double-check locking

Suppose we have table A with fields time: date, status: int, playerId: int, serverid: int
We added constraint on time, playerid and serverid (UNQ_TIME_PLAYERID_SERVERID)
At some time we try to update all rows in table A with new status and date:
update status = 1, time = sysdate where serverid=XXX and status != 1 and time > sysdate
Problem that there are two separated processes on separate machines that can execute same update at same sysdate.
And UNQ_TIME_PLAYERID_SERVERID violation occurs!
Is there any possibility to force Oracle check where cause before concrete update (when lock on row acquired)?
I do not want to use any 'select for update' things
If it's really the same update 100% of the time, then just catch the exception and ignore it.
In case you want to prevent an error occuring in the first place, you need to implement some logic to prevent the second update statement from ever executing.
I could think of a "lock table" just for this purpose. Create a table TABLE_A_LOCK_TB (add columns based on what information you want to have stored there for administrative reasons, e.g. user who set the lock or a timestamp, ...).
Before you execute an update statement on table A, just insert a row to TABLE_A_LOCK_TB. Once an update was successful, delete said row.
Before executing any update statement on table A just check whether the TABLE_A_LOCK_TB has a dataset. If it doesn't your update is good to go, if it does you don't execute the update.
To make this process easier you could just write a package for "locking" and "unlocking" table A by inserting / deleting a row from the TABLE_A_LOCK_TB. Also implement a function to check the "lock status".
If you need this logic for several tables you can also make it dynamic by just having a column holding the table name in TABLE_A_LOCK_TB and checking against that.
In your application logic you can handle every update like this then (pseudocode):
IF your_lock_package.lock_status(table_name) = false THEN
your_lock_package.set_lock(table_name);
-- update statement(s)
your_lock_package.release_lock(table_name);
ELSE
-- "error" handling / information to user + exit

Update SQL Server table with one time use values from another table

I have a table of users that has the usual suspects, name, email, etc. As the users complete an activity (queried from another table), I need to award them a gift card code.
update users
set giftcardcode = 'code from other table'
where email in (select email from useractivity where necessary conditions are met)
I have a table of unique gift card codes that are unique, one-time use codes. So I need to update my user table, setting the award code field equal to a distinct, unused gift card code from the gift card code table. Then I need to mark the 'used' field in the gift card table to 'Y'.
The goal is to do this with SQL and not any programming. I'm stumped.
I think there is a Many To Many relationship between User table and Activity table.
So, you can use a trigger to execute a query when update.
Each time a row will be updated in the Activity table, the trigger will do something.
It will UPDATE the User table by adding a new gift code.
I think you can add an attribute in your GiftCode table to easily check if the code as already been used. An you can get an unused code like that :
// Retrieve an unused code based on a BIT attribute.
SELECT TOP 1 [Code] FROM [GiftCode] WHERE IS_UNUSED = 1;
Don't forget to update this Gift code after using it.
You can use a SELECT statement including a sub SELECT statement to get a code too :
// Retrieve an unused code based on User table used codes.
SELECT TOP 1 [Code] FROM [GiftCode] WHERE [Code] NOT IN (SELECT [Code] FROM [User]);
It works well if you don't have too much users.
Otherwise , the first statement will be more efficient.
Don't forget to update the User table.
Now you can easily use one of these previous statement in a UPDATE statement.
It will be something like that :
UPDATE [User] SET [Code] = (
SELECT TOP 1 [Code] FROM [GiftCode] WHERE [Code] NOT IN (
SELECT [Code] FROM [User]))
WHERE USER_ID = // ...;
You can perform this in a trigger.
You can use a stored procedure, it's more efficient and will wrap all the SQL code in a compiled function. Then you can call it in your trigger.
You can execute a stored procedure in a job (see SQL Server Agent jobs) too.
create a Trigger on your table for update and do what you want inside it using inserted and deleted

DB2 locking when no record yet exists

I have a table, something like:
create table state {foo int not null, bar int not null, baz varchar(32)};
create unique index on state(foo,bar);
I'd like to lock for a unique record in this table. However, if there's no existing record I'd like to prevent anyone else from inserting a record, but without inserting myself.
I'd use "FOR UPDATE WITH RS USE AND KEEP EXCLUSIVE LOCKS" but that only seems to work if the record exists.
A) You can let DB2 create every ID number. Let's say you have defined your Customer table
CREATE TABLE Customers
( CustomerID Int NOT NULL
GENERATED ALWAYS AS IDENTITY
PRIMARY KEY
, Name Varchar(50)
, Billing_Type Char(1)
, Balance Dec(9,2) NOT NULL DEFAULT
);
Insert rows without specifying the CustomerID, since DB2 will always produce the value for you.
INSERT INTO Customers
(Name, Billing_Type)
VALUES
(:cname, :billtype);
If you need to know what the last value assigned in your session was, you can then use the IDENTITY_VAL_LOCAL() function.
B) In my environment, I generally specify GENERATED BY DEFAULT. This is in part due to the nature of our principle programming language, ILE RPG-IV, where developers have traditionally to allowed the compiler to use the entire record definition. This leads me to I can tell everyone to use a sequence to generate ID values for a given table or set of tables.
You can grant select to only you, but if there are others with secadm or other privileges, they could insert.
You can do something with a trigger, something like check the current session, and if the user is your user, then it inserts the row.
if (SESSION_USER <> 'Alex) then
rollback -- or generate an exception
end if;
It seems that you also want to keep just one row, then, you can control that also in a trigger:
select count(0) into value from state
if (value > 1) then
rollback -- or generate an exception
end if;

Alternative to check constraints in Views

So I have two tables:
Requests
--------
Id
RequestSchemeId
ReceivedByUserId
ForwardedRequests
-----------------
Id
RequestId (FK to Id column of Requests Table)
ForwardedToUserId
and one view
ForwardedRequestsInRequestSchemes
---------------------------------
Requests.RequestSchemeId
Requests.ReceivedByUserId
ForwardedRequests.ForwardedToUserId
What's the standard/recommended way of adding a constraint equivalent to Requests.ReceivedByUserId != ForwardedRequests.ForwardedToUserId in the view?
I know check constraints are not allowed in views. Using SQL Server 2008.
EDIT:
This is a followup question to this question.
Business rules:
The same request can be forwarded to multiple users. Hence the Id column in the ForwardedRequests table.
A user can receive only one Request for a particular RequestScheme. So I created a UniqueKey constraint for RequestSchemeId+ReceivedByUserId in the Requests table.
The request can be forwarded to another user only if forwarded user does not already have a forwarded request under the same scheme from any other user. So as Martin suggested in the linked question, I created a view from the two tables and added a unique constraint on Requests.RequestSchemeId+ForwardedRequests.ForwardedToUserId.
The business rule this question is about, is that the receiver of the request cannot forward it to himself/herself.
I can think of a couple of ways of getting SQL Server to enforce this for you. Both pretty hacky though so interested to see any other approaches.
1) You could add to the indexed view ForwardedRequestsInRequestSchemes an additional column 1/(ForwardedToUserId - ReceivedByUserId) AS FailIfSame which would raise a Divide by zero error if the two values are the same. This does mean that you end up storing a redundant column in the indexed view though.
2) You could create a new view that returns any such rows cross joined onto a two row table then define a unique constraint on that view. This view will always be empty.
CREATE TABLE dbo.TwoRows(C INT) INSERT INTO dbo.TwoRows VALUES(1),(1)
GO
CREATE VIEW dbo.FailIfForwardedUserEqualToReceivedByUser
WITH SCHEMABINDING
AS
SELECT 1 AS C
FROM dbo.ForwardedRequests FR
INNER JOIN dbo.Requests R
ON R.Id = FR.RequestId AND R.ReceivedByUserId = FR.ForwardedToUserId
CROSS JOIN dbo.TwoRows
GO
CREATE UNIQUE CLUSTERED INDEX ix ON
dbo.FailIfForwardedUserEqualToReceivedByUser(C)
One way is to disallow update, insert, delete rights on the tables, and enforce the business requirement using a stored procedure. For example,
create procedure dbo.AddRequestForward(
#requestId int
, #forwardedToUserId int)
as
insert ForwardedRequests
(ForwardedRequests, ForwardedRequests)
select #requestId
, #forwardedToUserId
where not exists
(
select *
from Requests
where Id = #requestId
and #forwardedToUserId = #forwardedToUserId
)
if ##rowcount = 0
return -1 -- Forwarded and Received identical user
return 1 -- Success
go

A trigger to find the sum of one field in a different table and error if it's over a certain value in oracle

I have two tables
moduleprogress which contains fields:
studentid
modulecode
moduleyear
modules which contains fields:
modulecode
credits
I need a trigger to run when the user is attempting to insert or update data in the moduleprogress table.
The trigger needs to:
look at the studentid that the user has input and look at all modules that they have taken in moduleyear "1".
take the modulecode the user input and look at the modules table and find the sum of the credits field for all these modules (each module is worth 10 or 20 credits).
if the value is above 120 (yearly credit limit) then it needs to error; if not, input is ok.
Does this make sense? Is this possible?
#a_horse_with_no_name
This looks like it will work but I will only be using the database to input data manually so it needs to error on input. I'm trying to get a trigger similar to this to solve the problem(trigger doesn't work) and forget that "UOS_" is before everything. Just helps me with my database and other functions.
CREATE OR REPLACE TRIGGER "UOS_TESTINGS"
BEFORE UPDATE OR INSERT ON UOS_MODULE_PROGRESS
REFERENCING NEW AS NEW OLD AS OLD
DECLARE
MODULECREDITS INTEGER;
BEGIN
SELECT
m.UOS_CREDITS,
mp.UOS_MODULE_YEAR,
SUM(m.UOS_CREDITS)
INTO MODULECREDITS
FROM UOS_MODULE_PROGRESS mp JOIN UOS_MODULES m
ON m.UOS_MODULE_CODE = mp.UOS_MODULE_CODE
WHERE mp.UOS_MODULE_YEAR = 1;
IF MODULECREDITS >= 120 THEN
RAISE_APPLICATION_ERROR(-20000, 'Students are only allowed to take upto 120 credits per year');
END IF;
END;
I get the error message :
8 23 PL/SQL: ORA-00947: not enough values
4 1 PL/SQL: SQL Statement ignored
I'm not sure I understand your description, but the way I understand it, this can be solved using a materialized view, which might give better transactional behaviour than the trigger:
CREATE MATERIALIZED VIEW LOG
ON moduleprogress WITH ROWID (modulecode, studentid, moduleyear)
INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW LOG
ON modules with rowid (modulecode, credits)
INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW mv_module_credits
REFRESH FAST ON COMMIT WITH ROWID
AS
SELECT pr.studentid,
SUM(m.credits) AS total_credits
FROM moduleprogress pr
JOIN modules m ON pr.modulecode = m.modulecode
WHERE pr.moduleyear = 1
GROUP BY pr.studentid;
ALTER TABLE mv_module_credits
ADD CONSTRAINT check_total_credits CHECK (total_credits <= 120)
But: depending on the size of the table this might however be slower than a pure trigger based solution.
The only drawback of this solution is, that the error will be thrown at commit time, not when the insert happens (because the MV is only refreshed on commit, and the check constraint is evaluated then)