Creating a status update trigger - sql

I have 2 tables like so:
JOBS table
Jobcode UserId Status
101 130 R
102 139 D
USERS table
UserId Email
130 test#example.com
I want to create a trigger on insert and update that sends an email to my stored procedure:
EXEC dbo.SendMyEmail #email, #jobcode;
when the jobcode is inserted as 'D' or updated to 'D'.

In my opinion, sending email in a trigger is not optimal.
Instead, you should just insert to a queue table, and have a process run frequently that checks the table and sends the email.
What happens if you get an error in your email procedure? It will force a rollback of your job completion status. Only you know whether that is minor or possibly catastrophic. But I can tell you for sure that DB best practice is to NOT do extended I/O during a DML operation.
CREATE TRIGGER TR_Jobs_EnqueueEmail_IU ON dbo.Jobs FOR INSERT, UPDATE
AS
SET NOCOUNT ON;
INSERT dbo.EmailQueue (UserID, JobCode)
SELECT UserID, JobCode
FROM
Inserted I
LEFT JOIN Deleted D
ON I.JobCode = D.JobCode -- or proper PK columns
WHERE
IsNull(D.Status, 'R') <> 'D'
AND I.Status = 'D';
Tables needed:
CREATE TABLE dbo.EmailQueue (
QueuedDate datetime NOT NULL
CONSTRAINT DF_EmailQueue_QeueueDate DEFAULT (GetDate()),
UserID int NOT NULL,
JobCode int NOT NULL,
CONSTRAINT PK_EmailQueue PRIMARY KEY CLUSTERED (QueuedDate, UserID, JobCode)
);
CREATE TABLE dbo.EmailSent (
SentDate datetime NOT NULL
CONSTRAINT DF_EmailSent_SentDate DEFAULT (GetDate()),
QueuedDate datetime NOT NULL,
UserID int NOT NULL,
JobCode int NOT NULL,
CONSTRAINT PK_EmailSent PRIMARY KEY CLUSTERED (SentDate, QueuedDate, UserID, JobCode)
);
Then, run the following stored procedure once a minute from a SQL Job:
CREATE PROCEDURE dbo.EmailProcess
AS
DECLARE #Email TABLE (
QueuedDate datetime,
UserID int,
JobCode int
);
DECLARE
#EmailAddress nvarchar(255),
#JobCode int;
WHILE 1 = 1 BEGIN
DELETE TOP 1 Q.*
OUTPUT Inserted.QueuedDate, Inserted.UserID, Inserted.JobCode
INTO #Email (QueuedDate, UserID, JobCode)
FROM dbo.EmailQueue Q WITH (UPDLOCK, ROWLOCK, READPAST)
ORDER BY QueuedDate;
IF ##RowCount = 0 RETURN;
SELECT #EmailAddress = U.EmailAddress, #JobCode = E.JobCode
FROM
#Email E
INNER JOIN dbo.User U
ON E.UserID = U.UserID;
EXEC dbo.SendMyEmail #EmailAddress, #JobCode;
DELETE E
OUTPUT QueuedDate, UserID, JobCode
INTO dbo.EmailSent (QueuedDate, UserID, JobCode)
FROM #Email E;
END;
The delete pattern and locks I used are very specifically chosen. If you change them or change the delete pattern in any way it is almost certain you will break it. Handling locks and concurrency is hard. Don't change it.
Note: I typed all the above without checking anything on a SQL Server. It is likely there are typos. Please forgive any.

I'm not sure about data types etc but this should at least put you on the right track.
Hope it helps...
CREATE TRIGGER SendEmailOnStatusD
ON JOBS
-- trigger is fired when an update is made for the table
FOR UPDATE --You can add the same for INSERT
AS
-- holds the UserID so we know which Customer was updated
DECLARE #UserID int
DECLARE #JobCode int
SELECT #UserID = UserId, #JobCode = JobCode
FROM INSERTED WHERE [Status] = 'D' --If you want the old value before the update, use 'deleted' table instead of 'inserted' table
IF (#UserID IS NOT NULL)
BEGIN
-- holds the email
DECLARE #email varchar(250)
SELECT #email = Email FROM USERS WHERE UserId = #UserID
EXEC SendMyEmail (#email, #jobcode);
END
GO
EDIT:
Above code does not handle multiple updates, so for better practice see below option
CREATE TRIGGER SendEmailOnStatusD ON JOBS
-- trigger is fired when an update is made for the table
FOR UPDATE --You can add the same for INSERT
AS
DECLARE #Updates table(UserID int, JobCode int, Email varchar(250))
INSERT INTO #Updates (UserID, JobCode, Email)
SELECT i.UserID, i.JobCode, u.Email
FROM INSERTED i
JOIN USERS u ON i.UserID = u.UserID
WHERE [Status] = 'D'
DECLARE #UserID int
DECLARE #JobCode int
DECLARE #Email varchar(250)
WHILE EXISTS(SELECT * FROM #Updates)
BEGIN
SELECT TOP 1
#UserID = UserID,
#Email = Email,
#JobCode = JobCode
FROM #Updates WHERE UserID = #UserID
EXEC SendMyEmail (#email, #jobcode);
DELETE FROM #Updates
WHERE UserID = #UserID
END
GO
Additionally, as discussed in the comments, sending emails from a trigger is also not the best, but as this is what the question asks for it has been included. I would recommend alternative options for sending emails such as a queue which has been mentioned in other answers.

Related

How to insert or update inlog time in new table when user logged in

I am trying to write a query which if the user is valid it stores the time in another table. I have the first part, to see if the log in credentials are valid. But the second part I don't know: get the id of the logged in email, then use this id to write it to the second table and insert or update the time in this table.
This is my first part of the query/ stored procedure
Create Procedure spAuthenticateUser
#Email nvarchar(100),
#Wachtwoord nvarchar(100)
as
Begin
Declare #Count int
Select #Count = COUNT(Email) from Signin
where [Email] = #Email and [Wachtwoord] = #Wachtwoord
if(#Count = 1)
Begin
Select 1 as ReturnCode
End
Else
Begin
Select -1 as ReturnCode
End
End
This does already work. I tried to add
if(#Count = 1)
Begin
Select 1 as ReturnCode
INSERT INTO Logintijd (Id, Tijd)
Select Id from Signin where [Email] = #Email
End
These are the table I tried to use
Create table Signin
(
Id int identity primary key not null,
Naam nvarchar(50) not null,
Email nvarchar(50) unique not null,
Wachtwoord nvarchar(50) not null
)
and
Create table Logintijd
(
Id int not null primary key,
Tijd smalldatetime not null
)
Tried to do it with the updated class
IF EXISTS (Select Tijd from Logintijd Inner join Signin ON Logintijd.Id = Signin.Id)
update Logintijd set Tijd = GETDATE() From Logintijd Inner join Signin ON Logintijd.Id = Signin.Id
ELSE
INSERT INTO Logintijd (Id, Tijd)
Select Id, GETDATE() from Signin where [Email] = #Email
This does do update and insert but it does it for all the item in the class
I know I have to do something with smalldatetime and the Getdate() function
Hope this is clear enough; if not please let me know.
Sounds like you just need this:
if(#Count = 1)
Begin
Select 1 as ReturnCode
INSERT INTO Logintijd (Id, Tijd)
Select Id, GETDATE() from Signin where [Email] = #Email
End

How to declare in stored procedure that a row does not exist?

I want to know if a certain QuestionID and EmployeeID exist; if they do, they need to be inserted like here below (that's working fine).
But if they don't exist, I want a good error for the user, so that he knows that the QuestionID or the EmployeeID or both do not exist. Also maybe a rollback of the transaction? Now I can add every number, but the stored procedure still completed the commands...
I have this code (I'm using SQL Server):
CREATE PROCEDURE Contentment
#employeeID INT,
#questionid INT
AS
BEGIN
IF EXISTS (SELECT questionid
FROM question
WHERE questionid = #questionid)
IF EXISTS (SELECT employeeID
FROM employee
WHERE employeeid = #employeeID)
BEGIN
INSERT INTO contentment (employeeid, questionid, date, score, comment)
VALUES (#employeeID, #questionid, null, null, null)
END
ELSE
IF #employeeID = 0
RAISERROR ('-certain error-', 16, 1, #employeeid)
IF #questionid = 0
RAISERROR ('-certain error-', 16, 1, #questionid)
END
I'd try to do it like this - check for the existence of both parameters in a single statement - if not, then provide an error:
CREATE PROCEDURE dbo.Contentment
#employeeID INT,
#questionid INT
AS
BEGIN
-- check for *both* conditions at once
IF EXISTS (SELECT * FROM dbo.question WHERE questionid = #questionid) AND
EXISTS (SELECT * FROM dbo.employee WHERE employeeid = #employeeID)
BEGIN
-- if OK --> insert
INSERT INTO dbo.contentment (employeeid, questionid, date, score, comment)
VALUES (#employeeID, #questionid, null, null, null)
END
ELSE
BEGIN
-- if NOT OK --> provide error
RAISERROR ('#EmployeeID or #QuestionID do not exist', 16, 1, null)
END
END
Also maybe a rollback of the transaction?
Since you're not really doing anything (no INSERT happening in either of the two ID's doesn't exist), there's really no transaction to roll back at this point....
you can use an error message parameter like that :
CREATE PROC dbo.Usp_Contentment
AS
begin
declare #ErrMsg NVARCHAR(MAX) = ''
IF not exists (select 1 from dbo.Question where QuestionId= #QuestionId)
set #ErrMsg = 'Some msg'
if not exists (select 1 from dbo.Employee where Employee Id= #EmployeeId)
set #ErrMsg =#ErrMsg + 'some msg'
IF #msg=''
INSERT INTO dbo.contentment (employeeid, questionid, date, score, comment)
VALUES (#employeeID, #questionid, null, null, null)
else
RAISERROR(#ErrMsg,16,1)
end
don't use ROLLBACK because you handled the NULL Values
I would let the database do the work. Declare the table to have proper foreign key relationships:
alter table contentment add constraint fk_conententment_question
foreign key (questionid) references question(questionid);
alter table contentment add constraint fk_conententment_employee
foreign key (employeeid) references employee(employeeid);
Then write the procedure as:
CREATE PROCEDURE dbo.usp_Contentment (
#employeeID INT,
#questionid INT
) AS
BEGIN
INSERT INTO dbo.contentment (employeeid, questionid, date, score, comment)
VALUES (#employeeID, #questionid, null, null, null)
END; -- usp_Contentment
The database will generate an error that the foreign key relationship fails -- if there is no matching key.
This approach is much better than doing the test yourself for several reasons:
The database ensures the data integrity, so you know the relationships are maintained regardless of where the data changes occur.
The integrity applies to both inserts and updates. There is no need to put checks in multiple places.
The integrity check is thread-safe, so if rows are being modified in the reference tables, the integrity check works.
If you really want a customized message, you can catch the error in a try/catch block and have a customized error.

SQL Update Trigger that inserts old values into audit table and only fires if two specific columns are being updated, otherwise returns error

I need to record all previous addresses and postcodes when they are updated in the Customer table.
Business requirement: Addresses cannot be changed without also updating the postcode and vice versa.
A mechanism to prevent this is required along with appropriate error messages.
I already created the table:
create table tblCustomerAudit
(
CustomerID int identity(1,1) not null,
CustomerName nvarchar(255) null,
CustomerAddress nvarchar(255) null,
CustomerPostcode nvarchar(255) null,
CardNumber nvarchar(255) null,
)
go
alter table tblCustomerAudit
add constraint FK_CustomerAudit
foreign key(CustomerID)
references CstmrEng.tblCustomer(CustomerID)
What would trigger look like? please help!
Perhaps you can do this with a stored procedure? Just be aware that stored procedures are not magic pixie dust, and maintaining them can be a nightmare.
You could have your trigger call a stored procedure that handles the particular constraint between CustomerAddress and CustomerPostcode.
This code is untested and may very well not work.
CREATE PROCEDURE UpdateCustomerAddressAndPostcode #CustomerID INT, #CustomerAddress NVARCHAR(255), #CustomerPostcode NVARCHAR(255)
AS BEGIN
IF (#CustomerAddress IS NULL OR #CustomerAddress = '')
BEGIN
PRINT 'Customer address must be present to modify Customer table.';
THROW;
END
IF (#CustomerPostcode IS NULL OR #CustomerPostcode = '')
BEGIN
PRINT 'Customer postcode must be present to modify Customer table.';
THROW;
END
INSERT INTO tblCustomerAudit (CustomerID, CustomerName, CustomerAddress, CustomerPostcode, CardNumber)
SELECT CustomerID, CustomerName, CustomerAddress, CustomerPostcode, CardNumber FROM Customer where CustomerID = #CustomerID;
-- just printing the table for example
SELECT * FROM tblCustomerAudit
-- make the change to Customer here or in the trigger
END
GO
CREATE TRIGGER CustomerTrigger
ON [dbo].[Customer]
INSTEAD OF INSERT
AS
DECLARE #CustomerID int
DECLARE #CustomerAddress nvarchar(255)
DECLARE #CustomerPostcode nvarchar(255)
BEGIN
SET NOCOUNT ON;
-- verify the data
EXEC UpdateCustomerAddressAndPostcode #CustomerID, #CustomerAddress, #CustomerPostcode
-- do the Customer change here or in the stored procedure
END
I really recommend a store procedure to control the update, insert, and delete, so you'll use the store procedure to pass the values before it goes to the table. If you use a trigger, then the values will be actually changed, then the trigger will be fired, and from the trigger you'll have to re-update the table with the old values if your conditions met. So, this is a kind of redundancy for me, which is why I recommended a store procedure to handle everything before change the table values.
anyhow, you can still use triggers with the advantage of deleted and inserted tables :
CREATE TRIGGER CustomerUpdate ON tblCustomerAudit
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#CustomerID INT
, #New_CustomerAddress nvarchar(255)
, #New_CustomerPostcode nvarchar(255)
, #Old_CustomerAddress nvarchar(255)
, #Old_CustomerPostcode nvarchar(255)
SELECT
#CustomerID = CustomerID
, #Old_CustomerAddress = CustomerAddress
, #Old_CustomerPostcode = CustomerPostcode
FROM
deleted
SELECT
#New_CustomerAddress = CustomerAddress
, #New_CustomerPostcode = CustomerPostcode
FROM
tblCustomerAudit
WHERE
CustomerID = #CustomerID
IF #Old_CustomerAddress = #New_CustomerAddress OR #New_CustomerPostcode = #Old_CustomerPostcode
BEGIN
-- IF one of them matches return the old values
UPDATE tblCustomerAudit
SET
CustomerAddress = #Old_CustomerAddress
, CustomerPostcode = #Old_CustomerPostcode
WHERE
CustomerID = #CustomerID
-- display an error message
RAISERROR( 'You need to change both address and postcode to save the new values', 18 , 0);
END
END
Something like this:
CREATE TRIGGER foo.bar ON foo.mytable
AFTER UPDATE
AS
IF (##ROWCOUNT_BIG = 0)
RETURN;
IF EXISTS (SELECT *
FROM foo.mytable AS t
JOIN inserted AS i
ON t.mykey = i.mykey
WHERE i.addr <> t.addr AND i.post = t.post -- address changes but post code doesn't
OR i.post <> t.post AND i.addr = t.addr -- post code changes by address doesn't
)
BEGIN
RAISERROR ('invalid changes', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END;
GO

Add extra column in insert with instead of trigger

EDIT 1:Basically I want to do
INSERT INTO Users(col1, col2, col3) when Users only have col1 and col2 , col3 doesn't exist.
I have a table UsersAddresses with two int fields "User" and "Addresses" which that binds my table Users to another table Addresses. I need to audit any change made to my user so I have a trigger on my Users that works fine, any changes made to my Users table creates a line in the audit table.
The problem is that I also need to trigger the same trigger if I add an addresses to my user. So I create a trigger on my UsersAddresses that will do an update of the Users table. But I need to have the modifierId aswell (to know who wanted to make this change) So I figure out I should alter the way I insert in UsersAddresses to add this Id. I've tried the following, adding a fake "ModifierId" but as expected I get "invalid column name" when I try to add this trigger.
Do you know what I should do ?
Thankfully
CREATE TRIGGER UpdateUser
ON [dbo].[UsersAddresses]
INSTEAD OF INSERT
AS
DECLARE #User INT;
DECLARE #Address INT;
DECLARE #ModifierId INT;
SELECT #User = [User] FROM inserted;
SELECT #Address = [Address] FROM inserted;
SELECT #ModifierId = [ModifierId] FROM inserted;
INSERT INTO [UsersAddresses]([User], [Address])
VALUES (#User, #Address);
UPDATE [Users]
SET [CreatorId] = #ModifierId
WHERE [Id] = #User;
Edit Solution: Credits to Michael
use context_info with a procedure.:
CREATE TABLE [dbo].[UsersAddresses] (
[User] INT NOT NULL,
[Address] INT NOT NULL,
CONSTRAINT [PK_UsersAddresses] PRIMARY KEY CLUSTERED ([User] ASC, [Address] ASC),
CONSTRAINT [FK_ToUser] FOREIGN KEY ([User]) REFERENCES [dbo].[Users] ([Id]),
CONSTRAINT [FK_ToAddress] FOREIGN KEY ([Address]) REFERENCES [dbo].[Addresses] ([Id])
);
GO
CREATE TRIGGER [ChangeToUsersAddresses]
ON [dbo].[UsersAddresses]
FOR DELETE, INSERT, UPDATE
AS
BEGIN
IF ( (SELECT COUNT(*) FROM inserted) > 1)
THROW 51000, 'To many records', 1
DECLARE #UserId INT;
IF (SELECT [User] FROM inserted) IS NOT NULL
SELECT #UserId = [User] FROM inserted
ELSE
SELECT #UserId = [User] FROM deleted
INSERT INTO [Audit_Users](UserId,ActiveDirectory,FirstName,LastName,Type,Status,ModifierId,Date)
SELECT [Id], [ActiveDirectory], [FirstName], [LastName], [Type], [Status], [dbo].GetUserContext(), GETDATE()
FROM [dbo].[Users] WHERE [Id] = #UserId
INSERT INTO [Audit_UsersAddresses]([User], [Address])
SELECT ##IDENTITY, [Address]
FROM [UsersAddresses]
WHERE [User] = #UserId;
END;
GetUserContext() that is used in the insert :
CREATE FUNCTION [dbo].[GetUserContext] ()
RETURNS INT
AS
BEGIN
RETURN COALESCE(CONVERT(INT, CONTEXT_INFO()), 0)
END
And Finally the procedure to set the context.
CREATE PROCEDURE [dbo].[SetUserContext]
#userId INT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #context VARBINARY(128)
SET #context = CONVERT(BINARY(128), #userId)
SET CONTEXT_INFO #context
END

How would I do this with SQL Server?

I have two tables users and address
users table schema
user_id
name
address_id
sent
address table schema
address_id
create_date
location
user_id
I have this query that returns 16 rows
select * from users where sent = 1;
but all the address_id are all NULL because they have not been created yet
So what I need to do is create 16 rows in the address table one for each of the users with the user_id of the user and then set the address_id in the users table of that address
For now I can leave the location field blank. All I need to do is set the create_date to CURRENT_DATE
Is there a way to do this in one query?
Try this:
declare #user table(user_id [int] IDENTITY(1,1), name varchar(25), address_id int, sent int)
declare #address table(address_id [int] IDENTITY(1,1) NOT NULL, create_date datetime default getdate(), location varchar(100), user_id int)
declare #t table(user_id int, address_id int)
insert #user (name) values('you')
insert #user (name) values('someone else')
begin Transaction
insert #address (user_id)
output inserted.user_id, inserted.address_id
into #t
select user_id from #user u
where not exists (select 1 from #address where u.user_id = user_id)
update u
set u.address_id = t.address_id
from #user u
join #t t
on u.user_id = t.user_id
commit transaction
select * from #user
select * from #address
Not exactly sure what your mean but this should at least point you in the right direction
Begin Try
Begin Transaction
Update Users
Set Users.address = Address.address, create_date = GetDate()
From Addresses
Inner Join Users On Addresses.userid = Users.userid
Commit Transaction
End Try
Begin Catch
Rollback Transaction
End Catch
It should be something like this. There are a couple of ways of doing the problem so have fun with it and hopefully this helped. For testing it write two Select * From Users statements one before and one after. Also change Commit Transaction to Rollback Transaction so you don't have to worry about making a mistake.
Just reread question yea you can't do that in one shot just replace the Update statement with
Insert Into Addresses (address_id, create_date, location, user_id)
Values ('#ddr355_1d', GetDate(), '1234theLocation', 123478)
and you will have to do that for each one but should be easy with only 16 entries in the User table. You might want to look into writing a Stored Procedure if you plan on adding more to the table. Something kind of like this
Create Procedure [dbo].[AddressesInsertData]
(
#Address Int,
#Location varchar(100),
#UserId Int
)
As
Begin Try
Begin Transaction
Insert Into Addresses (address_id, create_date, location, user_id)
Values (#Address, GetDate(), #Location, #UserId)
Commit Transaction
End Try
Begin Catch
Rollback Transaction
End Catch
Basic structure of a stored procedure. I would add an if not exists in there that would update instead of insert but this should be plenty to get you started. Hopefully these examples should clear up somethings for you and help you out.
There is a design rules of thumb that a table models EITHER an entity/class OR the relationship between entities/classes but not both.
Therefore, I suggest you remove the address_id column from the users table, remove user_id from the address table and create a third table comprising both user_id and address_id to model the relationship between users and their addresses. This will also rid you of the need to have nullable columns.