I am working with an insert trigger and work fine. I am creating an insert trigger and take a backup in tblHist table.
I have two tables:
tblUser - creating this table for insert,update and delete purpose
tblHist - creating this table for store a record for history purpose
tblUser table design:
tblHist table design:
Then I create an insert and update trigger:
ALTER TRIGGER [dbo].[trgr_tblUser_AFTERINSERT]
ON [dbo].[tblUser]
AFTER INSERT, UPDATE
--,DELETE
AS
BEGIN
DECLARE #userid int, #username varchar(50),
#useraddress varchar(50), #countryname varchar(5),
#statename varchar(50), #cityname varchar(50);
BEGIN
SELECT
#userid = u.userid, #username = u.username,
#useraddress = u.useraddress,
#countryname = u.countryname,
#statename = u.statename, #cityname = u.cityname
FROM tblUser u;
INSERT INTO tblHist (userid, username, useraddress, countryname, statename, cityname)
VALUES (#userid, #username, #useraddress, #countryname, #statename, #cityname);
PRINT 'AFTER INSERT update trigger fired.'
END
END
When I insert a record into the tblUser table, then it inserts a record into the tblHist table - this is working fine.
See below
Then I update a record then insert a history in tblHist table working fine.
but issue is when I add a code for delete a record functionality in trgr_tblUser_AFTERINSERT then delete functionality not work
And when I create a delete trigger separately then work fine
See below
ALTER TRIGGER [dbo].[trgr_tblUser_AFTERDELETE]
ON [dbo].[tblUser]
FOR DELETE
AS
BEGIN
DECLARE #userid int, #username varchar(50),
#useraddress varchar(50), #countryname varchar(5),
#statename varchar(50), #cityname varchar(50);
SELECT
#userid = u.userid, #username = u.username,
#useraddress = u.useraddress, #countryname = u.countryname,
#statename = u.statename, #cityname = u.cityname
FROM deleted u;
INSERT INTO tblHist (userid, username, useraddress, countryname, statename, cityname)
VALUES (#userid, #username, #useraddress, #countryname, #statename, #cityname);
PRINT 'AFTER DELETE trigger fired.'
END
I want to add insert, update, delete trigger functionality In one trigger but not work.
What I am trying:
exist select 1 --- but not work
which place I am doing wrong need help
I highly doubt that your first trigger will work properly .... you're just selecting an arbitrary rows from your tblUser table - not even one that's necessarily just been inserted or updated ....
I would strongly recommend these changes:
creating a separate trigger for each operation - that makes the trigger simpler, since you don't need to first figure out what you're dealing with....
add a ModifiedOn DATETIME2(3) column to your tblHist to record the date & time stamp when the change occurred
also possibly add an Operation column to your tblHist - so that you can understand what operation (insert, update, delete) caused this entry in the history table
properly handle the Inserted and Deleted pseudo tables in your trigger code taking into account they can (and will!) contain multiple rows - handle them in a proper, set-based fashion
drop the PRINT - makes no sense inside a trigger....
Code would be something like:
CREATE OR ALTER TRIGGER dbo.trgr_tblUser_AfterInsert
ON dbo.tblUser
AFTER INSERT
AS
BEGIN
-- do an "INSERT INTO" ...
INSERT INTO tblHist (ModifiedOn, userid, username, useraddress, countryname, statename, cityname)
-- based on the "Inserted" pseudo table, and use proper set-based approach
SELECT
SYSDATETIME(),
i.userid, i.username, i.useraddress, i.countryname, i.statename, i.cityname
FROM
Inserted i;
END
END
and
CREATE OR ALTER TRIGGER dbo.trgr_tblUser_AfterDelete
ON dbo.tblUser
AFTER DELETE
AS
BEGIN
INSERT INTO tblHist (ModifiedOn, userid, username, useraddress, countryname, statename, cityname)
SELECT
SYSDATETIME(),
d.userid, d.username, d.useraddress, d.countryname, d.statename, d.cityname
FROM
Deleted d;
END
END
If you want to check whether it was an insert, update or delete, you have to examine the row count of the inserted and deleted pseudotables - for an insert, records are only present in inserted, for delete they are only present in deleted. An update has both so you can tell the old values (deleted) and the new (inserted)
Make your life easy; put twice as many columns in your hist (should be called UserHist, no?) table as your user table, and make them e.g. old_username, new_username.. an insert the result of a full outer join between the inserted and deleted tables, this way you can tell if it was an insert, update or delete and particularly for an insert, what changed to what
Alternatively, use something like
IF EXISTS(SELECT null FROM inserted)
IF EXISTS(SELECT null FROM deleted)
--it was an update
ELSE
--it was an insert
END;
ELSE
--it was a delete
END;
Or, make 3 separate triggers
Final point of note, you're doing these queries wrong - you're declaring a bunch of variables (that can only hold a single value)and selecting the values from inserted/deleted into them but those pseudotables can have more than one row, if the query affected multiple rows (such as DELETE FROM user WHERE name = 'John')
You should be doing your operations in a bunch-of-rows way, not a "single row" way:
INSERT INTO tblHist
SELECT * FROM inserted
This can insert multiple rows into hist, and this is the way you should always think about doing things in SQLServer.. Even if you only ever insert one row and your inserted pseudotable has one row, you must get into the habit of treating it as "a collection of rows with one entry" so that any code you write won't fall apart when one day it becomes "a collection of rows with multiple entries"
Note in one of your attempts you did not select from inserted, you selected from the users table - this is wrong:
Of course, an AFTER delete trigger will insert nothing if you just deleted the only row from tblusers, but you shouldn't be using tblusers anyway
Related
On a table, there's a delete trigger that performs some operations and then at the end, executes a select statement, so when you do something like...
delete from mytable where id=1
it returns a recordset.
Is there a way to save the results of that recordset into a temp table or something? I tried something like this:
declare #temptable table (returnvalue int);
insert into #temptable (returnvalue)
delete from mytable where id=1;
But apparently that syntax doesn't work.
Well,
I can not imagine a situation that you need to return the recordset of the line you will delete using a trigger returning a recordset. But I am not here to judge your requests.
Well, you can use the OUTPUT to show the row data that will be excluded and enter this data into a temporary table. Follow the example below.
However you should know that: SQL Server does not guarantee the order in Which rows are processed and returned by DML statements using the OUTPUT clause. It is up to the application to include an WHERE clause Appropriate que can guarantee the Desired semantics, or Understand que When multiple rows may qualify for the DML operation, there is guaranteed in order. The Following example uses the subquery and you assume uniqueness is a characteristic of the column in order to DatabaseLogID in Place the Desired ordering semantics. See the link.
Example:
CREATE TABLE Person
(
PersonID int,
LastName varchar(255),
FirstName varchar(255)
);
GO
--DECLARE #MyTablePerson TABLE
--(
-- PersonID int,
-- LastName varchar(255),
-- FirstName varchar(255)
--);
--GO
--CREATE TRIGGER TRG_DLT_Person
--ON Person
--INSTEAD OF DELETE
--AS
--BEGIN
-- Some code you want to do before delete
-- DELETE Person
-- FROM DELETED D
--END
--GO
insert into Person
(PersonID,
LastName,
FirstName)
values
(1,
'Kilmister',
'Lemmy');
GO
insert into Person
(PersonID,
LastName,
FirstName)
values
(2,
'Gilmour',
'David');
GO
insert into Person
(PersonID,
LastName,
FirstName)
values
(3,
'Rose',
'Axl');
GO
insert into Person
(PersonID,
LastName,
FirstName)
values
(4,
'Bullock',
'Sandra');
GO
--
select * from Person;
GO
delete from Person
--output deleted.* INTO #MyTablePerson
output deleted.*
WHERE PersonID = 4 OR PersonID = 2;
GO
select * from Person;
GO
select * from #MyTablePerson;
GO
I put the example I'm showing in a this environment, but in this environment believe that are not supported for temporary tables.
SQL Fiddle
Regardless of this being a bad practice due to it being difficult for anyone interacting with the table to know that it will happen and deal with it when it does, and regardless of it being possible to capture, one pretty solid reason to not return result sets from a trigger is that doing so will be disallowed as of one of the next versions of SQL Server, so you would have to re-code the functionality anyway. The MSDN page for the disallow results from triggers Server Configuration Option states:
Important
This feature will be removed in the next version of Microsoft SQL Server. Do not use this feature in new development work, and modify applications that currently use this feature as soon as possible. We recommend that you set this value to 1.
If you are merely returning something like SELECT IdField FROM deleted; from the trigger, then you should (well, really need to) use the OUTPUT clause instead.
That being said, doing the following will do what you want:
CREATE TABLE #TempResults
(
ReturnValue INT
);
INSERT INTO #TempResults (ReturnValue)
EXEC('DELETE FROM mytable WHERE id = 1;');
You can test with the following:
SET NOCOUNT ON;
IF (OBJECT_ID('dbo.DeleteTriggerWithResults') IS NOT NULL)
BEGIN
DROP TABLE dbo.DeleteTriggerWithResults;
END;
CREATE TABLE dbo.DeleteTriggerWithResults
(
Col1 INT NOT NULL IDENTITY(1, 1),
Col2 DATETIME DEFAULT (GETDATE())
);
GO
CREATE TRIGGER dbo.tr_DeleteTriggerWithResults_d
ON dbo.DeleteTriggerWithResults
AFTER DELETE
AS
BEGIN
SELECT Col1
FROM deleted;
END;
GO
INSERT INTO dbo.DeleteTriggerWithResults DEFAULT VALUES;
GO 30
SELECT * FROM dbo.DeleteTriggerWithResults;
And then run the test:
DECLARE #TempResults TABLE (Col1 INT);
INSERT INTO #TempResults (Col1)
EXEC('
DELETE TOP (10)
FROM dbo.DeleteTriggerWithResults;
');
SELECT * FROM #TempResults;
Returns:
Col1
-------
10
9
8
7
6
5
4
3
2
1
I need to control table values uniqueness. It cannot be done by an index or a constraint (error message must show data from another table). I thought of after trigger but since it fires after the insert the below trigger will fire even if values are unique.
--table
CREATE TABLE Names (Id IDENTITY(1,1) NOT NULL, Name VARCHAR(20) NOT NULL)
--first record
INSERT INTO Names VALUES ('John')
--trigger
CREATE TRIGGER [dbo].[Names_Insert_Trigger]
ON [dbo].[Names]
FOR INSERT, UPDATE
AS
SET NOCOUNT ON
IF EXISTS (SELECT Name
FROM inserted
WHERE EXISTS (SELECT * FROM Names N JOIN inserted ON N.Name=inserted.Name))
BEGIN
RAISERROR('This name is already registered in file XYZ.', 16, 1)
ROLLBACK TRAN
SET NOCOUNT OFF
RETURN
END
SET NOCOUNT OFF
--I add another record with different value and the trigger fires
INSERT INTO Names VALUES ('Steven')
I also thought of an instead of insert trigger but the actual table has identity set and will likely get new columns in the future which would require updating the trigger code at each change so I can't use the below code:
CREATE TRIGGER [dbo].[Names_Insert_Trigger]
ON [dbo].[Names]
INSTEAD OF INSERT
AS
SET NOCOUNT ON
IF EXISTS (SELECT Name
FROM inserted
WHERE EXISTS (SELECT * FROM Names N JOIN inserted ON N.Name=inserted.Name))
BEGIN
RAISERROR('This name is already registed in file XYZ.', 16, 1)
ROLLBACK TRAN
SET NOCOUNT OFF
RETURN
END
ELSE
INSERT INTO Names
SELECT * FROM inserted
SET NOCOUNT OFF
Any ideas how to solve it?
Regards,
Przemek
You can use an after trigger. Just use COUNT instead of EXISTS. You should still have a non-unique index on name to optimize performance and concurrency.
IF (SELECT COUNT(*)
FROM inserted AS i
JOIN dbo.Names AS N ON
N.Name = i.Name
GROUP BY N.Name
) > 1
BEGIN
RAISERROR...
END;
The real solution is to use the UNIQUE constraint to this problem, it's designed to solve it and it's much more performant and safer than a trigger for this usage. The error message is better built client-side and ignore the server genereated one, save for determining the exact reason.
But if you really want to follow the trigger route, use the AFTER version, but fix the query for detect duplicates:
SELECT * FROM (
SELECT Name
FROM Names
WHERE id NOT IN (SELECT id FROM inserted)
) PreviousNames
INNER JOIN inserted ON PreviousNames.Name = inserted.Name
(I'm just showing the query to check duplication that goes into the IF EXIST instruction, not the whole trigger).
It begins by creating a subquery that gets the names NOT being inserted (so that you don't get a false positive), then simply joins again to inserted to check if any value is in both tables.
There is an additional problem that can happen when using SNAPSHOT issolation level. In this mode, the trigger will NOT see the changes made by other connections, nor they'll be blocked until the trigger ends. I'm not quite familiar with the details, but will leave this article as reference and possible solutions: https://sqlserverfast.com/?s=snapshot+integrity
Using Great Plains here and one of our users keeps screwing up customer data so we want to put a trigger on the customer table and so we can find out who it is.
Anyway, I created a table called audit_RM00101 as follows:
DATE nchar(10)
CUSTNMBR char(15)
CUSTNAME char(65)
UPSZONE char(3)
SALSTERR char(15)
USERID nchar(100)
I want to capture those same fields from the table I want to audit so I wrote the trigger as follows:
CREATE TRIGGER CatchCustomerRegionUpdate
ON RM00101
FOR UPDATE
AS
DECLARE #UserID VARCHAR(128)
SELECT #UserID = system_user
INSERT INTO audit_RM00101
SELECT DATE, CUSTNMBR, CUSTNAME, UPSZONE, SALSTERR, #UserID FROM UPDATED
The trigger gets created just fine but when I try to test it by updating a customer record in Great Plains, Great Plains throws up an ugly error and the trigger doesn't get fired.
What am I doing wrong here?
Thanks.
in a trigger, you get the DELETED and INSERTED tables, there is no UPDATED, so replace FROM UPDATED with FROM INSERTED
also try to fix your USERID column, your audit_RM00101.USERID is a nchar(100) while #UserID is a VARCHAR(128).
EDIT based on OPs comment: Ah, so there is no way to audit when a table is updated by using a trigger?
in a trigger when deleting, DELETED is populated, but INSERTED is empty
in a trigger when updating, DELETED is populated with the original value, and INSERTED is populated with the newly updated values
in a trigger when inserting, DELETED is empty, but INSERTED has the newly inserted values
There is no UPDATED in SQL Server; just inserted and deleted.
Also, it makes sense to add IF ##ROWCOUNT = 0 RETURN in the very beginning of triger's body.
When UPDATE takes place, both inserted and deleted tables are not empty. You may add the following code to make sure you handle UPDATE, not insert/delete:
IF EXISTS(SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted)
BEGIN
-- handle update
END ;
It's not really important for your trigger because you specify just FOR UPDATE, it would be important if you had, for instance, FOR UPDATE, INSERT, DELETE.
we have only two magic tables called INSERTED and DELETED
update indirectly is a Delete statement followed by Insert statement. so you have to update the column's value which is present in INSERTED.
CREATE TRIGGER CatchCustomerRegionUpdate
ON RM00101
AFTER UPDATE
AS
BEGIN
DECLARE #INSERTED INT, #DELETED INT
SET #INSERTED = SELECT COUNT(*) FROM INSERTED
SET #DELETED = SELECT COUNT(*) FROM DELETED
IF #INSERTED = 1 AND #DELETED = 1
BEGIN
UPDATE TABLE1
SET COL1 = INSERTED_COL1
WHERE IDCOL = INSERTED_IDCOL
END
END
I want to insert rows into the audit table whenever an insert, update or delete takes place in the master table "Table1" - doesn't matter which column is changed/inserted. I also want to add I, U or D on insert, update or delete. For insert and delete I am checking if rows exist in the inserted and deleted table. What is the best way to approach update.
My code for insert and delete is :
CREATE TRIGGER [dbo].[tr_Table1_InsertUpdate_Table1History_Insert]
ON [dbo].[Table1]
FOR INSERT, DELETE, UPDATE
AS
BEGIN
IF EXISTS(SELECT * FROM Inserted)
BEGIN
INSERT INTO Table1History(...., ModificationType)
SELECT ..., 'I'
FROM Inserted
END
IF EXISTS(SELECT * FROM Deleted)
BEGIN
INSERT INTO Table1History(..., ModificationType)
SELECT ..., 'D'
FROM Deleted
END
END
GO
Kindly help!
For updates, the original values for the row will be added to the deleted table, and the new values for the row will be added to the inserted table. So, to identify inserts, deletes and updates you would do the following
Inserts - get the rows from inserted that are not in deleted
Deletes - get the rows from deleted that are not in inserted.
Updates - get the rows that are in both inserted and deleted
Below is an example of a trigger generated by ApexSQL Audit
It’s not a cheap tool but you can probably use it in trial mode to get the job done.
Notice the INSERT INTO dbo.AUDIT_LOG_DATA part and repeat it for every column you want to audit.
There are two tables in the background for storing the data and several stored procedures as well but this will get you going in the right direction.
CREATE TRIGGER [dbo].[tr_d_AUDIT_TableName]
ON [dbo].[TableName]
FOR DELETE
NOT FOR REPLICATION
AS
BEGIN
DECLARE
#IDENTITY_SAVE varchar(50),
#AUDIT_LOG_TRANSACTION_ID Int,
#PRIM_KEY nvarchar(4000),
--#TABLE_NAME nvarchar(4000),
#ROWS_COUNT int
SET NOCOUNT ON
Select #ROWS_COUNT=count(*) from deleted
Set #IDENTITY_SAVE = CAST(IsNull(##IDENTITY,1) AS varchar(50))
INSERT
INTO dbo.AUDIT_LOG_TRANSACTIONS
(
TABLE_NAME,
TABLE_SCHEMA,
AUDIT_ACTION_ID,
HOST_NAME,
APP_NAME,
MODIFIED_BY,
MODIFIED_DATE,
AFFECTED_ROWS,
[DATABASE]
)
values(
'TableName',
'dbo',
3, -- ACTION ID For DELETE
CASE
WHEN LEN(HOST_NAME()) < 1 THEN ' '
ELSE HOST_NAME()
END,
CASE
WHEN LEN(APP_NAME()) < 1 THEN ' '
ELSE APP_NAME()
END,
SUSER_SNAME(),
GETDATE(),
#ROWS_COUNT,
'DatabaseName'
)
Set #AUDIT_LOG_TRANSACTION_ID = SCOPE_IDENTITY()
INSERT
INTO dbo.AUDIT_LOG_DATA
(
AUDIT_LOG_TRANSACTION_ID,
PRIMARY_KEY_DATA,
COL_NAME,
OLD_VALUE_LONG,
DATA_TYPE
, KEY1
)
SELECT
#AUDIT_LOG_TRANSACTION_ID,
convert(nvarchar(1500), IsNull('[Order_ID]='+CONVERT(nvarchar(4000), OLD.[Order_ID], 0), '[Order_ID] Is Null')),
'Order_ID',
CONVERT(nvarchar(4000), OLD.[Order_ID], 0),
'A'
, CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[Order_ID], 0))
FROM deleted OLD
WHERE
OLD.[Order_ID] Is Not Null
END
Have you consider using AutoAudit?
AutoAudit is a SQL Server (2005, 2008)
Code-Gen utility that creates Audit
Trail Triggers with:
Created, CreatedBy, Modified, ModifiedBy, and RowVersion
(incrementing INT) columns to table
Insert event logged to Audit table
Updates old and new values logged to Audit table
Delete logs all final values to the Audit tbale
view to reconstruct deleted rows
UDF to reconstruct Row History
Schema Audit Trigger to track schema changes
Re-code-gens triggers when Alter Table changes the table
I have complaint table
1. tblProfile with columns
userId | name | age | address | mobileno |
2. tblUserId with columns userId | role | status
now when user fills the form I want to insert one row in tblProfile, before inserting a new row I want to create userId by combining starting letters of name and mobile no and then insert into tblprofile with userId after this I want to insert that UserId into tblUserId table.
for this I have to use two triggers one is before insert trigger and another is after insert trigger.but I dont know how to capture user information to create userId and how to pass that Id to second trigger.
Since you're acting upon INSERTs, try #inserted or inserted as the container of your inserting values.
CREATE TRIGGER...
insert into tblProfile (userId, name, age, address, mobileno) (
select N'do-your-concat-here...', name, age, address, mobileno
from inserted
)
I have not worked with triggers for a long time now, but this should help you get what you need.
Please have an eye out this link: Using the inserted and deleted Tables.
However, you already have all the tools on the application-side as you have the information and the capability to work with the in-memory information data.
EDIT #1
how to pass this Id to second trigger?
In this particular situation, I see more suitable to process with a stored procedure than two independant triggers. We have interdependent information data here (userId). I do think the simplest way would be a stored procedure. It is adviseable to wrap these operation within a transaction scope, as if either insert fails, both won't be applied, assuring data integrity.
CREATE PROCEDURE prcInsProfileUserId
-- Assuming data types. Replace with proper data types as needed.
#name nvarchar(50) NOT NULL
, #age int NOT NULL
, #address nvarchar(150) NOT NULL
, #mobileno bigint NOT NULL
, #role nvarchar(10) NOT NULL
, #status int NOT NULL
AS BEGIN TRANSACTION
DECLARE #UserId nvarchar(10)
SET #UserId = N'do-your-concat-here...';
-- We then have the userId value, so we may insert into both tables accordingly.
insert into tblProfile (userId, name, age, mobileno)
values (#userId, #name, #age, #address, #mobileno)
insert into tblUserId (userId, role, status)
values (#userId, #role, #status)
COMMIT
END
However, if you do prefer to go with triggers, then the alternative would be to insert the concatenated value for userId into a temporary table. You then should have a DDL looking as follows:
DECLARE #UserIdTempTable TABLE (
userId nvarchar(10) NOT NULL
)
Then, within the first trigger, you would have to set the value of a #userId variable to contain you concatenated userId, then use it to insert into tblProfile, then perform a second insert into #UserIdTempTable.
CREATE TRIGGER...
DECLARE #userId nvarchar(10)
SET #userId = N'do-your-concat-here...'
insert into tblProfile (userId, name, age, address, mobileno) (
select #userId, name, age, address, mobileno
from inserted
)
IF ##ROWCOUNT > 0
BEGIN
delete from #UserIdTempTable -- assuring there is no mistake possible while populating and retrieving the userId
insert into #UserIdTempTable (userId)
values (#userId)
END
END
Then, you may select it from your second trigger.
CREATE TRIGGER second...
insert into tblUserId (userId, role, status) (
select tmp.userId, i.role i.status
from #UserIdTempTable tmp
, inserted i
)
Be careful here though, because no data integrity is absolutely preserved as the first insert may have been processed successfully, but the second not. To preserve data integrity, you would have to verify whether ##ROWCOUNT is greater than 0, unless you would delete the record with this actual userId from tblProfile.
This is abosolute hard hand-work. Processing through triggers is not adviseable here, because within the insertion of tblProfile, you do not have information data required by tblUserId, so you have to have two DbCommand and launch two ExecuteNonQuery() in a row. That is a lot of overhead for such a tiny task. Then, it would be more aviseable to process with the stored procedure as suggested, plus it assures data integrity by the DBMS itself, instead of simulating it through a ##ROWCOUNT verification.
Disclaimer: This code is provided as-is and is not guaranteed to compile without you to adapt it to fit your situation. I wrote it off the top of my head with no verification.
I do hope this helps! =)