Move from dictionary <int, StringBuilder> to SqlParameter table value. How?
I have some code that has a dictionary defined as:
Dictionary<int, StringBuilder> invoiceDict = new Dictionary<int, StringBuilder>();
Each value in each KeyValuePair Dictionary is actually three separate values, currently created as follows:
invoiceDict.Add(pdfCount+i, new StringBuilder(invoiceID.Groups[1].ToString() + "|" + extractFileName + "|" + pdfPair.Key));
As you can see, the three values ββare separated by '|'. The number of "rows" in a dictionary can range from 600 to 1200. I would like to use the table-valued parameter to get it all in my SQL Server 2008 database in one operation.
The parameter defined by the table is defined as follows:
CREATE TYPE dbo.BatchSplitterInvoices AS TABLE
(
InvoiceID varchar(24),
NotePath varchar(512),
BatchID varchar(50)
)
CREATE PROCEDURE dbo.cms_createBatchSplitterInvoices (
@Invoices dbo.BatchSplitterInvoices READONLY,
@StaffID int
)
What's the best way to get from the Dictionary what can be passed to a table parameter? Should I be using something other than Dictionary? Or do I need to parse the dictionary into a different, better data structure?
SqlParameter invoicesParam = cmd.Parameters.AddWithValue("@Invoices", invoiceDict.Values);
invoicesParam.SqlDbType = SqlDbType.Structured;
Thank.
source to share
Both @narayanamarthi and @ChrisGessler are on the right track regarding using an interface IEnumerable<SqlDataRecord>
instead of a DataTable. Copying a collection to a DataTable just wastes CPU and memory on no gain. But an iterator can be decoupled from a collection.
So, some notes:
- Don't use s
AddWithValue
when creatingSqlParameter
. It's just a bad grade. - Don't concatenate the 3 different values ββyou want in TVP, String or StringBuilder. The whole purpose of TVP is to pass a strongly typed record, so serializing the fields to a CSV list is defective. Use the Chris recommended Invoice class instead.
- Replace
invoiceDict.Add(...)
withList<Invoice>.Add(new Invoice(...));
-
Create a method to iterate over the collection:
private static IEnumerable<SqlDataRecord> SendRows(List<Invoice> Invoices) { SqlMetaData[] _TvpSchema = new SqlMetaData[] { new SqlMetaData("InvoiceID", SqlDbType.Int), new SqlMetaData("NotePath", SqlDbType.VarChar, 512), new SqlMetaData("BatchID", SqlDbType.VarChar, 50) }; SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema); foreach(Invoice _Invoice in Invoices) { _DataRecord.SetInt32(0, _Invoice.Id); _DataRecord.SetString(1, _Invoice.NotePath); _DataRecord.SetString(2, _Invoice.BatchId); yield return _DataRecord; } }
-
Declare the parameter like this:
SqlParameter _InvoiceParam = cmd.Parameters.Add("@Invoices", SqlDbType.Structured); _InvoiceParam.Value = SendRows(Invoices);
I have additional notes and links in the following two answers regarding FST in general:
source to share
SqlBulkCopy will dispatch it in one operation, but you will need to pass a DataTable or IDataReader to it instead of a dictionary <int, StringBuilder> and you will also need to point directly to the table instead of using a stored procedure.
source to share
Implementing the IEnumerable interface in a class that contains your collection and then explicitly implements the IEnemerable's GetEnumerator method will serve the purpose. There is an article http://lennilobel.wordpress.com/2009/07/29/sql-server-2008-table-valued-parameters-and-c-custom-iterators-a-match-made-in-heaven/ . Please walk through
source to share
You need to implement IEnumerable<SqlDataRecord>
in a custom collection object. In your case, you must change Dictionary<int, StringBuilder>
to List<Invoice>
orList<Tuple<int, string, int>>
For example:
class Invoice
{
public int Id { get; set; }
public string NotePath { get; set; }
public int BatchId { get; set; }
}
class InvoiceCollection : List<Invoice>, IEnumerable<SqlDataRecord>
{
IEnumerator<SqlDataRecord> IEnumerable<SqlDataRecord>.GetEnumerator()
{
SqlDataRecord r - new SqlDataRecord(
new SqlMetaData("InvoiceId", SqlDataType.Int),
new SqlMetaData("NotePath", SqlDataType.VarChar),
new SqlMetaData("BatchId", SqlDataType.Int)
);
foreach(var item in this)
{
r.SetInt32(0, item.Id);
r.SetString(1, item.NotePath);
r.SetInt32(2, item.BatchId);
yield return r;
}
}
Then just go to the custom list SqlParameter
:
SqlCommand cmd = new SqlCommand("dbo.InvoiceInsUpd", connection);
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter sqlParam = cmd.Parameters.AddWithValue("@Invoices", invoices);
sqlParam.SqlDbType = SqlDbType.Structured;
source to share