Add extra column in insert with instead of trigger - sql

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

Related

INSERT statement conflicting with FOREIGN KEY constraint, but there's no conflict apparent

I'm not sure why I'm getting this error, because no data is being inserted into the column mentioned in the error. It's a PK field set to IDENTITY, so the value is auto-filled with each record added. The function is supposed to insert a record into an audit table, called by a trigger on the source table. Here's the error and my code.
Msg 547, Level 16, State 0, Procedure tblTriggerAuditRecord_TTeamPlayers, Line 33 [Batch Start Line 344]
The INSERT statement conflicted with the FOREIGN KEY constraint "Z_TTeamPlayers_TTeams_FK". The conflict occurred in database "dbSQL1", table "dbo.Z_TTeams", column 'intTeamAuditID'.
--Problematic Code
DELETE FROM TTeamPlayers
DELETE FROM TTeams
WHERE strTeam = 'Reds'
SELECT * FROM TTeams
SELECT * FROM Z_TTeams
=========================
-- both these tables are updated when a DELETE is run. I have the FK constraint set to CASCADE so that when it's deleted out of the child table Z_TTeamPlayers, the parent record is deleted.
CREATE TABLE Z_TTeamPlayers
(
intTeamPlayerAuditID INTEGER IDENTITY NOT NULL
,intTeamAuditID INTEGER NOT NULL
,intPlayerAuditID INTEGER NOT NULL
,UpdatedBy VARCHAR(50) NOT NULL
,UpdatedOn DATETIME NOT NULL
,strAction VARCHAR(10) NOT NULL
,strModified_Reason VARCHAR(1000)
--,CONSTRAINT PlayerTeam_UQ UNIQUE ( intTeamID, intPlayerID )
,CONSTRAINT Z_TTeamPlayers_PK PRIMARY KEY ( intTeamPlayerAuditID )
)
CREATE TABLE Z_TTeams
(
intTeamAuditID INTEGER IDENTITY NOT NULL
,intTeamID INTEGER NOT NULL
,strTeam VARCHAR(50) NOT NULL
,strMascot VARCHAR(50) NOT NULL
,UpdatedBy VARCHAR(50) NOT NULL
,UpdatedOn DATETIME NOT NULL
,strAction VARCHAR(10) NOT NULL
,strModified_Reason VARCHAR(1000)
,CONSTRAINT Z_TTeams_PK PRIMARY KEY ( intTeamAuditID )
)
==============================
ALTER TABLE Z_TTeamPlayers ADD CONSTRAINT Z_TTeamPlayers_TTeams_FK
FOREIGN KEY ( intTeamAuditID ) REFERENCES Z_TTeams ( intTeamAuditID ) ON DELETE CASCADE
==============================
-- --------------------------------------------------------------------------------
-- Create Trigger for Z_TTeamPlayers
-- --------------------------------------------------------------------------------
GO
CREATE TRIGGER tblTriggerAuditRecord_TTeamPlayers on TTeamPlayers
AFTER UPDATE, INSERT, DELETE
AS
DECLARE #Now DATETIME
DECLARE #Modified_Reason VARCHAR(1000)
DECLARE #Action VARCHAR(10)
SET #Action = ''
-- Defining if it's an UPDATE, INSERT, or DELETE
BEGIN
IF (SELECT COUNT(*) FROM INSERTED) > 0
IF (SELECT COUNT(*) FROM DELETED) > 0
SET #Action = 'UPDATE'
ELSE
SET #Action = 'INSERT'
ELSE
SET #Action = 'DELETE'
END
SET #Now = GETDATE() -- Gets current date/time
IF (#Action = 'INSERT')
BEGIN -- Begin INSERT info
INSERT INTO Z_TTeamPlayers(intTeamAuditID, intPlayerAuditID, UpdatedBy, UpdatedOn, strAction, strModified_Reason)
SELECT I.intTeamID, I.intPlayerID, SUSER_NAME(), GETDATE(), #Action, I.strModified_Reason
FROM INSERTED as I
INNER JOIN TTeamPlayers as T ON T.intTeamPlayerID = I.intTeamPlayerID
END -- End Insert Info
ELSE
IF (#Action = 'DELETE')
BEGIN -- Begin INSERT info
INSERT INTO Z_TTeamPlayers(intTeamAuditID, intPlayerAuditID, UpdatedBy, UpdatedOn, strAction, strModified_Reason)
SELECT D.intTeamID, D.intPlayerID, SUSER_SNAME(), GETDATE(), #Action, ''
FROM DELETED as D
END -- End Delete Info
ELSE -- #Action = 'UPDATE'
BEGIN --begin UPDATE info get modified reason
IF EXISTS (SELECT TOP 1 I.strModified_Reason FROM INSERTED as I, TPlayers as T WHERE I.intPlayerID = T.intPlayerID
AND I.strModified_Reason <> '')
BEGIN -- beging insert of UPDATE info
INSERT INTO Z_TTeamPlayers(intTeamAuditID, intPlayerAuditID, UpdatedBy, UpdatedOn, strAction, strModified_Reason)
SELECT I.intTeamID, I.intPlayerID, SUSER_SNAME(), GETDATE(), #Action, I.strModified_Reason
FROM TTeamPlayers as T
INNER JOIN INSERTED as I ON T.intPlayerID = I.intPlayerID
-- set modified reason column back to empty string
UPDATE TPlayers SET strModified_Reason = NULL
WHERE intPlayerID IN (SELECT TOP 1 intPlayerID FROM INSERTED)
END
ELSE
BEGIN -- begin if no modified reasson supplied
PRINT 'Error and rolled back, please enter modified reason'
ROLLBACK
END
END
z_TTeamPlayers.intTeamAuditID references your audit table's primary key. In your code you are inserting that value into z_TTeamPlayers... INSERT INTO Z_TTeamPlayers(intTeamAuditID... while it doesn't exist (as a primary key) in your audit table yet... thus it fails.
Here is a demo.
I get that you are trying to audit, but i'm not sure your business logic on teams and players. You seem to have your design a bit backwards. You could always use versioning in SQL Server.
As a guess you probably want a design similar to this instead

SQL How to restrict parent row from updates once child added?

I use foreign keys to prevent rows from being deleted in some SQL tables once a child uses the key. However, now I want to be able to restrict any column of the parent table from updates once the child uses the key.
For example if the parent table is
[ID],[First Name],[Last Name]
And the child table is
[ListID],[Emp ID]
The foreign key prevents me from deleting or updating [ID] in the parent table however, it doesn't stop me from modifying [First Name]. Is there a way to set up foreign keys to prevent this?
Thanks.
you could use instead of trigger.
CREATE TABLE [dbo].[ParentTable]
(
ID INT, FirstName VARCHAR(50), LastName VARCHAR(50)
)
go
CREATE table ChildTable(EmpID INT)
go
CREATE TRIGGER [dbo].[ParentTable_InsteadOfUPDATE]
ON [dbo].[ParentTable]
INSTEAD OF UPDATE
AS
BEGIN
DECLARE #ID INT, #FirstName VARCHAR(50), #LastName VARCHAR(50)
SELECT #ID = INSERTED.ID,
#FirstName = INSERTED.FirstName,
#LastName = INSERTED.LastName
FROM INSERTED
IF EXISTS(select top 1 1 from DBO.ChildTable Where EmpID=#ID )
BEGIN
IF UPDATE([ID]) OR UPDATE([FirstName]) OR UPDATE([LastName])
BEGIN
RAISERROR('These fields cannot be updated.', 16 ,1)
ROLLBACK
END
END
ELSE
BEGIN
Update [ParentTable] SET FirstName=#FirstName,LastName=#LastName Where ID=#ID;
END
END
go
Try this:
CREATE TRIGGER trg_name ON parent_table INSTEAD OF UPDATE
AS
BEGIN
UPDATE parent_table
SET [FirstName] = I.[firstname]
,[LastName] = I.[lastname]
FROM parent_table A
INNER JOIN inserted B
ON A.[ID] = B.[ID]
LEFT JOIN child_table CT
ON A.[ID] = CT.[ListID] -- or [EmpID] - am not sure about the relation
WHERE CT.[LsitID] IS NULL -- where the element from parent_table does not have element in the child table
END
Also, always try to handle the logic in triggers in batches in order not to hurt the performance.

'An explicit value for the identity column can only be specified...' during Merge

There are quite a few questions which cover this error, but I'm having the issue that:
A) I'm currently doing a Merge (with an Insert)
B) I'm not explicitly setting the identity column!
My stored procedure (table names and properties obfuscated):
Table Definition
CREATE TABLE MyTable (
[ID] BIGINT IDENTITY(1, 1) NOT NULL,
[Value] NVARCHAR (256) NOT NULL,
[Property] NVARCHAR (256) NOT NULL,
CONSTRAINT [PK_MyTable] PRIMARY KEY CLUSTERED ([ID] ASC),
CONSTRAINT [IX_MyTable] UNIQUE NONCLUSTERED ([Value] ASC)
);
Stored Procedure Definition
CREATE TYPE TempType as Table (
[TempValue] nvarchar(256) NOT NULL,
[TempProperty] nvarchar(256) NOT NULL,
);
GO
CREATE PROCEDURE [Name]
#Source TempType READONLY
AS
BEGIN
MERGE [MyTable] as target
USING (SELECT * FROM #Source) as source (TempValue, TempProperty)
ON (target.Value = source.TempValue)
WHEN MATCH THEN
UPDATE SET Value = source.TempValue, Property = source.TempProperty
WHEN NOT MATCHED THEN
INSERT ([Value], [Property])
VALUES (source.TempValue, source.TempProperty)
OUTPUT deleted.*, $action, inserted.* INTO [MyTable];
END
From what I can see, I'm not anywhere explicitly specifying the IDENTITY column. I am specifying a UNIQUE-ly constrained column though.
Lastly, contrary to what the error says, specifying IDENTITY_INSERT ON does nothing.
Edit: I should also specify, I'm getting this error while deploying from a .dacpac via C#.
I would simply get rid of the MERGE statement (has lots of issues by design) and use a simple insert statement as follows:
CREATE PROCEDURE [Name]
#Source TempType READONLY
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [MyTable] ([Value], [Property])
SELECT s.TempValue, s.TempProperty
FROM #Source s
WHERE EXISTS (SELECT 1
FROM [MyTable]
WHERE Value = s.TempValue)
END
To read about issues with MERGE statement have a look at this article Use Caution with SQL Server's MERGE Statement
Since you have added an update into your Merge statement, now I would also add an update statement and wrap the update and insert into one transaction but still I would not recommend using merge statement.
CREATE PROCEDURE [Name]
#Source TempType READONLY
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION;
UPDATE T
SET T.[Property] = S.[Property]
FROM [MyTable] T
INNER JOIN #Source s ON T.Value = s.TempValue
INSERT INTO [MyTable] ([Value], [Property])
SELECT s.TempValue, s.TempProperty
FROM #Source s
WHERE NOT EXISTS (SELECT 1
FROM [MyTable]
WHERE Value = s.TempValue)
COMMIT TRANSACTION;
END

Add Column To one table on inserting value to another table

I have two tables Branch_TB and Branch_City.
Branch_TB :
CREATE TABLE Branch_TB(
Branch_Id int NULL,
Branch_Name varchar(50) NULL
)
Whenever there is an entry for Branch_Name, I want to add that entry as column name in Branch_City.
Is there any way for this. I don't know how to do this and haven't try any solution.
Thanks in advance.
You can achieve that using AFTER INSERT TRIGGER.
CREATE TABLE Branch_TB(
Branch_Id int NULL,
Branch_Name varchar(50) NULL
)
go
--drop table BranchCity
create table BranchCity(abc varchar(20))
go
create TRIGGER dbo.AddCol
ON Branch_TB
AFTER INSERT AS
BEGIN
DECLARE #NewVal VARCHAR(20)
DECLARE #AlterSQL VARCHAR(100)
CREATE TABLE #New
(
VAL VARCHAR(20)
)
INSERT INTO #New
select Branch_Name from inserted
select #NewVal = Val from #New
SET #AlterSQL = 'ALTER TABLE BranchCity add ' + #NewVal + ' VARCHAR(20)'
exec(#AlterSQL)
END
go
insert into Branch_Tb
values(1, 'City1')
go
insert into Branch_Tb
values(2, 'City2')
But in my opinion, you should re-evaluate your database design.
you need to create trigger on insert row in Branch_TB table.
In trigger you need to add code for Add column in require table.
How to create Trigger in Sql ? check this.
I understand it like you want to duplicate inserted value in some other table in column name. If this is true then you can try with OUTPUT:
INSERT INTO Branch_TB( Branch_Id, Branch_Name )
OUTPUT 'someValue1', Inserted.Branch_Name, 'someValue2'
INTO Branch_City ( someCol1, Name, comeCol2 )
VALUES ( 1, 'some name' )
you can try this
create proc proc_branch (#b_name varchar(50)
as
begin
exec('alter table Branch_city add column '+ #b_name + ';');
end
go
create trigger tr_branch
on branch_TB
for insert
as
begin
declare #branch_name varchar(50);
set #branch_name=(select branch_name from inserted)
exec proc proc_branch
end

Creating a status update trigger

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.