Inserting DataTable & individual parameters into a table using stored procedure - sql

I'm trying to update/insert a SQL table using a stored procedure. Its inputs are a DataTable and other individual parameters.
EmployeeDetails table:
ID | Name | Address | Operation | Salary
---+-------+------------+-------------+------------
1 | Jeff | Boston, MA | Marketing | 95000.00
2 | Cody | Denver, CO | Sales | 91000.00
Syntax for user-defined table type (DataTable):
CREATE TYPE EmpType AS TABLE
(
ID INT,
Name VARCHAR(3000),
Address VARCHAR(8000),
Operation SMALLINT
)
Procedure for the operation:
ALTER PROCEDURE spEmpDetails
#Salary Decimal(10,2),
#Details EmpType READONLY
AS
BEGIN
UPDATE e
SET e.Name = d.Name,
e.Address = d.Address
FROM EmployeeDetails e, #Details d
WHERE d.ID = e.ID
--For inserting the new records in the table
INSERT INTO EmployeeDetails(ID, Name, Address)
SELECT ID, Name, Address
FROM #Details;
END
This procedure spEmpDetails gets its inputs as individual parameter #Salary and a DataTable #Details. Using these inputs, I'm trying to update/unsert the EmployeeDetails table. But, I failed to join these inputs together in the update/insert statement. In the above code, I'm only using the #Details DataTable data to update the EmployeeDetails table and I'm missing the #Salary to update in the table.
I'm looking for some suggestions on how to do it. Any suggestion will be greatly appreciated.

...but the input data table also gets one record at a time...
That's a dangerous assumption, even if you control the data table being sent to the stored procedure now. One day in the future you might be replaced, or someone else might want to use this stored procedure - and since the procedure itself have no built in protection against having multiple records in the data table - it's just a bug waiting to happen.
If you only need one record to be passed into the stored procedure, don't use a table valued parameter to begin with - instead, make all the parameters scalar.
Not only will it be safer, it would also convey the intent of the stored procedure better, and therefor make it easier to maintain.
If you want the stored procedure to be able to handle multiple records, add a salary column to the table valued parameter, and remove the #salary scalar parameter.
Having said that, there are a other problems in your stored procedure:
There's no where clause in the insert...select statement - meaning you'll either insert all the records in the table valued parameter or fail with a unique constraint violation.
You're using an implicit join in your update statement. It might not be a big problem when you only use inner join with two tables, but explicit joins made it to SQL-92 with good reasons - since they provide better readability and more importantly, better compilation checks. For more information, read Aaron Bertrand's Bad Habits to Kick : Using old-style JOINs
So, how to properly write an "upsert" procedure? Well, Aaron have written about that as well - right here in StackOverflow.
However, there are valid use-cases where you do want to combine inputs from both a table valued parameter and scalar variables - and the way you do that is very simple.
For an update:
UPDATE target
SET Column1 = source.Column1,
Column2 = source.Column2,
Column3 = #ScalarVariable
FROM TargetTable As target
JOIN #TVP As source
ON target.Id = source.Id -- or whatever join condition
And for an insert:
INSERT INTO TargetTable(Column1, Column2, Column3)
SELECT Column1, Column2, #ScalarVariable
FROM #TVP

I think you're looking for something like this. By setting XACT_ABORT ON when there are 2 DML statements within a block then both will rollback completely if an exception is thrown. To ensure only new records are inserted an OUTPUT clause was added to the UPDATE statement in order to identify the ID's affected. The INSERT statement excludes ID's which were UPDATE'ed.
This situation is a little different from Aaron Bertrand's excellent answer. In that case there was only a single row being upserted and Aaron wisely checks to see if the UPDATE affected a row (by checking ##rowcount) before allowing the INSERT to happen. In this case the UDT could contain many rows so both UPDATE's and INSERT's are possible.
ALTER PROCEDURE spEmpDetails
#Salary Decimal(10,2),
#Details EmpType READONLY
AS
set nocount on;
set xact_abort on;
--set transaction isolation level serializable; /* please look into */
begin transaction
begin try
declare #e table(ID int unique not null);
UPDATE e
SET e.Name = d.Name,
e.Address = d.Address,
e.Salary = #Salary
output inserted.ID into #e
FROM EmployeeDetails e,
join #Details d on e.ID=d.ID;
--For inserting the new records in the table
INSERT INTO EmployeeDetails(ID, Name, Address, Operation, Salary)
SELECT ID, Name, Address, Operation, #Salary
FROM #Details d
where not exists(select 1
from EmployeeDetails e
where d.ID=e.ID);
commit transaction;
end try
begin catch
/* logging / raiserror / throw */
rollback transaction;
end catch
go

Related

How can I best create an employee AND certifications record in my tables?

I'm designing software that could potentially be used by several people at once.
My program will have the ability to "create an Employee," which entails filling out a form that is then used in a SQL query to insert a record into Employee and EmployeeCertifications.
Right now, Employee.id is a primary key and identity column. I have two stored procedures, Employee_CreateEmployee (which inserts a record into Employee) and Employee_CreateCertifications (which inserts a record into EmployeeCertifications with a provided emp_id).
I'm in the process of integrating these stored procedures into the employee creation process in my software, but am faced with a potential issue of more than one user trying to create an employee at a given moment. I was originally thinking of having my program execute Employee_CreateEmployee, then running a query to get the highest id (most recently created employee), and using the result for procedure Employee_CreateCertifications.
Is there a better way to go about this? I have thought about potentially using a transaction for all of these queries and executions but do not know if this will also leave room for error.
Use scope_identity to obtain the latest ID inserted and use an output parameter on your first stored procedure to return the new ID for use by the second:
create procedure dbo.Test1
(
#Input1 nvarchar(128)
-- ...
, #NewId int out
)
as
begin
set nocount, xact_abort on;
insert into dbo.MyTable (Column1 /* 2, 3... */)
select #Input1; -- #Input2, #Input3 ...
set #NewId = scope_identity();
return 0;
end;
Then call as:
exec dbo.Test1 #Input1, #NewId out;
exec dbo.Test2 #NewId, #OtherInput1;

Manually Checking of Value Changes in Tables for SQL

An example to the problem:
There are 3 columns present in my SQL database.
+-------------+------------------+-------------------+
| id(integer) | age(varchar(20)) | name(varchar(20)) |
+-------------+------------------+-------------------+
There are a 100 rows of different ids, ages and names. However, since many people update the database, age and name constantly change.
However, there are some boundaries to age and name:
Age has to be an integer and has to be greater than 0.
Name has to be alphabets and not numbers.
The problem is a script to check if the change of values is within the boundaries. For example, if age = -1 or Name = 1 , these values are out of the boundaries.
Right now, there is a script that does insert * into newtable where age < 0 and isnumeric(age) = 0 or isnumeric(name) = 0;
The compiled new table has rows of data that have values that are out of the boundary.
I was wondering if there is a more efficient method to do such checking in SQL. Also, i'm using microsoft sql server, so i was wondering if it is more efficient to use other languages such as C# or python to solve this issue.
You can apply check constraint. Replace 'myTable' with your table name. 'AgeCheck' and 'NameCheck' are names of the constraints. And AGE is the name of your AGE column.
ALTER TABLE myTable
ADD CONSTRAINT AgeCheck CHECK(AGE > 0 )
ALTER TABLE myTable
ADD CONSTRAINT NameCheck CHECK ([Name] NOT LIKE '%[^A-Z]%')
See more on Create Check Constraints
If you want to automatically insert the invalid data into a new table, you can create AFTER INSERT Trigger. I have given snippet for your reference. You can expand the same with additional logic for name check.
Generally, triggers are discouraged, as they make the transaction lengthier. If you want to avoid the trigger, you can have a sql agent job to do auditing on regular basis.
CREATE TRIGGER AfterINSERTTrigger on [Employee]
FOR INSERT
AS
BEGIN
DECLARE #Age TINYINT, #Id INT, Name VARCHAR(20);
SELECT #Id = ins.Id FROM INSERTED ins;
SELECT #Age = ins.Age FROM INSERTED ins;
SELECT #Name = ins.Name FROM INSERTED ins;
IF (#Age = 0)
BEGIN
INSERT INTO [EmployeeAudit](
[ID]
,[Name]
,[Age])
VALUES (#ID,
#Name,
#Age);
END
END
GO

what is the proper way to write this sql for insert?

I have an ADGroup table in my DB which has columns Id, Guid. The Guid column represents the Guid attribute of external Active Directory groups.
I have an ADGroupADGroup table in my DB which has columns ParentADGroupId, ChildADGroupId. These columns represent parent/child instances of the ADGroup.Id column in the ADGroup table.
I have a sproc which uses a Table-Valued Parameter. The TVP has columns ParentADGroupGuid and ChildADGroupGuid, both with a UNIQUEIDENTIFIER data type. These columns represent parent/child Group Guid relationships in AD.
I've inserted ADGroup data into my DB and now I need to insert ADGroupADGroup data with the sproc below. What would be the proper way to write the select statement for insert in the "/* select statement here */" section below?:
CREATE PROCEDURE InsertADGroupGroups
-- Add the parameters for the stored procedure here
#ADGroupADGroupParameter ADGroupADGroupParameter READONLY
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
INSERT INTO ADGroupADGroup
(
ParentADGroupId,
ChildADGroupId
)
/* select statement here */
END
UPDATE
Here's some sample SQL that would get the proper ADGroup.Id to insert for the ParentADGroupGuid in the TVP:
-- get ADGroup.Id for the AD Group Guid in the tvp
SELECT adg.Id
FROM ADGroup adg
JOIN ADGroupADGroupParameter tvp ON tvp.ParentADGroupGuid = adg.Guid
So now I need to figure out a streamlined way to update this query to also include the ADGroup.Id for the ChildADGroupGuid in the TVP
You can treat a table valued parameter just like a regular table so a basic select statement will work.
select ParentADGroupId,
ChildADGroupId
from #ADGroupADGroupParameter
I think the following SQL might do the trick:
INSERT INTO ADGroupADGroup
(
ParentADGroupId,
ChildADGroupId
)
SELECT adg1.Id, adg2.Id
FROM ADGroup adg1
JOIN #ADGroupADGroupParameter tvp ON tvp.ParentADGroupGuid = adg1.Guid
JOIN ADGroup adg2 ON tvp.ChildADGroupGuid = adg2.Guid
Testing now....

SQL trigger for update

I was just trying to figure out how to do a basic trigger when I updated a row
Heres the setup
CREATE TABLE marriage(
personid int
married varchar(20)
);
INSERT INTO marriage
values (1, unmarried);
What im trying to do is create a sql trigger that will make it so that when I update a person can only go from married to divorced but not unmarried to divorced.
If anyone can help me with structuring this that would be great
This is what I was looking for if someone was looking for something similar
alter trigger
trigtest3
on married
for update
as
begin
declare #old varchar(20)
declare #new varchar(20)
select #old = married from deleted
select #new = married from inserted
if(#old like 'Unmarried' AND #new like 'Divorced')
rollback
end
SQL Server doesn't provide per-row triggers unfortunately, but only triggers for a complete command. And one single update command can update several rows, so you must look whether at least one affected row has undergone a forbidden change. You do this by joining the deleted and inserted pseudo tables on a column or a combination of columns that uniquely identify a record (i.e. the primary key).
create trigger trg_upd_married on marriage for update as
begin
declare #error_count int
select #error_count = count(*)
from deleted d
join inserted i on i.id = d.id
where d.married = 'Unmarried'
where i.married = 'Divorced'
if #error_count > 0
begin
raiserror('Unmarried persons cannot get divorced.', 16, 121)
rollback transaction
end
end;
The above trigger may still have errors. I am not fluent with TSQL (and just notice that I find its triggers quite clumsy - at least compared to Oracle's triggers I am used to).
You need to use instead of triggers as you need to prevent update. For update triggers are run after the insert happens. Use the following code -
create trigger abc on marriage
for instead of update
as
begin
Begin transaction
if exists(select 1 from deleted as a
inner join inserted as b
on a.personid = b.personid
where a.married = 'unmarried' and b.married = 'Divorced')
begin
raiserror('Status can not be changed from unmarried to Divorced',16,1)
Rollback transaction
end
else
begin
update a
set a.married = b.married
from marriage as a
inner join inserted as b
on a.personid = b.personid
Commit transaction
end
end
Let me know if this helps

SQL IF/Case in stored procedure

I am trying to create procedure which will insert two values in my Pickup table.
create procedure sp_InsertPickup
#ClientID int,
#PickupDate date
as
insert into Pickup (ClientID ,PickupDate )values (#ClientID,#PickupDate)
However I need check if this client already did pickup in this month (record in table)it should not insert any new records it table.
example if this data in table
ClientID PickupDate
11 03-01-2013
And I want insert ClientId 11 and new PickupDate 03-24-2013 it should just not insert because this person already did pickup this month.
Any Ideas how to implement it ?
So in that case, use a IF NOT EXISTS:
CREATE PROCEDURE dbo.InsertPickup
#ClientID int,
#PickupDate date
AS
IF NOT EXISTS (SELECT * FROM Pickup
WHERE ClientID = #ClientID
AND MONTH(PickupDate) = MONTH(#PickupDate)
AND YEAR(PickupDate) = YEAR(#PickupDate) )
INSERT INTO Pickup (ClientID, PickupDate)
VALUES (#ClientID, #PickupDate)
You might want to somehow indicate to the caller that there was no data inserted, due to the fact it already exists....
As a side note: you should not use the sp_ prefix for your stored procedures. Microsoft has reserved that prefix for its own use (see Naming Stored Procedures), and you do run the risk of a name clash sometime in the future. It's also bad for your stored procedure performance. It's best to just simply avoid sp_ and use something else as a prefix - or no prefix at all!
The safest way to do this is to either use merge or to put a constraint on the table and trap for the error.
The reason merge is safer is because it is an atomic transaction. Checking for existence and then doing the insert is dangerous, because someone else might have already inserted (or deleted) the row. You can start playing with transaction semantics in the stored procedure, but why bother when SQL Server provides merge:
merge Pickup as target
using (select #PickupDate, #ClientId) as source(PickupDate, ClientId)
on target.clientId = source.ClientId and year(source.PickupDate) = year(target.PickupDate) and month(source.PickupDate) = month(target.PickupDate)
when NOT MATCHED then
insert(PickupDate, ClientId) values(source.PickupDate, source,ClientId);
You can read more about merge an.
This is how you can implement it using IF NOT EXISTS
create procedure sp_InsertPickup
#ClientID int,
#PickupDate date
as
IF NOT EXISTS (SELECT * FROM Pickup
WHERE ClientID = #ClientID
AND DATEPART(mm,PickupDate) = DATEPART(mm,#PickupDate)
AND DATEPART(yy,PickupDate) = DATEPART(yy,#PickupDate))
BEGIN
insert into Pickup (ClientID ,PickupDate )values (#ClientID,#PickupDate)
END
begin
end