SQL Server 2012 Trigger - sql

I have a small little thing with SQL that's been bothering me now for a while, let's say I have two tables (Customer and Loan). However, I want a trigger that's checking based on the Borrowertype attribute. I suppose with the second query after AND I need something to check whether the userID in Loans are the same as the one in Customer, but must be messing it up or I'm completely thinking this the wrong way.
CREATE TABLE Customer
(
userID int identity primary key,
Name varchar(20),
Borrowertype varchar(20)
);
CREATE TABLE Loan
(
Id int identity primary key,
userID int,
FOREIGN KEY (userID) REFERENCES Customer(userID)
);
IF OBJECT_ID ('Customer.maximum_books_per_user','TR') IS NOT NULL
DROP TRIGGER Customer.maximum_books_per_user;
GO
CREATE TRIGGER maximum_books_per_user ON Customer
AFTER INSERT
AS
IF (SELECT Borrowertype FROM Customer) = 'diffborrowertypehere'
AND (SELECT COUNT(*) FROM inserted AS i JOIN Customer AS c
ON ??? WHERE ???
) > 5
BEGIN
ROLLBACK TRANSACTION
RAISERROR('You have reached maximum allowed loans.', 16, 1)
END
GO

Your trigger needs to be on the Loan table, as that's where a row would be being inserted that could be rejected. Something like this:
EDIT: rewritten to handle inserts for multiple Customers at once
CREATE TRIGGER maximum_books_per_user ON Loan
FOR INSERT
AS
-- Fail if there are any customers that will have more than the maximum number of loans
IF EXISTS (
SELECT i.userID, COUNT(*)
FROM inserted i
JOIN Loan l
ON i.userID = l.userID
GROUP BY i.userID
HAVING COUNT(*) >= 5
)
BEGIN
ROLLBACK TRANSACTION
RAISERROR('You have reached maximum allowed loans.', 16, 1)
END

Related

Trigger to update balance after a transaction amount record has been changed

Tables account and transactions
Account = {accNumber, balance, ...}
Transaction = {accNumber, amount, transNumber, ...}
Where Transaction.accNumber references Account.accNumber
I have managed to create trigger to update balance after INSERT
CREATE TRIGGER TR_Account_Balance
ON Transactions AFTER INSERT
AS
BEGIN
UPDATE account SET
balance = ins.newBalance
FROM (
SELECT a.accnumber,a.balance + SUM(i.amount) AS newBalance
FROM Account a
INNER JOIN inserted i ON i.accNumber = a.accNumber
GROUP BY a.accNumber, a.balance
) AS ins
WHERE account.accnumber = ins.accnumber
END
Now I need to create a trigger that would change balance accordingly to transaction AFTER UPDATE.
Example:
|accNumber | balance | ...
|-----------------------------
| 1 | 100 | ...
|accNumber | amount | ...
|-----------------------------
| 1 | 20 | ...
If we UPDATE amount to 10, balance should change to 90.
How can I possibly do that?
You should really do this in a normalized fashion, by using a view. For better performance, you can index it.
Indexed views are subject to some restrictions, in particular:
No outer joins or applys
Must be schema-bound
Grouped views need COUNT_BIG and can only use SUM as another aggregate
CREATE VIEW dbo.vAccountBalance
WITH SCHEMABINDING AS
SELECT
tr.accnumber,
SUM(tr.amount) AS balance,
COUNT_BIG(*) AS numTransactions -- this MUST be added
FROM dbo.Transactions tr; -- schema-qualify
GO
CREATE UNIQUE CLUSTERED INDEX CX_vAccountBalance ON dbo.vAccountBalance (accnumber);
The server will maintain this index together with other indexes, during any insert, update or delete.
If you really wanted to do this in a trigger, you can use the following
Note how the Account table is only referenced once, and the difference is added, rather than self-joining again
Note how inserted and deleted are joined together by primary key, and the difference is summed
CREATE TRIGGER TR_Account_Balance
ON Transactions AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM inserted) AND NOT EXISTS (SELECT 1 FROM deleted)
RETURN; -- early bail-out
UPDATE a -- update the already referenced Account table
SET
balance += ins.diffBalance
FROM Account a
INNER JOIN (
SELECT
i.accnumber,
SUM(i.amount) AS diffBalance
FROM (
SELECT i.transNumber, i.accnumber, i.amount
FROM inserted i
)
FULL JOIN (
SELECT d.transNumber, d.accnumber, -(d.amount)
FROM deleted d
) ON i.transNumber = a.transNumber
GROUP BY i.accNumber
) AS ins ON a.accnumber = ins.accnumber;
GO
You could also split this up into separate INSERT UPDATE and DELETE triggers, in which case you can remove the deleted section for the former, remove the inserted section for the latter, and change the UPDATE one to use an INNER JOIN instead of a FULL JOIN.
If you do the insert update delete via a proc that will be the best place to update the mapped table or other tables as well.
If you still want to do it in a trigger (Carefully) please compute your SUM at same table level and update the balance on main table so it'll cover update and delete as well.
Schema:
DROP TABLE IF EXISTS dbo.AccountTransaction
DROP TABLE IF EXISTS dbo.Account
CREATE TABLE dbo.Account
(
AccountNumber INT CONSTRAINT PK_AccountId PRIMARY KEY CLUSTERED IDENTITY(1, 1) NOT NULL,
Balance DECIMAL(18, 9) CONSTRAINT DF_Account_Balance DEFAULT 0.0 NOT NULL
)
GO
INSERT INTO dbo.Account
(
Balance
)
VALUES
(
DEFAULT -- decimal(18, 9)
)
CREATE TABLE dbo.AccountTransaction
(
AccountTransactionId INT CONSTRAINT PK_AccountTransactionId PRIMARY KEY CLUSTERED IDENTITY(1, 1) NOT NULL,
AccountNumber INT CONSTRAINT FK_AccountTransaction_Account FOREIGN KEY REFERENCES dbo.Account (AccountNumber) NOT NULL,
Amount DECIMAL(18, 9) CONSTRAINT DF_AccountTransaction_Amount DEFAULT 0.0 NOT NULL
)
GO
CREATE TRIGGER dbo.tr_AccountTransaction
ON dbo.AccountTransaction
FOR INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Inserted AS INT =
(
SELECT COUNT (1)
FROM INSERTED
)
DECLARE #Deleted AS INT =
(
SELECT COUNT (1)
FROM DELETED
)
IF #Inserted > 0
BEGIN
UPDATE dbo.Account
SET Balance = x.NewBalance
FROM
(
SELECT SUM (at.Amount) AS NewBalance
FROM Inserted AS i
JOIN dbo.AccountTransaction AS at
ON at.AccountNumber = i.AccountNumber
GROUP BY i.AccountNumber
) AS x
END
IF #Inserted = 0
AND #Deleted > 0
BEGIN
UPDATE dbo.Account
SET Balance = x.NewBalance
FROM
(
SELECT SUM (at.Amount) AS NewBalance
FROM Deleted AS d
JOIN dbo.AccountTransaction AS at
ON at.AccountNumber = d.AccountNumber
GROUP BY d.AccountNumber
) AS x
END
END
GO
** DEBUG**
INSERT INTO dbo.AccountTransaction
(
AccountNumber,
Amount
)
SELECT a.AccountNumber,
12.0
FROM dbo.Account AS a
SELECT a.AccountNumber,
a.Balance
FROM dbo.Account AS a
UPDATE at
SET at.Amount += 30
FROM dbo.AccountTransaction AS at
WHERE at.AccountTransactionId = 1
SELECT a.AccountNumber,
a.Balance
FROM dbo.Account AS a
SELECT at.AccountTransactionId,
at.AccountNumber,
at.Amount
FROM dbo.AccountTransaction AS at
UPDATE at
SET at.Amount -= 20
FROM dbo.AccountTransaction AS at
WHERE at.AccountTransactionId = 1
SELECT a.AccountNumber,
a.Balance
FROM dbo.Account AS a
SELECT at.AccountTransactionId,
at.AccountNumber,
at.Amount
FROM dbo.AccountTransaction AS at
DELETE a
FROM dbo.AccountTransaction AS a
WHERE a.AccountTransactionId = 2
SELECT a.AccountNumber,
a.Balance
FROM dbo.Account AS a
SELECT at.AccountTransactionId,
at.AccountNumber,
at.Amount
FROM dbo.AccountTransaction AS at

Creating a trigger to store an audit trail

I have to create a trigger on the Claims table that is triggered whenever a new record is inserted into the Claims table. This trigger should store the customer name and the total amount_of_claim by that customer.
Claim_audits (audit table) have already been created.
Schema:
Claims
id int(11)
status_id int(11)
customer_policy_id int(11)
date_of_claim date
amount_of_claim float
> one or many to one(and only one) towards Customer_policy
Customer_policy
id int(11)
policy_start_date date
policy_renewal_date date
policy_id int(11)
customer_id int(11)
agent_id(11)
> one or many to one (and only one) towards Customer
Customer
id int(11)
first_name varchar(30)
last_name varchar(30)
email varchar(30)
address_id int(11)
Output should look like this:
customer_name amount_of_claim
abhinav 195000
This is what I have tried:
CREATE TRIGGER claim_audits on claims
for insert
as
declare #custname varchar(25);
declare #amount varchar(25);
declare #action varchar(25);
select #custname = first_name from customer c
join inserted i on i.id=c.id;
select #amount = i.amount_of_claim from inserted i;
select #action = 'Updated customer claimed amount';
insert into claim_audits values(#custname , #amount , #action);
select * from claim_audits;
go
The Inserted pseudo-table can have 0-N rows, and you need to handle that. And as with anything SQL related you should approach it using a set-based approach - not a procedural approach.
You also don't appear to have been obtaining the customer id correctly - at least based on your table definitions. I must say, its very odd to be storing the first name of the customer in your audit table. Why not store the customer id? The name is not unique, so you haven't provided a reliable audit trail.
create trigger claim_audits
on claims
for insert
as
begin
set nocount on;
insert into dbo.claim_audits (custname, amount, [action])
select C.first_name, I.amount_of_claim, 'Updated customer claimed amount'
from Inserted I
inner join Customer_Policy CP on CP.id = I.customer_policy_id
inner join Customer C on C.id = CP.customer_id;
end;
Note - you do not want to be attempting to return data from a trigger.
And as pointed out by #Squirral: amount_of_claim float: float is an approximate value and should never be used for money. Use decimal or numeric instead.

SQL Server: automatically add a unique identifier to all rows inserted at one time

The below SQL Server code successfully calculates and inserts the monthly pay for all employees along with their staffID number and inserts it into Tablepayroll.
INSERT INTO Tablepayroll (StaffID,Totalpaid)
(SELECT Tabletimelog.StaffID , Tabletimelog.hoursworked * Tablestaff.hourlypay
FROM Tabletimelog
JOIN Tablestaff ON
Tabletimelog.StaffID = Tablestaff.StaffID)
However, I want to be able to also insert a batchIDso that you can identify each time the above insert has been run and the records inserted by it at that time. Meaning that all staff payroll calculated at the same time would have the same batchID number. Each subsequent batchID should just increase by 1.
Please see image below for visual explanation .
I think that Select MAX(batch_id) + 1 would work , but I don't know how to include it in the insert statement.
You can use subquery to find latest batch_id from your current table using this query:
INSERT INTO TablePayroll (StaffID, TotalPaid, batch_id)
SELECT T1.StaffID
, T1.HoursWorked * T2.HourlyPay
, ISNULL((SELECT MAX(batch_id) FROM TablePayRoll), 0) + 1 AS batch_id
FROM TableTimeLog AS T1
INNER JOIN TableStaff AS T2
ON T1.StaffID = T2.StaffID;
As you can see, I just add 1 to current MAX(batch_id) and that's it.
By the way, learn to use aliases. It will make your life easier
Yet another solution would be having your batch_id as a GUID, so you wouldn't have to create sequences or get MAX(batch_id) from current table.
DECLARE #batch_id UNIQUEIDENTIFIER = NEWID();
INSERT INTO TablePayroll (StaffID, TotalPaid, batch_id)
SELECT T1.StaffID, T1.HoursWorked * T2.HourlyPay, #batch_id
FROM TableTimeLog AS T1
INNER JOIN TableStaff AS T2
ON T1.StaffID = T2.StaffID;
Updated
First of all obtain the maximum value in a large table (based on the name of the table it must be big) can be very expensive. Especially if there is no index on the column batch_id
Secondly, pay attantion your solution SELECT MAX(batch_id) + 1 may behave incorrectly when you will have competitive inserts. Solution from #EvaldasBuinauskas without opening transaction and right isolation level can also lead to same batch_id if you run the two inserts at the same time in parallel.
If your SQL Server ver 2012 or higer you can try SEQUENCE. This at least ensures that no duplicates batch_id
Creating SEQUENCE:
CREATE SEQUENCE dbo.BatchID
START WITH 1
INCREMENT BY 1 ;
-- DROP SEQUENCE dbo.BatchID
GO
And using it:
DECLARE #BatchID INT
SET #BatchID = NEXT VALUE FOR dbo.BatchID;
INSERT INTO Tablepayroll (StaffID,Totalpaid, batch_id)
(SELECT Tabletimelog.StaffID , Tabletimelog.hoursworked * Tablestaff.hourlypay, #BatchID
FROM Tabletimelog
JOIN Tablestaff ON Tabletimelog.StaffID = Tablestaff.StaffID)
An alternative SEQUENCE may be additional table:
CREATE TABLE dbo.Batch (
ID INT NOT NULL IDENTITY
CONSTRAINT PK_Batch PRIMARY KEY CLUSTERED
,DT DATETIME
CONSTRAINT DF_Batch_DT DEFAULT GETDATE()
);
This solution works even on older version of the server.
DECLARE #BatchID INT
INSERT INTO dbo.Batch (DT)
VALUES (GETDATE());
SET #BatchID = SCOPE_IDENTITY();
INSERT INTO Tablepayroll (StaffID,Totalpaid, batch_id)
(SELECT Tabletimelog.StaffID , Tabletimelog.hoursworked * Tablestaff.hourlypay, #BatchID
FROM Tabletimelog ...
And yes, all of these solutions do not guarantee the absence of holes in the numbering. This can happen during a transaction rollback (deadlock for ex.)

inserting into A errors because of a foreign key contraint issue

Can someone help explain this to me and resolve it?
http://sqlfiddle.com/#!6/2adc7/9
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_tblMobileForms_tblForms". The conflict occurred in database "db_6_2adc7", table "dbo.tblForms", column 'fm_id'.: insert into tblMobileForms(fm_name) values ('lol')
My schema has the ID from tblMobileForms be a foreign key to tblForms.fm_id
To do what you are trying to do you cannot set up the FK on tblMobileForms as an identity. See my fiddle below for more information.
http://sqlfiddle.com/#!6/be6f7/2
Alternatively what you could do is to have tblMobileForms have it's own separate surrogate key and have a different FK column to the tblForms table.
The PK on the tblMobileForms table has the same name as the FK on the same table. Seeing the PK is an IDENTITY column, you can end up with non-matching values.
In my fiddle, the tblForms table contained IDs in the upper 60s. Running the INSERT in the child table would add a record with id 1, which does not exist in the parent table.
I'd create a new row in the tblMobileForms table, and reference that to the parent table.
You could use an INSTEAD OF trigger to apply a random ID to each mobile form as it is inserted:
CREATE TRIGGER dbo.tblMobileForms_Insert
ON dbo.tblMobileForms
INSTEAD OF INSERT
AS
BEGIN
DECLARE #Inserted TABLE (fm_ID INT, fm_html_file VARBINARY(MAX), fm_name NVARCHAR(50));
INSERT #Inserted (fm_ID, fm_html_File, fm_Name)
SELECT fm_ID, fm_html_File, fm_Name
FROM inserted;
IF EXISTS (SELECT 1 FROM #Inserted WHERE fm_ID IS NULL)
BEGIN
WITH NewRows AS
( SELECT fm_ID, fm_html_File, fm_Name, RowNumber = ROW_NUMBER() OVER (ORDER BY fm_name)
FROM #Inserted
WHERE fm_ID IS NULL
), AvailableIDs AS
( SELECT fm_ID, RowNumber = ROW_NUMBER() OVER (ORDER BY fm_ID)
FROM tblForms f
WHERE NOT EXISTS
( SELECT 1
FROM tblMobileForms m
WHERE f.Fm_ID = m.fm_ID
)
AND NOT EXISTS
( SELECT 1
FROM inserted i
WHERE f.fm_ID = i.fm_ID
)
)
UPDATE NewRows
SET fm_ID = a.fm_ID
FROM NewRows n
INNER JOIN AvailableIDs a
ON a.RowNumber = n.RowNumber
IF EXISTS (SELECT 1 FROM #Inserted WHERE fm_ID IS NULL)
BEGIN
RAISERROR ('Not enough free Form IDs to allocate an ID to the inserted rows', 16, 1);
RETURN;
END
END
INSERT dbo.tblMobileForms (fm_ID, fm_html_File, fm_Name)
SELECT fm_ID, fm_html_file, fm_name
FROM #Inserted
END
When each row is inserted the trigger will check for the next available ID in tblForms and apply it sequentially to the inserted rows where fm_id is not specified. If there are no free ID's in tblForms then the trigger will throw an error so a 1 to 1 relationship is maintained (The error would be thrown anyway since tblMobileForms.fm_id is also a PK).
N.b. this requires tblForms.fm_ID to just be an int column, and not identity.

Create trigger prevent insert

I'm trying to execute the following trigger:
create trigger t23
on studies
after insert, update, delete
as
begin
REFERENCING NEW ROW NewStudent
FOR EACH ROW
WHEN (30 <= (SELECT SUM(credits) FROM Studies)
DELETE FROM NewStudent N
WHERE N.spnr = NewStudent.spnr
end
I'm trying to create a trigger which only inserts a student if the credits is < or == to '30'. The "Credits" is a type int.
I'm getting numerous errors trying to implement this trigger. I really have tried everything and i m out of options. Could someone who is expert in the field point me in the right direction?
The example "Using a DML AFTER trigger to enforce a business rule between the PurchaseOrderHeader and Vendor tables" in the CREATE TRIGGER MSDN documentation does exaclty what you're looking for:
USE AdventureWorks2008R2;
GO
IF OBJECT_ID ('Purchasing.LowCredit','TR') IS NOT NULL
DROP TRIGGER Purchasing.LowCredit;
GO
-- This trigger prevents a row from being inserted in the Purchasing.PurchaseOrderHeader table
-- when the credit rating of the specified vendor is set to 5 (below average).
CREATE TRIGGER Purchasing.LowCredit ON Purchasing.PurchaseOrderHeader
AFTER INSERT
AS
DECLARE #creditrating tinyint, #vendorid int;
IF EXISTS (SELECT *
FROM Purchasing.PurchaseOrderHeader p
JOIN inserted AS i
ON p.PurchaseOrderID = i.PurchaseOrderID
JOIN Purchasing.Vendor AS v
ON v.BusinessEntityID = p.VendorID
WHERE v.CreditRating = 5
)
BEGIN
RAISERROR ('This vendor''s credit rating is too low to accept new purchase orders.', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END;
The key here is ROLLBACK TRANSACTION, just adapt the example to suit your need and you're done.
Edit: This should accomplish what you're looking for, but I have not tested it so your mileage may vary.
create trigger dbo.something after insert as
begin
if exists ( select * from inserted where sum(credits) > 30 )
begin
rollback transaction
raiserror ('some message', 16, 1)
end
end
Another edit, based on some assumptions (please note I wrote this script on the fly since I can't test it right now):
create table dbo.students
(
student_id int not null,
name varchar (50) not null
)
create table dbo.courses
(
course_id int not null,
name varchar (50) not null,
required_credits int not null
)
create table dbo.results
(
student_id int not null,
course_id int not null,
course_result int not null
)
create trigger dbo.check_student_results on dbo.results after insert as
(
declare #check int
select #check = count(*)
from inserted as a
join dbo.courses as b on b.course_id = a.course_id
where b.required_credits > a.course.result
if #check <> 0
begin
rollback transaction
raiserror('The student did not pass the course.', 16, 1)
end
)
This way when you insert records in the dbo.results table the constraint checks if the student has passed the course, and cancels the insertion if appropriate. However, it's better to check this things in the application layer.