I'm trying to understand how to streamline the process of inserting a record if none exists or updating a record if it already exists. I'm not using stored procedures, although maybe that would be the most efficient way of doing this.
The actual scenario in which this is necessary is saving a user preference/setting to my SettingsUser table.
In MS Access I would typically pull a DAO recordset looking for the specified setting. If the recordset comes back empty then I know I need to add a new record which I can do with the same recordset object. On the other hand, if it isn't empty, I can just update the setting's value right away. In theory, this is only two database operations.
What is the recommended way of doing this in .NET?
A stored procedure is certainly an easy way to do that. There you can try to update the record, and if no record changes, you add it. Example:
create procedure UpateUserSetting
#UserId int,
#Setting int
as
set nocount on
update UserSetting
set Setting = #Setting
where UserId = #UserId
if (##rowcount = 0) begin
insert into UserSetting (
UserId, Setting
) values (
#UserId, #Setting
)
end
You can do the same without a stored procedure. Then you would first run the update, and check the number of affected rows, which you get returned from the ExecuteNonQuery method, and do the insert in another query if needed.
Related
I have a table that utilizes an instead of insert trigger. The trigger manipulates the data for each inserted row before inserting it. Because the inserted table is not modifiable, it was implemented using a temp (#) table. At the end of the trigger a select from the temp table is done to return the inserted data to the calling client. When I do a an insert in SSMS, I can see the data that is returned and the columns all have names and values. My Trigger looks like this:
Create TRIGGER [dbo].[RealTableInsteadOfInsert] on [dbo].[RealTable]
INSTEAD OF INSERT
AS
BEGIN
set nocount on;
declare #lastDigit int;
if exists (select * from inserted)
Begin
Select *
into #tempInserted
from inserted
.... Logic to add and manipulate column data .....
INSERT INTO RealTAble(id, col1, col2, col3,....)
Select *
from #tempInserted;
Select id as insertId, *
from #tempInserted;
END
End
My question is how can I capture the output of the instead of trigger into a table for further processing using the returned data. I can't use an output clause on the insert statement as the temp table no longer exists and the data that was calculated/modified on the insert was not done in the inserted table. Is my only option to update the trigger to use a global temp table instead of a local?
You have two main options:
Send data through service broker. This is complicated and potentially slow. On the plus side, it does let you do your further processing work decoupled from the original transaction, which is nice in certain use cases.
Write the data to a real table. This can also give you transactional decoupling, but you don't get automatic activation of the decoupled processing logic if you want that.
OK, but if you write to a real table, and a lot of processes are causing the trigger to fire, how do you find "your" data? Putting aside the fact that a global temp table has the same problem unless you want to get dynamic, one solution is to use a process keyed table, as described by Erland Sommarskog in his data sharing article.
Returning data from triggers (ie, to a client) is a bad idea. The ability to do this at all will soon be removed I know you don't need to do this in your solution, just giving you a heads up.
I have an insert statement executed via ADODB.Connection.Execute inserting into a table on MSSQL server with an identity column, but the returned recordset does not return the ID of the newly inserted record.
I have tried the following forms of obtaining the inserted ID of the record.
INSERT INTO blah VALUES (blah); SELECT SCOPE_IDENTITY()
INSERT INTO blah VALUES (blah); SELECT ##IDENTITY
INSERT INTO blah OUTPUT INSERTED.ID INTO #ID VALUES(blah); SELECT ID FROM #ID
In all cases, when querying the recordset returned it does not contain the identity
var I = DB.Execute(insert_statement_from_above);
if (I) {
var INSERTED_ID = I(0).Value;
}
Why might this be happening?
Check the table being inserted into for any INSERT triggers, and check that those INSERT triggers are using SET NOCOUNT ON.
When a trigger does not specify SET NOCOUNT ON, if it updates the database, it will generate a result set, that result set will appear before the result set that contains the SCOPE_IDENTITY result.
See: https://msdn.microsoft.com/en-us/library/ms189837.aspx
In reality what is happening is that the recordset contains one or more result sets.
TRIGGER RESULT SET
SCOPE_IDENTITY RESULT SET
When you query the recordset for a scope identity you are actually querying the result set output by the trigger.
You can use RS.nextRecordset() and then query the SCOPE_IDENTITY but if the trigger may or may not perform an update you end up not knowing which result set contains your SCOPE_IDENTITY, is it the first or the second, is there a second?
It is generally accepted good practice for triggers to specify SET NOCOUNT ON however if you absolutely must have SET NOCOUNT OFF in your trigger or have no control over the trigger, you can work around it with the following code.
Note: The Scope Identity in the examples above will always be the last result set in the recordset. So we can find it as follows:
var I = DB.Execute(insert_statement_from_above);
if (I) {
// find the result set with the scope identity in it
while (I.State == 0) {
I = I.nextRecordset();
}
var INSERTED_ID = I(0).Value;
}
This looks for the result set that contains your scope identity by ignoring the earlier closed result sets.
See Also:
State property, nextRecordset()
If it's fine for you to prevent all triggers from returning the result sets, server option "Disallow returning results from triggers" might be a solution for you.
sp_configure 'disallow results from triggers', 1
GO
RECONFIGURE
GO
I'm design a new db schema for a SQL Server 2012 database.
Each table should get two extra columns called modified and created which should be automatically change as soon a row gets inserted or updated.
I don't know how rather the best way to get there.
I assuming that trigger are the best way to handle it.
I was trying to find examples with triggers.. but the tutorials which I found insert data in another table etc.
I assumed it's a quite common scenario but I couldn't find the answer yet.
The created column is simple - just a DATETIME2(3) column with a default constraint that gets set when a new row is inserted:
Created DATETIME2(3)
CONSTRAINT DF_YourTable_Created DEFAULT (SYSDATETIME())
So when you insert a row into YourTable and don't specify a value for Created, it will be set to the current date & time.
The modified is a bit more work, since you'll need to write a trigger for the AFTER UPDATE case and update it - you cannot declaratively tell SQL Server to do this for you....
Modified DATETIME2(3)
and then
CREATE TRIGGER updateModified
ON dbo.YourTable
AFTER UPDATE
AS
UPDATE dbo.YourTable
SET modified = SYSDATETIME()
FROM Inserted i
WHERE dbo.YourTable.PrimaryKey = i.PrimaryKey
You need to join the Inserted pseudo table which contains all rows that were updated with your base table on your primary key for that table.
And you'll have to create this AFTER UPDATE trigger for each table that you want to have a modified column in.
Generally, you can have the following columns:
LastModifiedBy
LastModifiedOn
CreatedBy
CreatedOn
where LastModifiedBy and CreatedBy are references to a users table (UserID) and the LastModifiedOn and CreatedOn columns are date and time columns.
You have the following options:
Solution without triggers - I have read somewhere that "The best way to write triggers is not to write such." and you should know that generally they are hurting the performance. So, if you can avoid them it is better to do so, even using triggers may look the easiest thing to do in some cases.
So, just edit all you INSERT and UPDATE statements to include the current UserID and current date and time. If such user ID can not be defined (anonymous user) you can use 0 instead and the default value of the columns (in case no user ID is specified will be NULL). When you see NULL values are inserted you should find the "guilty" statements and edit it.
Solution with triggers - you can created AFTER INSERT, UPDATE trigger and populated the users columns there. It's easy to get the current date and time in the context of the trigger (use GETUTCDATE() for example). The issue here is that the triggers do not allowed passing/accepting parameters. So, as you are not inserting the user ID value and you are not able to pass it to the trigger. How to find the current user?
You can use SET CONTEXT_INFO and CONTEXT_INFO. Before all you insert and update statements you must use the SET CONTEXT_INFO to add the current user ID to the current context and in the trigger you are using the CONTEXT_INFO function to extract it.
So, when using triggers you again need to edit all your INSERT and UPDATE clauses - that's why I prefer not to use them.
Anyway, if you need to have only date and time columns and not created/modified by columns, using triggers is more durable and easier as you are not going to edit any other statements now and in the future.
With SQL Server 2016 we can now use the SESSION_CONTEXT function to read session details. The details are set using sp_set_session_context (as read-only or read and write). The things are a little bit user-friendly:
EXEC sp_set_session_context 'user_id', 4;
SELECT SESSION_CONTEXT(N'user_id');
A nice example.
Attention, above works fine but not in all cases,
I lost a lot of time and found this helpfull:
create TRIGGER yourtable_update_insert
ON yourtable
AFTER UPDATE
as
begin
set nocount on;
update yourtable set modified=getdate(), modifiedby = suser_sname()
from yourtable t
inner join inserted i on t.uniqueid=i.uniqueid
end
go
set nocount on; is needed else you get the error:
Microsoft SQL Server Management Studio
No row was updated.
The data in row 5 was not committed.
Error Source: Microsoft.SqlServer.Management.DataTools.
Error Message: The row value(s) updated or deleted either do not make the row unique or they alter multiple rows(2 rows).
Correct the errors and retry or press ESC to cancel the change(s).
OK Help
CREATE TRIGGER [dbo].[updateModified]
ON [dbo].[Transaction_details]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Transaction_details
SET ModifedDate = GETDATE() FROM dbo.Transaction_details t JOIN inserted i ON
t.TransactionID = i.TransactionID--SYSDATETIME()
END
One important thing to consider is that you should always have the inserted / updated time for all of your tables and rows be from the same time source. There is a danger - if you do not use triggers - that different applications making direct updates to your tables will be on machines that have different times on their clocks, or that there will not be consistent use of local vs. UTC in the application layer.
Consider a case where the system making the insert or update query that directly sets the updated / modified time value has a clock that is 5 minutes behind (unlikely, but worth considering) or is using local time versus UTC. If another system is polling using an interval of 1 minute, it might miss the update.
For a number of reasons, I never expose my tables directly to applications. To handle this situation, I create a view on the table explicitly listing the fields to be accessed (including the updated / modified time field). I then use an INSTEAD OF UPDATE, INSERT trigger on the view and explicitly set the updatedAt time using the database server's clock. This way I can guarantee that the timebase for all records in the database is identical.
This has a few benefits:
It only makes one insert to the base table and you don't have to
worry about cascading triggers being called
It allows me to control at the field level what information I expose
to the business layer or to other consumers of my data
It allows me to secure the view independently from the base table
It works great on SQL Azure.
Take a look at this example of the trigger on the view:
ALTER TRIGGER [MR3W].[tgUpdateBuilding] ON [MR3W].[vwMrWebBuilding]
INSTEAD OF UPDATE, INSERT AS
BEGIN
SET NOCOUNT ON
IF EXISTS(SELECT * FROM DELETED)
BEGIN
UPDATE [dbo].[Building]
SET
,[BuildingName] = i.BuildingName
,[isActive] = i.isActive
,[updatedAt] = getdate()
FROM dbo.Building b
inner join inserted i on i.BuildingId = b.BuildingId
END
ELSE
BEGIN
INSERT INTO [dbo].[Building]
(
[BuildingName]
,[isActive]
,[updatedAt]
)
SELECT
[BuildingName]
,[isActive]
,getdate()
FROM INSERTED
END
END
I hope this helps, and I would welcome comments if there are reasons this is not the best solution.
This solution might not work for all use cases but wherever possible its a very clean way.
Create an stored procedure for inserting/updating row in table and only use this sp for modifying the table. In stored procedure you can always set created and updated column as required. e.g. setting updatedTime = GetUTCTime()
I've got this database trigger:
CREATE TRIGGER setDescToUpper
ON part_numbers
AFTER INSERT,UPDATE
AS
DECLARE #PnumPkid int, #PDesc nvarchar(128)
SET #PnumPkid = (SELECT pnum_pkid FROM inserted)
SET #PDesc = (SELECT UPPER(part_description) FROM inserted)
UPDATE part_numbers set part_description_upper = #PDesc WHERE pnum_pkid=#PnumPkid
GO
Is this a bad idea? That is to update a column on the same table. I want it to fire for both insert and update.
It works, I'm just afraid of a cyclical situation. The update, inside the trigger, fires the trigger, and again and again. Will that happen?
Please, don't nitpick at the upper case thing. Crazy situation.
It depends on the recursion level for triggers currently set on the DB.
If you do this:
SP_CONFIGURE 'nested_triggers',0
GO
RECONFIGURE
GO
Or this:
ALTER DATABASE db_name
SET RECURSIVE_TRIGGERS OFF
That trigger above won't be called again, and you would be safe (unless you get into some kind of deadlock; that could be possible but maybe I'm wrong).
Still, I do not think this is a good idea. A better option would be using an INSTEAD OF trigger. That way you would avoid executing the first (manual) update over the DB. Only the one defined inside the trigger would be executed.
An INSTEAD OF INSERT trigger would be like this:
CREATE TRIGGER setDescToUpper ON part_numbers
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO part_numbers (
colA,
colB,
part_description
) SELECT
colA,
colB,
UPPER(part_description)
) FROM
INSERTED
END
GO
This would automagically "replace" the original INSERT statement by this one, with an explicit UPPER call applied to the part_description field.
An INSTEAD OF UPDATE trigger would be similar (and I don't advise you to create a single trigger, keep them separated).
Also, this addresses #Martin comment: it works for multirow inserts/updates (your example does not).
Another option would be to enclose the update statement in an IF statement and call TRIGGER_NESTLEVEL() to restrict the update being run a second time.
CREATE TRIGGER Table_A_Update ON Table_A AFTER UPDATE
AS
IF ((SELECT TRIGGER_NESTLEVEL()) < 2)
BEGIN
UPDATE a
SET Date_Column = GETDATE()
FROM Table_A a
JOIN inserted i ON a.ID = i.ID
END
When the trigger initially runs the TRIGGER_NESTLEVEL is set to 1 so the update statement will be executed. That update statement will in turn fire that same trigger except this time the TRIGGER_NESTLEVEL is set to 2 and the update statement will not be executed.
You could also check the TRIGGER_NESTLEVEL first and if its greater than 1 then call RETURN to exit out of the trigger.
IF ((SELECT TRIGGER_NESTLEVEL()) > 1) RETURN;
Use a computed column instead. It is almost always a better idea to use a computed column than a trigger.
See Example below of a computed column using the UPPER function:
create table #temp (test varchar (10), test2 AS upper(test))
insert #temp (test)
values ('test')
select * from #temp
And not to sound like a broken record or anything, but this is critically important. Never write a trigger that will not work correctly on multiple record inserts/updates/deletes. This is an extremely poor practice as sooner or later one of these will happen and your trigger will cause data integrity problems asw it won't fail precisely it will only run the process on one of the records. This can go a long time until someone discovers the mess and by themn it is often impossible to correctly fix the data.
It might be safer to exit the trigger when there is nothing to do. Checking the nested level or altering the database by switching off RECURSIVE can be prone to issues.
Ms sql provides a simple way, in a trigger, to see if specific columns have been updated. Use the UPDATE() method to see if certain columns have been updated such as UPDATE(part_description_upper).
IF UPDATE(part_description_upper)
return
Yes, it will recursively call your trigger unless you turn the recursive triggers setting off:
ALTER DATABASE db_name SET RECURSIVE_TRIGGERS OFF
MSDN has a good explanation of the behavior at http://msdn.microsoft.com/en-us/library/aa258254(SQL.80).aspx under the Recursive Triggers heading.
Yea...having an additional step to update a table in which you can set the value in the inital insert is probably an extra, avoidable process.
Do you have access to the original insert statement where you can actually just insert the part_description into the part_description_upper column using UPPER(part_description) value?
After thinking, you probably don't have access as you would have probably done that so should also give some options as well...
1) Depends on the need for this part_description_upper column, if just for "viewing" then can just use the returned part_description value and "ToUpper()" it (depending on programming language).
2) If want to avoid "realtime" processing, can just create a sql job to go through your values once a day during low traffic periods and update that column to the UPPER part_description value for any that are currently not set.
3) go with your trigger (and watch for recursion as others have mentioned)...
HTH
Dave
create or replace
TRIGGER triggername BEFORE INSERT ON
table FOR EACH ROW
BEGIN
/*
Write any select condition if you want to get the data from other tables
*/
:NEW.COLUMNA:= UPPER(COLUMNA);
--:NEW.COUMNa:= NULL;
END;
The above trigger will update the column value before inserting.
For example if we give the value of COLUMNA as null it will update the column as null for each insert statement.
I simply want a stored procedure that calculates a unique id (that is separate from the identity column) and inserts it. If it fails it just calls itself to regenerate said id. I have been looking for an example, but cant find one, and am not sure how I should get the SP to call itself, and set the appropriate output parameter. I would also appreciate someone pointing out how to test this SP also.
Edit
What I have now come up with is the following (Note I already have an identity column, I need a secondary id column.
ALTER PROCEDURE [dbo].[DataInstance_Insert]
#DataContainerId int out,
#ModelEntityId int,
#ParentDataContainerId int,
#DataInstanceId int out
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
WHILE (#DataContainerId is null)
EXEC DataContainer_Insert #ModelEntityId, #ParentDataContainerId, #DataContainerId output
INSERT INTO DataInstance (DataContainerId, ModelEntityId)
VALUES (#DataContainerId, #ModelEntityId)
SELECT #DataInstanceId = scope_identity()
END
ALTER PROCEDURE [dbo].[DataContainer_Insert]
#ModelEntityId int,
#ParentDataContainerId int,
#DataContainerId int out
AS
BEGIN
BEGIN TRY
SET NOCOUNT ON;
DECLARE #ReferenceId int
SELECT #ReferenceId = isnull(Max(ReferenceId)+1,1) from DataContainer Where ModelEntityId=#ModelEntityId
INSERT INTO DataContainer (ReferenceId, ModelEntityId, ParentDataContainerId)
VALUES (#ReferenceId, #ModelEntityId, #ParentDataContainerId)
SELECT #DataContainerId = scope_identity()
END TRY
BEGIN CATCH
END CATCH
END
In CATCH blocks you must check the XACT_STATE value. You may be in a doomed transaction (-1) and in that case you are forced to rollback. Or your transaction may had already had rolled back and you should not continue to work under the assumption of an existing transaction. For a template procedure that handles T-SQL exceptions, try/catch blcoks and transactions correctly, see Exception handling and nested transactions
Never, under any languages, do recursive calls in exception blocks. You don't check why you hit an exception, therefore you don't know if is OK to try again. What if the exception is 652, read-only filegroup? Or your database is at max size? You'll re-curse until you'll hit stackoverflow...
Code that reads a value, makes a decision based on that value, then writes something is always going to fail under concurrency unless properly protected. You need to wrap the SELECT and INSERT in a transaction and your SELECT must be under SERIALISABLE isolation level.
And finally, ignoring the blatantly wrong code in your post, here is how you call a stored procedure passing in OUTPUT arguments:
exec DataContainer_Insert #SomeData, #DataContainerId OUTPUT;
Better yet, why not make UserID an identity column instead of trying to re-implement an identity column manually?
BTW: I think you meant
VALUES (#DataContainerId + 1 , SomeData)
Why not use the:
NewId()
T SQL function? (assuming sql server 2005/2008)
that sp will never ever do a successful insert, you have an identity property on the DataContainer table but you are inserting the ID, in that case you will need to set identity_insert on but then scope_identity() won't work
A PK violation also might not be trapped so you might also need to check for XACT_STATE()
why are you messing around with max, use scope_identity() and be done with it