Blog Archives

Passing Multiple Records to a Stored Procedure in SQL Server

There are so many different ways to work with data that it can make your head spin.  Regardless of the technology, one thing I always try to do when working with databases is minimize the number of calls that are made.  I use ORM frameworks a lot so I have to make sure that my code doesn’t end up calling the database a ton and being overly “chatty”. 

My company is currently working on a large Silverlight 3 application for a customer and have a requirement to allow CSV file uploads through the application.  That’s pretty straightforward to do with Silverlight but in looking through the CSV file there can be 1000s of records and there is a lot of custom functionality around each record.  Initially I was going to handle the business rules and insertion for each row individually.  However, once I saw how many rows were involved I backed off my initial approach since that would mean a single file upload could trigger thousands and thousands of calls to the database which certainly isn’t efficient.  Instead of going that route I decided it would be better to pass all of the rows into a stored procedure and let it handle everything.  That way I can make a single database connection yet handle inserting (and updating and deleting based upon the business rules) many records. 

So how can you pass multiple records into a stored procedure?  Back in “the day” I’d pass in delimited strings and then parse them.  Although that technique probably wasn’t the most efficient, it got the job done.  With SQL Server 2005 and higher we have access to a much more efficient technique due to the availability of XQuery.  The current project I’m working on relies on SQL Server 2005 behind the scenes so that’s what I’ll focus on here.  Note that SQL Server 2008 allows .NET DataTable objects to be passed when using table value parameters as well in a stored procedure.  See www.codeproject.com/KB/database/sqlserver2008.aspx for an overview of the table value parameter functionality.

For the import I can serialize the list of records to XML and then pass them into a stored procedure.  It appears that you can pass an XmlDocument object from .NET if the stored procedure takes an XML input parameter type.  However, I never got it to work properly doing that so I pass a string containing serialized XML data and then load it into the XML data type.  The stored procedure can then use XQuery to convert the XML into actual rows.  Here’s some code that handles parsing a flat file, converting the data into a List<JobMaterialImport>, serializing the list into XML and then passing the serialized XML data to a stored procedure named ImportJobMaterials.

public List<ImportJobMaterialsResult> ImportJobMaterials(int jobID, Stream stream, string coNumber, string coDesc, string xmlMapFile)
{
    //Convert flat file to List<JobMaterialImport> using converter and XML mapping file
    //Mapping file passed from FlatFileHandler.ashx in web project
    FlatFileToObjectConverter<JobMaterialImport> converter = new FlatFileToObjectConverter<JobMaterialImport>(stream, xmlMapFile);
    List<JobMaterialImport> imports = converter.ConvertToList();
    imports = imports.Take(imports.Count - 2).ToList(); //last rows are bogus in flat-file

    //Generate XML and pass to sproc
    using (StringWriter sw = new StringWriter())
    {
        XmlSerializer xs = new XmlSerializer(typeof(List<JobMaterialImport>));
        xs.Serialize(sw, imports);
        try
        {
            //Pass materials into sproc
            string xml = sw.ToString().Replace("utf-16", "utf-8");
            return this.DataContext.ImportJobMaterials(xml, jobID, coNumber, coDesc).ToList();
        }
        catch (Exception exp)
        {
            Logger.Log("Error in JobManagementRepository.ImportJobMaterials", exp);
        }
    }
    return null;
}

 

The XML that’s generated from the code above looks like the following.  Security Note: We’ll assume that appropriate measures have been taken to clean the data and ensure it doesn’t contain any bad stuff related to injection attacks. That’s especially important if you’ll be running any dynamic SQL in your stored procedure.  I’m not, but I thought I’d be a good citizen and point it out…always sanitize your data with some Clorox before using it. :-)

<ArrayOfJobMaterialImport xmlns:xsi="www.w3.org/2001/XMLSchema-instance" xmlns:xsd="www.w3.org/2001/XMLSchema">
  <JobMaterialImport>
    <Area>BUILDING A</Area>
    <Phase>LIGHTING</Phase>
    <WorkCode>0</WorkCode>
    <WorkCodeTitle>Manually Assigned</WorkCodeTitle>
    <Description>4x1 1/2in. SQ BOX COMB KO</Description>
    <Quantity>2</Quantity>
    <TotalHours>10.46</TotalHours>
  </JobMaterialImport>
  <JobMaterialImport>
    <Area>BUILDING A</Area>
    <Phase>LIGHTING</Phase>
    <WorkCode>0</WorkCode>
    <WorkCodeTitle>Manually Assigned</WorkCodeTitle>
    <Description>#8x   3/4 P/H SELF-TAP SCREW</Description>
    <Quantity>4</Quantity>
    <TotalHours>0.28</TotalHours>
  </JobMaterialImport>
  <JobMaterialImport>
    <Area>BUILDING A</Area>
    <Phase>LIGHTING</Phase>
    <WorkCode>605</WorkCode>
    <WorkCodeTitle>Wiring and System Devices</WorkCodeTitle>
    <Description>1G TGL SWITCH PLATE - PLASTIC IVY</Description>
    <Quantity>2</Quantity>
    <TotalHours>0.89</TotalHours>
  </JobMaterialImport>
</ArrayOfJobMaterialImport>

 

Here’s what the stored procedure looks like:

 

CREATE PROCEDURE [ImportJobMaterials]
    @JobMaterialsXml AS VARCHAR(MAX),
    @JobID AS INT,
    @ChangeOrderNumber AS VARCHAR(10) = NULL,
    @ChangeOrderDescription AS VARCHAR(100) = NULL
AS
    BEGIN
        DECLARE @XML AS XML

        DECLARE @MaterialsTable TABLE
        (
            ID INT IDENTITY(1,1),
            Area VARCHAR(250),
            Phase VARCHAR(250),
            WorkCodeID INT,
            WorkCodeTitle VARCHAR(250),
            MaterialTitle VARCHAR(250),
            Quantity DECIMAL(18,2),
            TotalHours DECIMAL(18,2)
        )

        SELECT @XML = @JobMaterialsXml

        INSERT INTO @MaterialsTable (Area, Phase, WorkCodeID, WorkCodeTitle, MaterialTitle, Quantity, TotalHours)
        SELECT M.Item.query('./Area').value('.','VARCHAR(250)') Area,
               M.Item.query('./Phase').value('.','VARCHAR(250)') WorkCode,
               M.Item.query('./WorkCodeID').value('.','INT') WorkCodeID,
               M.Item.query('./WorkCodeTitle').value('.','VARCHAR(250)') WorkCodeTitle,
               M.Item.query('./MaterialTitle').value('.','VARCHAR(250)') MaterialTitle,
               M.Item.query('./Quantity').value('.','DECIMAL(18,2)') Quantity,
               M.Item.query('./TotalHours').value('.','DECIMAL(18,2)') TotalHours
        FROM @XML.nodes('/ArrayOfJobMaterialImport/JobMaterialImport') AS M(Item)

        --Process the data        
    END

Once the XML data comes in it’s converted into an XML data type using SELECT @XML = @JobMaterialsXml syntax.  The key part of the T-SQL code is the SELECT statement that grabs each value from the XML data type and looks for specific child nodes.  If the child nodes were attributes instead then you would do something like M.Item.value(‘@attributeName’,’DBType’).  Each JobMaterialImport node in the XML is located by the @XML.nodes(‘/ArrayOfJobMaterialImport/JobMaterialImport’) AS M(Item) code.  What’s nice about this approach is that a single call can be made to the database yet 1000s of records can be processed.  Not optimal for every situation, but exactly what I needed and fairly straightforward to use.

Pivots to the Rescue – Providing Flexible SQL Server Queries

image One of the projects my company is currently working on has a requirement to be able to store timesheet data in a more flexible manner.  The existing Access forms application is being converted to Silverlight 3 and has a fairly rigid (albeit standard) database structure for timesheets.  Hours and quantities are associated directly with days using columns such as MonHours, MonQuant, etc.  (see the table image to the right).  The catch is that the company needs to be able to store several other pieces of data in the future.  Of course, if they need a new column that tracks overtime, they’d have to modify the database table and add in 7 new columns if the existing table structure is used which definitely isn’t optimal.

After thinking through a few options we decided to go with a more complex yet flexible solution.  It’s only useful (in my opinion anyway) when the data that will be collected can change in the future.  I’m not a big fan of architecting for “what if” type scenarios since in my experience most of those scenarios are never realized, but in this situation it was a requirement to be able to handle different types of timesheet data without having to re-work the table structure and add new columns.  We ended up going with the following table structure to store timesheet values:

image

Here’s a quick overview of what the different tables store:

  • TimeSheet – Associates a job, employee and week ending date together
  • TimeSheet_WorkCode – Associates work codes with a timesheet
  • TimeSheetDayValue – This is where the values are actually stored.  It can store hours, overtime, quantity or anything else really.  The DayValue column stores the actual value and the type of value being stored (hours, quantity, etc.)  is tracked by the TimeSheetDayValueTypeID which links to the TimeSheetDayValueType table.
  • TimeSheetDayValueType – This table tracks the type of data being stored in TimeSheetDayValue (currently just hours and quantity of materials installed).  The type of data is tracked in the ValueType column which is a simple varchar data type.
  • Day – This is a simple lookup table that contains all of the day names (Monday, Tuesday, Wednesday, etc.)

While the table structure provides a flexible way to store different types of timesheet data, it doesn’t store the data in a format that’s compatible with timesheets.  Normally, an employee will select a work code and then enter data for the hours worked on each day.  With the initial table structure that we inherited from the original database (image to the right) that was easy since the existing columns match up perfectly with grid header columns.  With the new table structure we gain flexibility but add complexity since we have to reshape the data to fit into a timesheet grid.

Fortunately, SQL Server 2005 or higher supports pivots which allow data to be “pivoted”.  In other words, row data can be turned into columns.  If I run the following query against the tables shown earlier I’ll get back rows but they won’t match up with Monday, Tuesday, Wednesday timesheet header columns as you can see next:

SELECT TS.TimeSheetID, TS.WeekEnding, TS.EmployeeID, TS.JobID, TSWC.WorkCodeID,
TSDV.DayValue, D.Day + TSDVT.ValueType as RotateColumn
FROM TimeSheet TS
INNER JOIN Timesheet_WorkCode TSWC on TS.TimeSheetID = TSWC.TimeSheetID
INNER JOIN TimeSheetDayValue TSDV on TSWC.TimeSheetWorkCodeID = TSDV.TimeSheetWorkCodeID
INNER JOIN TimeSheetDayValueType TSDVT on TSDV.TimeSheetDayValueTypeID = TSDVT.TimeSheetDayValueTypeID
INNER JOIN Day D on TSDV.DayID = D.DayID
WHERE TS.JobID = 1 AND EmployeeID = 1 AND WeekEnding = '5/10/2009'

image

However, by using a pivot I can make it so that the values shown above for RotateColumn are “pivoted” to be columns in the resultset so that everything fits into a single row on a timesheet grid.  There are a few tricks that have to be leveraged to make it all work such as leveraging some of the SQL Server 2005+ XML functionality but we felt it was a good tradeoff to get the data storage flexibility required by the client.  Here’s the pivot query we’re currently using and the resultset that it returns.  We’ll likely change things as the application is optimized more, but you’ll get the idea.  The query is in a stored procedure which accepts jobID, employeeID and week ending date and returns timesheet data for different work codes.

DECLARE @Cols varchar(500)
SELECT @Cols = STUFF((SELECT ',' + QUOTENAME(D.Day + TSDVT.ValueType) AS [text()]
FROM Day D
CROSS JOIN TimeSheetDayValueType TSDVT
FOR XML PATH('')), 1, 1, '')

DECLARE @Query nvarchar(MAX)
SELECT @Query = '
SELECT * FROM
(
    SELECT TS.TimeSheetID, TS.WeekEnding, TS.EmployeeID, TS.JobID, TSWC.WorkCodeID,
    TSDV.DayValue, D.Day + TSDVT.ValueType as RotateColumn
    FROM TimeSheet TS
    INNER JOIN Timesheet_WorkCode TSWC on TS.TimeSheetID = TSWC.TimeSheetID
    INNER JOIN TimeSheetDayValue TSDV on TSWC.TimeSheetWorkCodeID = TSDV.TimeSheetWorkCodeID
    INNER JOIN TimeSheetDayValueType TSDVT on TSDV.TimeSheetDayValueTypeID = TSDVT.TimeSheetDayValueTypeID
    INNER JOIN Day D on TSDV.DayID = D.DayID
    WHERE TS.JobID = ' + CONVERT(NVARCHAR,@JobID) +
    ' AND EmployeeID = ' + CONVERT(NVARCHAR,@EmployeeID) + ' AND WeekEnding = ''' + @WeekEnding + '''
) AS SourceTable
PIVOT (SUM(DayValue) FOR RotateColumn IN (' + @Cols + ')) AS TimeSheetRow'
-- FOR XML AUTO, ELEMENTS XSINIL
EXEC sp_executesql @Query

 

image


You can see that the RotateColumn values shown earlier have been changed to be columns using SQL Server’s pivot functionality which is exactly what we needed for the Silverlight 3 application that displays the data.  We had to resort to some dynamic SQL to make it work, but the end result is that we’re able to shape the data as needed yet still retain the flexibility provided by the table structure.  As new types of timesheet values are required the table structure and query can be left intact.  Pivots to the rescue…pretty nice when you need them.

WP Like Button Plugin by Free WordPress Templates