Pregunta

Tengo un código que tiene un Diccionario definido como:

Dictionary<int, StringBuilder> invoiceDict = new Dictionary<int, StringBuilder>();

Cada valor en cada KeyValuePair the Dictionary es en realidad tres valores separados actualmente creados de la siguiente manera:

invoiceDict.Add(pdfCount+i, new StringBuilder(invoiceID.Groups[1].ToString() + "|" + extractFileName + "|" + pdfPair.Key));

Como puede ver, los tres valores están separados por un '|'. El número de '' filas '' en el Diccionario puede oscilar entre 600-1200. Me gustaría utilizar un parámetro con valores de tabla para obtener todo eso en mi base de datos SQL Server 2008 en una sola operación.

El parámetro con valores de tabla se define de la siguiente manera:

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
)

¿Cuál es la mejor manera de pasar del Diccionario a algo que se puede pasar al parámetro con valores de tabla? ¿Debo usar algo más que el Diccionario? ¿O necesito analizar el Diccionario en otra estructura de datos mejor?

SqlParameter invoicesParam = cmd.Parameters.AddWithValue("@Invoices", invoiceDict.Values);
invoicesParam.SqlDbType = SqlDbType.Structured;

Gracias.

¿Fue útil?

Solución

Debido a un problema con la documentación algo pobre que rodea a los TVP (necesita más que solo IEnumerable), y al tener que analizar el contenido de los diccionarios de todos modos, decidí recorrer el Diccionario en una DataTable, que parece ser el objeto preferido para alimentar a un TVP.

Otros consejos

Tanto @narayanamarthi como @ChrisGessler están en el camino correcto con respecto al uso de la interfaz IEnumerable < SqlDataRecord > en lugar de DataTable. Copiar la colección a una DataTable es simplemente desperdiciar CPU y memoria sin ganancia. Pero el iterador puede estar separado de la colección.

Entonces, algunas notas:

  • No utilice AddWithValue al crear SqlParameter s. Es solo una mala práctica
  • No concatene los 3 valores distintos que desea en el TVP en un String o StringBuilder. El propósito completo de un TVP es pasar un registro fuertemente tipado para que la serialización de los campos en una lista CSV anule el propósito. En su lugar, use la clase Factura recomendada por Chris.
  • Reemplace su invoiceDict.Add (...) con List < Invoice > .Add (nueva factura (...));
  • Cree un método para iterar sobre la colección:

    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 el parámetro de la siguiente manera:

    SqlParameter _InvoiceParam = cmd.Parameters.Add("@Invoices", SqlDbType.Structured);
    _InvoiceParam.Value = SendRows(Invoices);
    

Tengo notas y enlaces adicionales en las siguientes dos respuestas con respecto a los TVP en general:

SqlBulkCopy lo enviará en una operación, pero deberá pasar una DataTable o un IDataReader en lugar de un Diccionario < int, StringBuilder > y también deberá apuntar directamente a la tabla, en lugar de utilizar un procedimiento almacenado.

Implementar la interfaz IEnumerable en la clase que contiene su colección y luego implementar explícitamente el Método GetEnumerator para IEnemerable servirá para este propósito. Hay un artículo en http://lennilobel.wordpress.com/2009/07/29/sql-server-2008-table-valued-parameters-and-c-custom-iterators- a-match-made-in-heaven / . Por favor revisa esto

Debe implementar IEnumerable < SqlDataRecord > en un objeto de colección personalizado. En su caso, debe cambiar su Dictionary < int, StringBuilder > a List < Invoice > o List < Tuple < int, string, int > >

Por ejemplo:

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;
    }
}

Luego, simplemente pase la lista personalizada a un SqlParameter :

SqlCommand cmd = new SqlCommand("dbo.InvoiceInsUpd", connection);
cmd.CommandType = CommandType.StoredProcedure;

SqlParameter sqlParam = cmd.Parameters.AddWithValue("@Invoices", invoices);
sqlParam.SqlDbType = SqlDbType.Structured;
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top