Suppose I have 3 tables in my database like this:
Person (personid, ...) ---this is entity, personid is identity column
Phone (phoneid, ...) ---this is entity, phoneid is identity column
PersonPhone (personid, phoneid) ---this relationship
When I insert data into tables, I need to insert rows into the entity tables first and get the generated id, and then I need to insert a row into the relationship table.
It's working fine. Question is: I have a stored procedure with transaction with Try Catch statement like:
BEGIN TRY
declare aCursor cursor local fast_forward for (Select Query...)
open aCursor;
fetch next from aCursor into #variables....
while ##fetch_status = 0
begin
INSERT INTO Person(...);
set #personid =##IDENTITY;
INSERT INTO Phone(...);
set #phoneid =##IDENTITY;
INSERT INTO PersonPhone(#personid, #phoneid);
end;
END TRY
BEGIN CATCH
close aCursor;
deallocate selectdistributor;
SELECT ERROR_NUMBER() AS ErrorNumber,ERROR_MESSAGE() AS ErrorMessage;
ROLLBACK TRAN;
RETURN;
END CATCH
COMMIT;
close aCursor;
deallocate aCursor;
With the cursor, there is more the one records will be inserted for person, phone. When run the SP, it will stop for first time data insert on relationship. Also I can get right #personid, #phoneid, but before the transaction is done, I get error on INSERT INTO PersonPhone(#personid, #phoneid); as:
The INSERT statement conflicted with the FOREIGN KEY constraint "Person_PersonPhone_FK1". The conflict occurred in database "MYDB", table "dbo.Person", column 'PersonID'.
Looks like Id generated by system not recognized before transaction done.
How to resolve this problem?
Likely your problem is that you used ##identity which should underr no circumstances ever be used for this purpose. If one of the tables has a trigger, it is possoble to return the wrong value (the identity from the table the trigger is inserting into) and that value coincidentally does not exist in the table that the foreign key links to.
So suppose you havea trigger onteh first table that iunserts to an audit table with an identity. The record is inserted with an ID of 1234. BUt the tirigger inserts to a differnt table and so value retiurned by ##Identity is 5678 which does not exist inteh orginal table. When you go to insert to the table with the FK back on the first table, you get theerror message because value 5678 does not exist in the first table.
Use the OUTPUT clause instead. You could also use scope_identity(), but output is more flexible (And lets you do multiple record inserts and receive all the identities back as well as the other fields which might uniquely identify a record.
And I'm with Aaron, I would not use a cursor for this unless someone threatened my life.
Related
I have to create query that after delete will insert data in other table. But I've deleted 3 rows in my table, but table with insert has only one row. And it's the first row that was deleted.
This is my trigger:
CREATE trigger trigger1
ON Sales.SalesPerson
FOR DELETE
AS
BEGIN
DECLARE #ID int
SELECT #ID = BusinessEntityID FROM deleted
INSERT INTO dbo.Deleted(ID)
VALUES (#ID)
PRINT 'TRIGGER'
END
What did I do wrong?
In SQL Server, triggers fire per operation, not per row.
If you delete three rows, your SELECT assignment is going to (at least logically) assign each of those three values to the variable one at a time, and so the value that ultimately ends up in your logging table is the arbitrary value that happened to be assigned last.
You can simulate this as follows:
DECLARE #id int;
SELECT #id = database_id FROM sys.databases;
PRINT #id;
There are multiple rows in sys.databases, why did only one value get printed?
Instead of using a scalar variable and expecting it to somehow hold multiple values (or for the insert to happen multiple times), you need to insert as a set in a single operation:
INSERT dbo.Deleted(ID)
SELECT BusinessEntityID from deleted;
Further reading.
I would like to know if it's possible that a select is blocking a table if it's inside a transaction.
It's something like this:
CREATE PROCEDURE InsertClient (#name NVARCHAR(256))
AS
BEGIN
DECLARE #id INT = 0;
BEGIN TRY
BEGIN TRAN InsertingClient
SELECT #id = MAX(ID) + 1 FROM Clients;
INSERT INTO Clients (Id, Name)
VALUES (#id, #name);
SELECT id, name
FROM Clients;
COMMIT TRAN InsertingClient
END TRY
BEGIN CATCH
ROLLBACK TRAN InsertingClient
END CATCH;
END
It's a dummy example, but if there's a lot of records in that table, and an API is receiving a lot of requests and calling this stored procedure, could be blocked by the initial and final select? Should I use the begin and commit only in the insert to avoid the block?
Thanks!
Based on the sample code you have provided it is critical that the first select is within the transaction because it appears you are manually creating an id based on the max id in the table, and without locking the table you could end up with duplicates. One assumes your actual code has some locking hints (e.g. with (updlock,holdlock)) to ensure that.
However your second select should not be in your transaction because all it will serve to do is make the locks acquired earlier in the transaction last the additional time of the select, when (again based on the sample code) there is no need to do that.
As an aside there are much better ways to generate an id such as using an identity column.
Recently, I needed to write a stored procedure to insert only one record when the first user come and ignore for others. I think the IF NOT EXISTS INSERT will not work for me. Also, some people saying online that MERGE adds race condition. Any quick way to achieve this? This is my code for now.
IF NOT EXISTS (SELECT ......)
INSERT
You might add another table to use as the lock mechanism.
Let's say your table's name is a, and the name of the table which has the locked value is check_a :
create table a (name varchar(10))
create table check_a (name varchar(10))
Insert only one record to the lock table:
insert into check_a values ('lock')
go
Then create a stored procedure which checks if there is a value in the main table. If there is no record, we might lock the only value in the table check_a and insert our value into the table a.
create proc insert_if_first
as
begin
set nocount on
if not exists (select name from a)
begin
declare #name varchar(10)
begin tran
select #name = name from check_a with (updlock)
if not exists (select name from a)
begin
insert into a values ('some value')
end
commit
end
end
go
First selection from the table a to check there is no record is for using system resources as low as we can. If there is a record in the table a, we can skip opening transaction and skip locking the row.
Second check is to make sure that while we are waiting to obtain the lock, no one inserted a row to the table a.
This way, only the first user which can lock check_a will be able to insert a value to the table a.
I'm guessing that you mean you want users to make a stored procedure that makes sure only one user can run the procedure. Then you need to use isolation levels. There are different Isolation levels, so you need to decide which one you need.
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
You can read what they do here:
https://msdn.microsoft.com/en-us/library/ms173763.aspx
I am creating a stored procedure to create a new customer so for instance,
CREATE PROCEDURE Customer_Create
#customer_arg
#type_arg
AS
BEGIN
INSERT INTO Customer (Customer_id, Type_id)
VALUES (#Customer_arg,#type_arg)
End;
If I have several foreign keys in my statement and they are all ID's is there a way for me to pull the NEXT ID number automatically without having to know what it would be off the top of my head when I run the execute statement? I would like to just have it pull the fact that the ID will be 2 because the previous record was 1
EXECUTE Customer_Create 16,2
Is it something wnith output? If so how does this work code wise
I suspect that what you want to do is return the new id after the record is inserted. For that:
CREATE PROCEDURE Customer_Create (
#customer_arg,
#type_arg,
#NewCustomerId int output
) AS
BEGIN
INSERT INTO Customer(Customer_id, Type_id)
VALUES (#Customer_arg, #type_arg);
#NewCustomerId = scope_identity();
End;
There are several other choices for getting the identity, which are explained here.
To get to the last inserted IDENTITY value you should use the OUTPUT clause like this:
DECLARE #IdentValues TABLE(v INT);
INSERT INTO dbo.IdentityTest
OUTPUT INSERTED.id INTO #IdentValues(v)
DEFAULT VALUES;
SELECT v AS IdentityValues FROM #IdentValues;
There are several other mechanisms like ##IDENTITY but they all have significant problems. See my Identity Crisis article for details.
In your case you can also experiment with #IDENTITY like this
DECLARE #NextID int
--insert statement goes here
SET #NextID = ##Identity`
Here are couple good resources for getting familiar with this
http://blog.sqlauthority.com/2007/03/25/sql-server-identity-vs-scope_identity-vs-ident_current-retrieve-last-inserted-identity-of-record/
http://blog.sqlauthority.com/2013/03/26/sql-server-identity-fields-review-sql-queries-2012-joes-2-pros-volume-2-the-sql-query-techniques-tutorial-for-sql-server-2012/
I work using SQL Server. I have 2 tables, Animal and Cat. When I add a new cat to the database, I want to update both tables. I should add the cat to the Animal table first, so that I can add the animal_Id to the Cat table afterwards.
Is there a way to add the record at two tables at the same time? If there isn't, what is the best way to do it?
I just want an idea.
If you use a transaction, both inserts will be done, at least logically, "at the same time".
That means that no other query, done from outside of the transaction, can see the base "between the inserts". And if there is a failure between both inserts (and no effective commit), the final state will ignore first insert.
In order to get the id of a row just added in your session, use SCOPE_IDENTITY.
You can't use INSERT against two tables in one statement.
SET XACT_ABORT ON
BEGIN TRANSACTION
INSERT INTO [A](...) VALUES(...);
INSERT INTO [B](...) VALUES(...);
COMMIT TRANSACTION
SET XACT_ABORT OFF
The transaction is to make sure it is everything or nothing is committed. The XACT_ABORT ensures that if one fails with an error (therefore COMMIT TRANSACTION will not fire), the transaction will be forced to roll back.
I would suggest to use transaction here. For example (if you know the Id of new row beforehand):
DECLARE #CAT TABLE(id int, name varchar(50));
DECLARE #ANIMAL TABLE(id int);
DECLARE #anmalId INT = 1;
BEGIN TRAN
INSERT INTO #ANIMAL VALUES(#anmalId);
INSERT INTO #CAT VALUES(#anmalId, 'Kitty');
COMMIT TRAN
SELECT * FROM #CAT;
SELECT * FROM #ANIMAL;
You can use ##identity in case of auto increments.
Use triggers. That is the best way
how about using trigger while insertion to one table??