TSQL: Insert/Update with multiple tables/scope_identity() - sql

I have two tabs on my front end collecting information for Addresses and Users. Currently, my stored procedures are called on the final submit button. My Addresses table has an ID (primary key), Line1, and Line2 column. My Users table has an ID (primary key) FirstName, LastName and AddressID column (which should map to the ID column in the addresses table). My address insert proc looks like this:
ALTER PROC [dbo].[Addresses_Insert]
#Line1 nvarchar(50),
#Line2 nvarchar (50), = null
#ID = INT output
As
BEGIN
INSERT INTO [dbo].[Addresses]
(
[Line1],
[Line2]
)
VALUES
(#Line1, #Line2)
SET #Id = SCOPE_IDENTITY()
END
My Users update looks like this:
ALTER PROC [dbo].[Users_Update]
#Id int
,#FirstName nvarchar(50) = null
,#LastName nvarchar(50) = null
,#AddressId INT = null
AS
BEGIN
UPDATE dbo.Users
SET
[FirstName] = #FirstName
,[LastName] = #LastName
,[AddressId] = #AddressId --not sure about this
WHERE Id = #Id
I need to get the scope_identity() from the address stored procedure into the AddressID column for my Users table, but I'm not sure how to make that work.

When you add your parameters in your code, set the #Id parameter as an output parameter and check the parameter value after your procedure executes:
SqlParameter x = new SqlParameter();
x.Direction = System.Data.ParameterDirection.Output;
or you can just return the value from your #Id parameter like this:
SET #Id = SCOPE_IDENTITY();
Return #Id

Related

Is there any way to exec a stored procedure for all selected data rows?

I'm setting up a storekeeping program in which I have 2 tables, one for products and another for materials.
In the products table, each product has several materials. Is there any way to select these rows and decrement materials availability?
I tried to use a foreach loop but I couldn't implement it and store each rows data
CREATE TABLE materials
(
materialID INT PRIMARY KEY IDENTITY,
materialName NVARCHAR(100) NULL,
materialAmount INT NULL,
)
CREATE TABLE productStack
(
ID INT PRIMARY KEY IDENTITY,
productsID INT NULL,
materialID INT NULL,
amount INT NULL
)
GO;
CREATE PROCEDURE updateMaterials
(#ID INT,
#AMOUNT INT)
AS
BEGIN
UPDATE materials
SET materialAmount = (materialAmount - #AMOUNT)
WHERE materialID = #ID
END
You could use a temp table and while loop such as :
SELECT * INTO #TEMP_A FROM PRODUCTSTACK
DECLARE #ID INT,#AMOUNT INT
WHILE EXISTS(SELECT * FROM #TEMP_A)
BEGIN
SET #ID = (SELECT TOP 1 ID FROM PRODUCTSTACK)
SET #AMOUNT = (SELECT TOP 1 AMOUNT FROM PRODUCTSTACK WHERE ID = #ID)
EXEC UPDATEMATERIALS #ID,#AMOUNT
DELETE FROM #TEMP_A WHERE ID = #ID
END
As we have no sample to base this on, this is a guess. Like I said, however, seems like a table-type parameter would do this:
CREATE TYPE dbo.MaterialAmount AS table (ID int, Amount int);
GO
CREATE PROC dbo.UpdateMaterials #Materials dbo.MaterialAmount READONLY AS
BEGIN
UPDATE M
SET materialAmount = MA.Amount
FROM dbo.materials M
JOIN #Materials MA ON M.materialID = MA.ID;
END;
GO
--Example of usage:
DECLARE #Materials dbo.MaterialAmount;
INSERT INTO #Materials
VALUES(1,100),
(5,20);
EXEC dbo.UpdateMaterials #Materials;

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

SQL Server Stored Procedure - Return varchar id of inserted row

I'm working on a cascading insertion where a stored procedure should return an id of the inserted row. This would not be a problem if the id of the table was an int. However, the id is a varchar and therefore I cannot use SCOPE_IDENTITY().
This is my procedure so far:
CREATE PROCEDURE NEW_ARTICLE
#id varchar(50) OUTPUT,
#name varchar(100),
#articleNr varchar(50),
#gategory varchar(50),
#containerId varchar(50),
#contPerContainer int,
#pictureId varchar(50)
AS
SET NOCOUNT OFF
INSERT INTO nextlabel.[Article] (name, article_nr, category, container_id, count_per_container, picture_id )
VALUES (#name, #articleNr, #gategory, #containerId, #contPerContainer, #pictureId)
SET #id = SCOPE_IDENTITY()
Where the last row is not correct since the column id is a varchar.
How can I return the id?
Try this:
CREATE PROCEDURE NEW_ARTICLE
#id varchar(50) OUTPUT,
#name varchar(100),
#articleNr varchar(50),
#gategory varchar(50),
#containerId varchar(50),
#contPerContainer int,
#pictureId varchar(50)
AS
SET NOCOUNT OFF
SET #id = newid()
INSERT INTO nextlabel.[Article] (id, name, article_nr, category, container_id, count_per_container, picture_id)
VALUES (#id, #name, #articleNr, #gategory, #containerId, #contPerContainer, #pictureId)
GO

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.

Update the table using stored procedure

I have a table and I have 50 columns in it. From my code behind at first I am inserting 10 values using a stored procedure, after that in second page based on userid I want to update other 40 columns. So I am updating the table and the userid column of the table is an identity column which is auto incremented so how to get the user id for update stored procedure?
CREATE PROCEDURE sp_update
(#FormFiledBy varchar(50), #MaritalStatus varchar(50),
#Height varchar(50), #Religion varchar(50), #Caste varchar(100),
#MotherTongue varchar(50), #Education varchar(100),
#Occupation varchar(50), #CountryofResidence varchar(50),
#EducationDetails varchar(100), #AnnualIncome varchar(50),
#CountryOfBirth varchar(50), #BirthPlace varchar(50),
#TimeOfBirth nchar(10), #StarSign varchar(100),
#Gothram varchar(50), #Rassi varchar(50), #HavinChildren varchar(10),
#PhysicalStatus varchar (100)
)
AS
BEGIN
UPDATE Profile_Master
SET FormFiledBy = #FormFiledBy,
MaritalStatus = #MaritalStatus,
Height = #Height,
physicalStatus = #physicalStatus,
Religion = #Religion,
Caste = #Caste,
MotherTongue = #MotherTongue,
Education = #Education,
Occupation = #Occupation,
CountryofResidence = #CountryofResidence,
EducationDetails = #EducationDetails,
AnnualIncome = #AnnualIncome,
CountryOfBirth = #CountryOfBirth,
BirthPlace = #BirthPlace,
TimeOfBirth = #TimeOfBirth,
StarSign = #StarSign,
Gothram = #Gothram,
Rassi = #Rassi,
HavinChildren = #HavinChildren,
PhysicalStatus = #PhysicalStatus
WHERE
????
END
In your initial procedure, when you insert the data, you should retrieve and return the userid value in an OUTPUT parameter so that you can then supply it to your update procedure.
You can use SCOPE_IDENTITY() for this fairly easily.
As requested, some sample code (simplified, but you should see the pattern):
CREATE PROCEDURE add_row #data1 varchar(20), #data2 varchar(20), #id int OUTPUT
AS
BEGIN
INSERT INTO Table (Data1, Data2) VALUES (#data1, #data2)
SELECT #id = SCOPE_IDENTITY()
END
GO
CREATE PROCEDURE update_row #id int, #data3 varchar(20), #data4 varchar(20)
AS
BEGIN
UPDATE Table SET Data3 = #data3, Data4 = #data4
WHERE Id = #id
END
GO
you don't need update your primary key, but you must pass UserId as parameter of your stored procedure.
Update Profile_Master
Set ....
Where UserId = #UserId --your parameter
Nota : yuo must ensure that your primary key is just UserId
You just have to pass SCOPE_IDENTITY() from fist Stored Procedure as below:
#Id int OUTPUT,
---
SET #Id = SCOPE_IDENTITY()
And in the second Stored Procedure Apply the Id as an argument an Update the record based on that.
#Id VARCHAR(15)
Update Profile_Master
set ....
........
where UserId = #Id