0

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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • 2
    Do you want all the employees in your table to have the same salary? If not, why is the salary a scalar value and not another column of the table valued parameter? Also, why are you using implicit joins? explicit joins are a part of the ANSI standard for 30 years now, there really is no reason to use implicit joins anymore. – Zohar Peled Oct 11 '20 at 05:38
  • @ZoharPeled - Thanks for your response. Not all the employees have the same salary but the input data table also gets one record at a time, based on the source for the Datatable. "salary" is get from a different data source to the stored proc, but I think I can add this salary to the datatable source while creating the record with other information i.e., ID, Name and address. – Sudha Vuppala Oct 11 '20 at 05:49
  • 1
    [Bad habits to kick : using old-style JOINs](https://sqlblog.org/2009/10/08/bad-habits-to-kick-using-old-style-joins) - that old-style *comma-separated list of tables* style was replaced with the *proper* ANSI `JOIN` syntax in the ANSI-**92** SQL Standard (**more than 25 years** ago) and its use is discouraged – marc_s Oct 11 '20 at 05:59
  • @marc_s. I can easily correct that but my actual question is how to insert two data source values using a Insert statement. I think we are deviating from my initial question. – Sudha Vuppala Oct 11 '20 at 06:05
  • As @ZoharPeled already mentioned: it's very unclear how the `@Salary` variable should be used here. Set the salary of all employees being updated to that value? Set the salary of all the employees being inserted to that value?? You need to clarify that ..... – marc_s Oct 11 '20 at 06:08
  • The UDT contains ID and yet is a source for INSERT new rows. What is the primary key of the EmployeeDetails table? What is the ID value in the UDT when the row is a new value? – SteveC Oct 11 '20 at 13:25

2 Answers2

2

...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
Zohar Peled
  • 79,642
  • 10
  • 69
  • 121
0

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
SteveC
  • 5,955
  • 2
  • 11
  • 24