Alternative to check constraints in Views - sql

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

Related

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

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.

Is it possible to apply default filter on a table in SQL Server?

I have a table named Users(for example), and it has 3 columns(Id, Name, and IsDeleted), IsDeleted indicates the user is deleted, or not?
When i run below select statement(select * from users), is it possible to filter out the records which has IsDeleted equals 1, automatically? I have a database which designed as soft delete only, so there are many table with this IsDeleted column. when query database, we have to add where clause to filter these records out, it's very annoying, especially when query/join multiple tables. I want ask here, is there some feature(like default filter?) to do this. so,the deleted records can only be queried, when the table default filter is disabled.
A clean and easily understandable way to achieve this is with views. You could create a view that filters out users by this flag and returns all columns.
CREATE VIEW v_Users AS
SELECT * FROM Users WHERE IsDeleted = 0
Then in all your queries you can use this view instead of the table.
SELECT u.email, u.name, likes.*
FROM v_Users AS u
INNER JOIN likes ON likes.user_id = u.id
Full example fiddle: http://rextester.com/QDN58706
Variant 1: You can create new table with deleted records, trigger on deletion (or instead of insert/update/delete) and with delete move all records into new table. You don't have to remap your actual queries. You can have still foreign keys and you can force referencial integrity.
CREATE TABLE dbo.r_users (s__row_id int not null identity(1,1), s__dml_dt datetime not null, s__dml_type char(1) not null, col1...)
CREATE TRIGGER dbo.trg_del_users
ON dbo.users
AS
INSERT INTO dbo.r_users select getdate(), 'd', * from dbo.users
DELETE FROM dbo.users WHERE IsDeleted = 1
Variant 2: You can create view on filtered table/TVF/CTE.
There is not feathure you want, only scripting.

Use a column name to specify a predefined table

I'm trying to use set up a database for a school project, and I'm using triggers to set up referential integrity for one table. I have a table, Addresses, which stores the address for People, Studios, and Directors. Then I have a table called Address Reference. This table points to the Address table, and it has a two fields, the ReferenceID and the TableName to show which table and row this address is for. I have a Constraint so TableName will always be valid.
I'm trying to set up a trigger to make sure any rows inserted are valid, which I can do, I'm just trying to improve it. My code would look like this:
SELECT *
FROM inserted
WHERE ReferenceID IN
(SELECT PersonID
FROM inserted.TableName)
However I found I needed to use dynamic sql. So I was thinking something like this:
SELECT *
FROM inserted
WHERE ReferenceID IN
(EXEC('SELECT PersonID FROM' + inserted.TableName))
Which didn't work, even when I removed the exec.
I'm doing this in SQL Server Management Studio With SQL Server 11.0.3128
Let me know if you need any more information. I've looked around, and I haven't found any answers to this question that work.
This is a poor way to maintain referential integrity. There are a number of ways you could approach this.
The first would be to have an address table, then multiple tables to contain the links, e.g.
CREATE TABLE StudioAddress
( StudioID INT NOT NULL,
AddressID INT NOT NULL,
CONSTRAINT PK_StudioAddress__StudioID_AddressID PRIMARY KEY (StudioID, AddressID),
CONSTRAINT FK_StudioAddress__StudioID FOREIGN KEY (StudioID) REFERENCES Studio (StudioID),
CONSTRAINT FK_StudioAddress__AddressID FOREIGN KEY (AddressID) REFERENCES Address (AddressID)
);
This maintains your referenctial integrity without needing triggers, and still caters for a 1 to many relationship.
Another option would be to have 3 nullable columns in your address table (StudioID, PersonID, DirectorID), each with a foreign key to the relevant table, you can the add a check constraint to ensure only one of the 3 fields is populated (if this is required).
I much prefer the first option though, it is much cleaner, and also allows for the same address to be used for multiple things.
ADENDUM
If this has to be done using triggers, then I think you would need to use something like this:
IF EXISTS( SELECT 1
FROM inserted i
WHERE NOT EXISTS
( SELECT 1
FROM People p
WHERE p.PersonID = i.ReferenceID
AND i.TableName = 'People'
UNION ALL
SELECT 1
FROM Studios s
WHERE s.StudioID = i.ReferenceID
AND i.TableName = 'Studios'
UNION ALL
SELECT 1
FROM Directors d
WHERE d.DirectorID = i.ReferenceID
AND i.TableName = 'Directors'
)
)
BEGIN
ROLLBACK TRANSACTION;
RAISERROR('Referential integrity error', 16, 1);
END
This essentially checks that for all inserted/updated rows a record exists with the relevant ID in the relevant table.
I still stand by my earlier answer though, that this is a terrible approach, and I would question any syllabus this is on!

Creating a trigger in order to maintain an intermediate table integrity

I've created a database with the following diagram:
http://imagizer.imageshack.us/v2/800x489q90/440/o1yu.png
The TrainerPokemon table represents an occurrence of a pokemon (that belongs to a trainer), while the Pokemon table represents a pokemon 'specie'. One certain specie of pokemon may have access to certain abilities, but an occurrence of this pokemon may carry only one ability (column AbilityID on the TrainerPokemon table).
Here's my problem: I was trying to create a trigger in order to prevent that any other ability (different from what is specified in the PokemonAbility table) is inserted/updated to a register in the TrainerPokemon table, but I did not have success.
Does anyone has any idea for a trigger? New ideas for this database model are welcome as well.
(Important Note: I am using SQL Server 2012)
Something like this (typing off the top of my head):
CREATE TRIGGER trg_pok_abil ON TrainerPokemon FOR UPDATE, INSERT
AS
BEGIN
IF EXISTS (
SELECT *
FROM inserted
WHERE inserted.AbilityID NOT (
SELECT AbilityID
FROM PokemonAbility
WHERE PokemonAbility.PokemonID = inserted.PokemonID
)
BEGIN
ROLLBACK
END
END
You could add a two column foreign key constraint between pokemonability and trainerpokemon on pokemonid, abilityid.
HTH

How Do I Deep Copy a Set of Data, and Change FK References to Point to All the Copies?

Suppose I have Table A and Table B. Table B references Table A. I want to deep copy a set of rows in Table A and Table B. I want all of the new Table B rows to reference the new Table A rows.
Note that I'm not copying the rows into any other tables. The rows in table A will be copied into table A, and the rows in table B will be copied into table B.
How can I ensure that the foreign key references get readjusted as part of the copy?
To clarify, I'm trying to find a generic way to do this. The example I'm giving involves two tables, but in practice the dependency graph may be much more complicated. Even a generic way to dynamically generate SQL to do the work would be fine.
UPDATE:
People are asking why this is necessary, so I'll give some background. It may be way too much, but here goes:
I'm working with an old desktop application that's been moved to a client-server model. But, the application still uses a rudimentary in-house binary file format for storing data for its tables. A data file is just a header followed by a series of rows, each of which is just the binary serialized field values, the order of which is determined by a schema text file. The only thing good about it is that it's very fast. It's terrible in every other respect. I'm moving the application to SQL Server and trying not to degrade the performance too badly.
This is a kind of scheduling application; the data's not critical to anybody, and there's no audit tracking, etc. necessary. It's not a supermassive amount of data, and we don't necessarily need to keep very old data around if the database grows too large.
One feature that they are accustomed to is the ability to duplicate entire schedules in order to create "what-if" scenarios that they can muck with. Any user can do this as many times as they want, as often as they want. In the old database, the data files for each schedule are stored in their own data folder, identified by name. So, copying a schedule was as simple as copying the data folder and renaming it.
I must be able to do effectively the same thing with SQL Server or the migration will not work. Maybe you're thinking that I can just only copy the data that actually gets changed in order to avoid redundancy; but that honestly sounds too complicated to be feasible.
To throw another wrench into the mix, there can be a hierarchy of schedule data folders. So, a data folder may contain a data folder, which may contain a data folder. And the copying can occur at any level.
In SQL Server, I'm implementing a nested set hierarchy to mimic this. I have a DATA_SET table like this:
CREATE TABLE dbo.DATA_SET
(
DATA_SET_ID UNIQUEIDENTIFIER PRIMARY KEY,
NAME NVARCHAR(128) NOT NULL,
LFT INT NOT NULL,
RGT INT NOT NULL
)
So, there's a tree structure of data sets. Each data set represents a schedule, and may contain child data sets. Every row in every table has a DATA_SET_ID FK reference, indicating which data set it belongs to. Whenever I copy a data set, I copy all the rows in the table for that data set, and every other data set, into the same table, but referencing new data sets.
So, here's a simple concrete example:
CREATE TABLE FOO
(
FOO_ID BIGINT PRIMARY KEY,
DATA_SET_ID BIGINT FOREIGN KEY REFERENCES DATA_SET(DATA_SET_ID) NOT NULL
)
CREATE TABLE BAR
(
BAR_ID BIGINT PRIMARY KEY,
DATA_SET_ID BIGINT FOREIGN KEY REFERENCES DATA_SET(DATA_SET_ID) NOT NULL,
FOO_ID UNIQUEIDENTIFIER PRIMARY KEY
)
INSERT INTO FOO
SELECT 1, 1 UNION ALL
SELECT 2, 1 UNION ALL
SELECT 3, 1 UNION ALL
INSERT INTO BAR
SELECT 1, 1, 1
SELECT 2, 1, 2
SELECT 3, 1, 3
So, let's say I copy data set 1 into a new data set of ID 2. After I copy, the tables will look like this:
FOO
FOO_ID, DATA_SET_ID
1 1
2 1
3 1
4 2
5 2
6 2
BAR
BAR_ID, DATA_SET_ID, FOO_ID
1 1 1
2 1 2
3 1 3
4 2 4
5 2 5
6 2 6
As you can see, the new BAR rows are referencing the new FOO rows. It's not the rewiring of the DATA_SET_ID's that I'm asking about. I'm asking about rewiring the foreign keys in general.
So, that was surely too much information, but there you go.
I'm sure there are a lot of concerns about performance with the idea of bulk copying the data like this. The tables are not going to be huge. I'm not expecting more than 1000 records in any table, and most of the tables will be much much smaller than that. Old data sets can be deleted outright with no repercussions.
Thanks,
Tedderz
Here is an example with three tables that can probably get you started.
DB schema
CREATE TABLE users
(user_id int auto_increment PRIMARY KEY,
user_name varchar(32));
CREATE TABLE agenda
(agenda_id int auto_increment PRIMARY KEY,
`user_id` int, `agenda_name` varchar(7));
CREATE TABLE events
(event_id int auto_increment PRIMARY KEY,
`agenda_id` int,
`event_name` varchar(8));
An SP to clone a user with his agenda and events records
DELIMITER $$
CREATE PROCEDURE clone_user(IN uid INT)
BEGIN
DECLARE last_user_id INT DEFAULT 0;
INSERT INTO users (user_name)
SELECT user_name
FROM users
WHERE user_id = uid;
SET last_user_id = LAST_INSERT_ID();
INSERT INTO agenda (user_id, agenda_name)
SELECT last_user_id, agenda_name
FROM agenda
WHERE user_id = uid;
INSERT INTO events (agenda_id, event_name)
SELECT a3.agenda_id_new, e.event_name
FROM events e JOIN
(SELECT a1.agenda_id agenda_id_old,
a2.agenda_id agenda_id_new
FROM
(SELECT agenda_id, #n := #n + 1 n
FROM agenda, (SELECT #n := 0) n
WHERE user_id = uid
ORDER BY agenda_id) a1 JOIN
(SELECT agenda_id, #m := #m + 1 m
FROM agenda, (SELECT #m := 0) m
WHERE user_id = last_user_id
ORDER BY agenda_id) a2 ON a1.n = a2.m) a3
ON e.agenda_id = a3.agenda_id_old;
END$$
DELIMITER ;
To clone a user
CALL clone_user(3);
Here is SQLFiddle demo.
I recently found myself needing to solve a similar problem; that is, I needed to copy a set of rows in a table (Table A) as well as all of the rows in related tables which have foreign keys pointing to Table A's primary key. I was using Postgres so the exact queries may differ but the overall approach is the same. The biggest benefit of this approach is that it can be used recursively to go infinitely deep
TLDR: the approach looks like this
1) find all the related table/columns of Table A
2) copy the necessary data into temporary tables
3) create a trigger and function to propagate primary key column
updates to related foreign keys columns in the temporary tables
4) update the primary key column in the temporary tables to the next
value in the auto increment sequence
5) Re-insert the data back into the source tables, and drop the
temporary tables/triggers/function
1) The first step is to query the information schema to find all of the tables and columns which are referencing Table A. In Postgres this might look like the following:
SELECT tc.table_name, kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE constraint_type = 'FOREIGN KEY'
AND ccu.table_name='<Table A>'
AND ccu.column_name='<Primary Key>'
2) Next we need to copy the data from Table A, and any other tables which reference Table A - lets say there is one called Table B. To start this process, lets create a temporary table for each of these tables and we will populate it with the data that we need to copy. This might look like the following:
CREATE TEMP TABLE temp_table_a AS (
SELECT * FROM <Table A> WHERE ...
)
CREATE TEMP TABLE temp_table_b AS (
SELECT * FROM <Table B> WHERE <Foreign Key> IN (
SELECT <Primary Key> FROM temp_table_a
)
)
3) We can now define a function that will cascade primary key column updates out to related foreign key columns, and trigger which will execute whenever the primary key column changes. For example:
CREATE OR REPLACE FUNCTION cascade_temp_table_a_pk()
RETURNS trigger AS
$$
BEGIN
UPDATE <Temp Table B> SET <Foreign Key> = NEW.<Primary Key>
WHERE <Foreign Key> = OLD.<Primary Key>;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_temp_table_a
AFTER UPDATE
ON <Temp Table A>
FOR EACH ROW
WHEN (OLD.<Primary Key> != NEW.<Primary Key>)
EXECUTE PROCEDURE cascade_temp_table_a_pk();
4) Now we just update the primary key column in to the next value of the sequence of the source table (). This will activate the trigger, and the updates will be cascaded out to the foreign key columns in . In Postgres you can do the following:
UPDATE <Temp Table A>
SET <Primary Key> = nextval(pg_get_serial_sequence('<Table A>', '<Primary Key>'))
5) Insert the data back from the temporary tables back into the source tables. And then drop the temporary tables, triggers, and functions after that.
INSERT INTO <Table A> (SELECT * FROM <Temp Table A>)
INSERT INTO <Table B> (SELECT * FROM <Temp Table B>)
DROP TRIGGER trigger_temp_table_a
DROP cascade_temp_table_a_pk()
It is possible to take this general approach and turn it into a script which can be called recursively in order to go infinitely deep. I ended up doing just that using python (our application was using django so I was able to use the django ORM to make some of this easier)