13 March 2010

Saving Data With Stored Procedures

    Without going too much into details on the benefits of stored procedures, in this post I will show a simple technique I usually like to use for database updates. It’s not rocket science, I bet many of the developers use similar approaches, and many more don’t! Stored procedures are the ideal place for storing your business logic, including insert/updates, in addition stored procedures being recommended as a way of avoiding SQL injection and facilitate code’s maintenance. Doing inserts/updates in a stored procedure implies sending each attribute that needs to be maintain, and given the fact that, with small exceptions, the same parameters are sent, it makes sense to bring the two statements in the same stored procedure. The difference in the number of parameters resides actually in the Primary Key which is sent in the update case in order to identify uniquely the record that needs to be updated, though this fact could be used to determine whether is dealt with an insert or an update.

   Enough with the talking, so let’s consider the below table in which a unique constrain is defined on ProductNumber attribute:
CREATE TABLE Production.Products(
ProductID int IDENTITY(1,1) NOT NULL,

Name nvarchar(250) NOT NULL,

ProductNumber nvarchar(25) NOT NULL,

MakeFlag bit NOT NULL,

FinishedGoodsFlag bit NOT NULL,
Color nvarchar(15) NULL,

SafetyStockLevel int NULL,
ListPrice decimal(13,2) NOT NULL,

StandardCost decimal(13,2) NOT NULL,

CreationDate smalldatetime NOT NULL,

CreatedBy int NOT NULL,

ModifiedDate smalldatetime NOT NULL,

ModifiedBy int NOT NULL)


    The table is based on Production.Product table from AdventureWorks database, however I reduced the number of attributes and simplified table’s definition. I introduced also the Creation/Modified Dates used as timestamps for the creation/modification of a record, and the Created/Modified By as a reference to the User that performed the respective actions. From all these 4 attributes is enough to send only the User ID, the actual timestamp being obtained using the GetDate function in the background, so we’ll have in the end 13-3= 10 parameters. Here is the stored procedure:
CREATE PROCEDURE dbo.pUpdateProducts(
  @ProductID int

,
@Name nvarchar(250)
,
@ProductNumber nvarchar(25)
,
@MakeFlag bit

,
@FinishedGoodsFlag bit

,
@Color nvarchar(15)
,
@SafetyStockLevel int

,
@ListPrice decimal(13,2)

,
@StandardCost decimal(13,2)

,
@StartDate smalldatetime

,
@EndDate smalldatetime

,
@UserID int)

AS

BEGIN
BEGIN
TRY

    IF ISNULL(@ProductID, 0) = 0
    BEGIN

        -- insert statement

        INSERT INTO Production.Products (Name, ProductNumber, MakeFlag, FinishedGoodsFlag, Color , SafetyStockLevel
, ListPrice, StandardCost, StartDate, EndDate, CreationDate, CreatedBy, ModifiedDate, ModifiedBy)

        VALUES ( @Name, @ProductNumber, IsNull(@MakeFlag, 0), IsNull(@FinishedGoodsFlag, 0), @Color , @SafetyStockLevel
, @ListPrice, @StandardCost, @StartDate, @EndDate, GETDATE(), @UserID, GETDATE(), @UserID)

        SET @ProductID = @@IDENTITY
     END
     ELSE

     BEGIN

          -- update statement

          UPDATE Production.Products
          SET Name = @Name
         , ProductNumber = @ProductNumber
         , MakeFlag = IsNull(@MakeFlag, 0)
 
         , FinishedGoodsFlag = IsNull(@FinishedGoodsFlag, 0)

         , Color = @Color
         , SafetyStockLevel = @SafetyStockLevel
         , ListPrice = @ListPrice
         , StandardCost = @StandardCost
        , StartDate = @StartDate
        , EndDate = @EndDate
        , ModifiedDate = GETDATE()

        , ModifiedBy = @UserID
        WHERE ProductID = @ProductID
     END

     SELECT @ProductID

END
TRY
BEGIN
CATCH

     SELECT Cast(ERROR_NUMBER() as varchar(10)) + ':' + ERROR_MESSAGE()

END
CATCH
END

    The TRY… CATCH… block introduced with SQL Server 2005 allows trapping the errors and thus returns error details to the calling application. In case the DML statement succeeded is returned the Primary Key of the just updated, respectively inserted record. Let’s see the stored procedure in action:
EXEC dbo.pUpdateProducts 0, 'Product 1', 'Product1', 1, 1, 'Blue', 100, 50.32 , 44.06, '2010-01-01', NULL, 1 -- Example 1
EXEC
dbo.pUpdateProducts 0, 'Product 2', 'Product2', 0, 1, 'Red', 100, 51.32 , 44.45, '2010-01-01', NULL, 1 -- Example 2

EXEC
dbo.pUpdateProducts 0, 'Product 3', 'Product3', 1, 0, 'Gren', 100, 59.32 , 44.87, '2010-01-01', NULL, 1 -- Example 3

EXEC
dbo.pUpdateProducts 0, 'Product 4', 'Product4', 0, 0, 'Red', 100, 57.32 , 44.78, '2010-01-01', NULL, 1 -- Example 4


    After running the above EXEC statements try to run them again, each of them will error out with the following error details: 2601:Cannot insert duplicate key row in object 'Production.Products' with unique index 'IX_Products_UniqueProductNumber'. It will be in developer’s attribution to translate the error to a more appealing error message.

    In order to update a record all is needed to do is to transmit the ID of the record to be updated and the new values. For example in a new created table the record created by first example will more likely correspond to ProductID = 1, thus the following statement is used to change its values:
EXEC dbo.pUpdateProducts 1, 'Product 1', 'Product1', 1, 1, 'White', 100, 50.32 , 44.06, '2010-01-01', NULL, 1 -- Example 1

Note:
    This technique has the inconvenience that it doesn’t consider the cases in which two different Users update the same record, though that’s a topic for another post.

No comments: