Trigger for 2 Tables - sql

this is a homework question, just to make it clear.
This is the relational schema:
PaperInvolvement (paperNr, academicId, paperRole)
Academic (academicId, acadName, employer)
So (academicID) is the primary key for Academic and (paperNr, academicId) is the primary key for PaperInvolvement table.
Here is the trigger that I am asked to do:
On PaperInvolvement after insert, update
On Academic after update
Prevent any 2 academics who work for the same company are involved in the same paper in the opposite roles.
Use stored procedure or cover it completely in trigger
There are only 2 roles available in this table, which is Reviewer and Author
This is what I have done so far:
CREATE TRIGGER TR_PaperInvolvement_1
ON PaperInvolvement
AFTER INSERT, UPDATE
AS
IF EXISTS
(
SELECT a.academicId, paperRole, paperNr
FROM
(SELECT academicId
FROM Academic
GROUP BY employer, academicId) AS a
JOIN
(SELECT academicId, paperRole, paperNr
FROM PaperInvolvement
GROUP BY paperNr, academicId, paperRole) AS p_inv
ON a.academicId = p_inv.academicId
WHERE paperRole = 'Author' AND paperRole = 'Reviewer'
)
BEGIN
RAISERROR('Cannot have 2 Academics from the same company to work on
different roles for this paper.',16,1)
ROLLBACK TRANSACTION
END
GO
My question is, based on the requirements (what I have listed on the bullet-lists), is this the correct way to answer the question?

Try this
CREATE TRIGGER TR_PaperInvolvement_Modify
ON PaperInvolvement
AFTER INSERT, UPDATE
AS
begin
if exists
(
select P.paperNr, A.employer
from PaperInvolvement as P
inner join Academic as A on A.academicID = P.academicID
where P.paperNr in (select i.paperNr from inserted as i)
group by P.paperNr, A.employer
having
count(case when P.paperRole = 'Author' then 1 end) > 0 and
count(case when P.paperRole = 'Reviewer' then 1 end) > 0
)
begin
raiserror('Cannot have 2 Academics from the same company to work on different roles for this paper.', 16, 1)
rollback transaction
end
end

Related

Trigger to insert multiple rows based on many-to-many relationship

I have three tables Reservation, Reservation_Passenger and Ticket.
Each reservation can have multiple passengers. I need to create a trigger to insert a ticket (according to the number of passengers) every time the Reservation status is updated to 'Booked'. How can I achieve it?
Reservation (reservationId, status)
Reservation_Passenger (reservationId, passengerId)
Ticket (ticketId, passengerId, issuedDate)
What I have tried:
CREATE
TRIGGER Generate_Ticket
ON Reservation
AFTER UPDATE
AS
DECLARE #reservationStatus varchar(15)
SELECT #reservationStatus = INSERTED.Status from INSERTED
IF #reservationStatus = 'Booked'
BEGIN
--stuck here
END
GO
The same way you store the status into a variable, you could also retrieve the reservationId
DECLARE #reservationStatus varchar(15)
DECLARE #reservationId int
SELECT #reservationId = INSERTED.reservationId,
#reservationStatus = INSERTED.Status
FROM INSERTED
Now in the part where you are stuck, to create a Ticket to every passenger on the reservation you can feed an INSERT with a SELECT of the related passengers.
INSERT INTO Ticket (passengerId, issuedDate)
SELECT passengerId, getdate()
FROM Reservation_Passenger
WHERE reservationId = #reservationId
PS You will need to be careful that your code doesn't change more than one reservation to booked on the same UPDATE command. Because in that case the trigger is only fired once, with all the updated reservations stored in the INSERTED dataset. You will need to use a CURSOR to loop through all those reservations to apply your logic, or switch to this simpler trigger that creates tickets for all the passengers of all the booked reservations in one single step:
CREATE TRIGGER Generate_Ticket ON Reservation AFTER UPDATE
AS
INSERT INTO Ticket (passengerId, issuedDate)
SELECT P.passengerId, getdate()
FROM INSERTED as R
INNER JOIN Reservation_Passenger as P on P.reservationId = R.reservationID
WHERE R.Status = 'Booked'
You should also be careful because the trigger fires when any field is updated on the Reservation table. If you were to update another field, for example a comment, on an already booked reservation, your trigger will duplicate all his tickets again.
I recommend you to check not only that INSERTED.Status = 'Booked', but also that DELETED.Status <> 'Booked', so you only create tickets when the Status field has changed to Booked from something else.
That would be :
CREATE TRIGGER Generate_Ticket ON Reservation AFTER UPDATE
AS
INSERT INTO Ticket (passengerId, issuedDate)
SELECT P.passengerId, getdate()
FROM INSERTED as I
INNER JOIN DELETED as D on D.reservationId = I.reservationID
INNER JOIN Reservation_Passenger as P on P.reservationId = I.reservationID
WHERE I.Status = 'Booked' and coalesce(D.Status, '') <> 'Booked'

IF ELSE condition with SQL Server trigger

I am populating a table entirely using triggers, it populates the table if LocationID and ProductID does not exists and if it already does it updates the given data.
I have posted the following code snippet looking for a possible solution or link to one.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[newpurchase]
ON [dbo].[PurchaseMaster]
AFTER INSERT
AS
BEGIN
IF (((SELECT COUNT(*) FROM StockMaster
WHERE LocationID = (SELECT LocationID FROM inserted)) > 0)
AND ((SELECT COUNT(*) FROM StockMaster
WHERE LocationID = (SELECT LocationID FROM inserted)) > 0))
UPDATE StockMaster
SET TotalPurchased = TotalPurchased + (SELECT PurchasedQTY FROM inserted)
WHERE LocationID = (SELECT LocationID FROM inserted)
AND ProductID = (SELECT ProductID FROM inserted);
ELSE
INSERT INTO StockMaster (LocationID, ProductID, TotalPurchased, TotalSold, OnHand)
SELECT LocationID, ProductID, PurchasedQTY, 0, 0 FROM inserted;
END
I am not entirely sure exactly what you are looking for. If your return sets cannot be collected dynamically, and you need another table to always update based off the information persisted by another another table then I do think triggers are fine. There is always overhead with triggers, so keep that in mind.
You could do the way you are doing it, and from what I can see on the surface level the trigger may be what you are looking for (but this also depends upon your business needs and relationships of tables). HOWEVER, you will have to be careful if your Insert is a bulk insert then what you have will throw an error.
I also noticed your If statement is checking LocationId twice instead of LocationId and ProductId.
The trigger you are trying to create works with only one value at a time. You could rewrite this to a more set based trigger for when bulk inserts occur. This way if you have one insert or bulk inserts these two queries can perform for that values where needed.
first you want to update all the values where LocationId and ProductId are the >same found in StockMaster
if no values match then nothing will be updated
Update StockMaster
Set TotalPurchased = sm.TotalPurchased + i.PurchasedQty
From StockMaster sm
inner join inserted i on sm.LocationId = i.LocationId and sm.ProductId = i.ProductId
next you want to insert any row that isn't found in StockMaster
if all values matched above, they will be weeded out with the wheree condition >and,then nothing would be inserted here, those that didn't match would be
inserted
INSERT INTO StockMaster (LocationID, ProductID, TotalPurchased, TotalSold, OnHand)
SELECT i.LocationID, i.ProductID, i.PurchasedQTY, 0, 0
FROM inserted i
left join StockMaster sm on i.LocationId = sm.LocationId and i.ProductId = sm.ProductId
where sm.{Id} is null;--not {id} is for whatever key this table uses

Postgres trigger that affects rows with certain values

I'm postgres newbie and i have a question regarding triggers. Lets say we have 2 tables. One with cities and one with people.
CITIES:
PEOPLE:
I need a trigger that updates 'processed' value in table cities from 0 to 1 when 'processed' value in table people updates from 0 to 1 for all people from that city.
For example: John, Ian and Claire are from Berlin and when 'processed' value is updated to 1 for each of them, then the trigger updates processed value for Berlin in table cities to 1.
What is the best way to do this?
This will work:
CREATE OR REPLACE FUNCTION mytrigger() RETURNS TRIGGER AS $mytrigger$
BEGIN
IF (SELECT EXISTS (SELECT 1 FROM PEOPLE WHERE city_id = NEW.city_id AND processed = '0')) THEN
UPDATE CITIES SET processed = '0' WHERE id = NEW.city_id;
RETURN NEW;
ELSE
UPDATE CITIES SET processed = '1' WHERE id = NEW.city_id;
RETURN NEW;
END IF;
END;
$mytrigger$ LANGUAGE plpgsql;
CREATE TRIGGER mytrigger
AFTER UPDATE ON PEOPLE
FOR EACH ROW EXECUTE PROCEDURE mytrigger();
try this query:
with c as (
select city_id
from people
where name = NEW.name
)
, a as (
select avg(processed)
from people
join c on c.city_id = people.city_id
)
update cities u
set processed = 1
from a
where u.id = a.city_id
and avg = 1
alias c gets city id, then a aggregates processed for that sict id in people and lastly update updates only when all names are processed.

Trigger referring to another subelement table

In SQL Server there two tables: Invoices (InvoiceId, Number, Date, Customer, TotalValue) and InvoicesElements (InvoiceId, Good, Qty, Value).
Each transaction inserts one row into Invoices and one/many rows into InvoicesElements.
I need to set a trigger on the Invoices table that will raise an error and rollback transaction when Good in the InvoicesElements table is 'Bike' and Customer is 'ABC'.
Any help much appreciated.
Przemek
ALTER TABLE
InvoicesElements
ADD CONSTRAINT
CHK_GOOD
CHECK (good <> 'Bike' OR good IS NULL)
Update:
CREATE TRIGGER
TR_InvoicesElements_AIU
ON InvoicesElements
AFTER INSERT, UPDATE
AS
IF EXISTS
(
SELECT NULL
FROM INSERTED ie
JOIN Invoices inv
ON inv.id = ie.invoiceId
WHERE ie.good = 'bike'
AND inv.customer = 'ABC'
)
THROW 50000, 'Not sure why but you cannot sell bikes to ABC', 0
GO
CREATE TRIGGER
TR_Invoices_AIU
ON Invoices
AFTER INSERT, UPDATE
AS
IF EXISTS
(
SELECT NULL
FROM InvoiceElements ie
JOIN INSERTED inv
ON inv.id = ie.invoiceId
WHERE ie.good = 'bike'
AND inv.customer = 'ABC'
)
THROW 50000, 'Not sure why but you cannot sell bikes to ABC', 0
GO

Is it possible to delete from multiple tables in the same SQL statement?

It's possible to delete using join statements to qualify the set to be deleted, such as the following:
DELETE J
FROM Users U
inner join LinkingTable J on U.id = J.U_id
inner join Groups G on J.G_id = G.id
WHERE G.Name = 'Whatever'
and U.Name not in ('Exclude list')
However I'm interested in deleting both sides of the join criteria -- both the LinkingTable record and the User record on which it depends. I can't turn cascades on because my solution is Entity Framework code first and the bidirectional relationships make for multiple cascade paths.
Ideally, I'd like something like:
DELETE J, U
FROM Users U
inner join LinkingTable J on U.id = J.U_id
...
Syntactically this doesn't work out, but I'm curious if something like this is possible?
Nope, you'd need to run multiple statements.
Because you need to delete from two tables, consider creating a temp table of the matching ids:
SELECT U.Id INTO #RecordsToDelete
FROM Users U
JOIN LinkingTable J ON U.Id = J.U_Id
...
And then delete from each of the tables:
DELETE FROM Users
WHERE Id IN (SELECT Id FROM #RecordsToDelete)
DELETE FROM LinkingTable
WHERE Id IN (SELECT Id FROM #RecordsToDelete)
The way you say is Possible in MY SQL but not for SQL SERVER
You can use of the "deleted" pseudo table for deleting the values from Two Tables at a time like,
begin transaction;
declare #deletedIds table ( samcol1 varchar(25) );
delete #temp1
output deleted.samcol1 into #deletedIds
from #temp1 t1
join #temp2 t2
on t2.samcol1 = t1.samcol1
delete #temp2
from #temp2 t2
join #deletedIds d
on d.samcol1 = t2.samcol1;
commit transaction;
For brief Explanation you can take a look at this Link
and to Know the Use of Deleted Table you can follow this Using the inserted and deleted Tables
The only way I could think of is logically break the bi-directional foreign keys in a procedural way.
This approach can have huge impact to your application side if you don't have some flags for visualization state or status
Something like
INSERT dummy not visible rows to Users (with something like Id = -1 for dummy values)
Add to LinkingTable an alternative column to point back to Users, I'll call it U_ComesFrom
ALTER TABLE LinkingTagble ADD U_ComesFrom_U_id INT DEFAULT(-1)
Add FOREIGN KEY with a NOCHECK
ALTER TABLE LinkingTable WITH NOCHECK
FOREIGN KEY (U_ComesFrom_U_id)
REFERENCES Users (Id) ;
Add to Users column
ALTER TABLE Users ADD MarkedForDeletion BIT NOT NULL DEFAULT(0)
Then your SQL would look like
BEGIN TRANSACTION
UPDATE J
SET U_Comes_From_U_id = U_ID, U_id = -1 -- or some N/R value that you define in Users
FROM Users U
inner join LinkingTable J on U.id = J.U_id
inner join Groups G on J.G_id = G.id
WHERE G.Name = 'Whatever'
and U.Name not in ('Exclude list')
UPDATE U
SET MarkedForDeletion = 1
FROM Users
inner join LinkingTable J on U.id = J.U_ComesFrom_U_id
WHERE U_id > 0
DELETE FROM LinkingTable
WHERE U_ComesFrom_U_id > 0
DELETE FROM Users
WHERE MarkedForDeletion = 1
COMMIT
This approach would impact the performance since each transaction would be at least 4 DML operations per bi-directional keys.
Use TRY CATCH with Transaction
BEGIN TRANSACTION
BEGIN TRY
DELETE from A WHERE id=1
DELETE FROM b WHERE id=1
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH
or
you can also use Store procedure for same
Using Stored Procedure With Transaction:
If you are creating the foreign key through T-SQL you must append the ON DELETE CASCADE option to the foreign key:
Code Snippet
ALTER TABLE <tablename>
ADD CONSTRAINT <constraintname> FOREIGN KEY (<columnname(s)>)
REFERENCES <referencedtablename> (<columnname(s)>)
ON DELETE CASCADE;