Cómo pasar los parámetros del valor de la tabla al procedimiento almacenado desde el código .net

171

Tengo una base de datos SQL Server 2005. En algunos procedimientos, tengo parámetros de tabla que paso a un proceso almacenado como un nvarchar(separados por comas) y los divido internamente en valores únicos. Lo agrego a la lista de parámetros del comando SQL de esta manera:

cmd.Parameters.Add("@Logins", SqlDbType.NVarchar).Value = "jim18,jenny1975,cosmo";

Tengo que migrar la base de datos a SQL Server 2008. Sé que hay parámetros de valores de tabla y sé cómo usarlos en procedimientos almacenados. Pero no sé cómo pasar uno a la lista de parámetros en un comando SQL.

¿Alguien sabe la sintaxis correcta del Parameters.Addprocedimiento? ¿O hay otra forma de pasar este parámetro?

Marek Kwiendacz
fuente
Vea esta solución: Procedimiento almacenado con parámetro de valor de tabla en EF. code.msdn.microsoft.com/Stored-Procedure-with-6c194514
Carl Prothman
En un caso como este, generalmente concateno cadenas y las divido en el lado del servidor o paso incluso un xml si tengo varias columnas. SQL es muy rápido al procesar xml. Puede probar todos los métodos y verificar el tiempo de procesamiento y luego elegir el mejor método. Un XML se vería como <Items> <Item value = "sdadas" /> <Item value = "sadsad" /> ... </Items>. El proceso en SQL Server también es simple. Con este método, siempre puede agregar un nuevo atributo a <item> si necesita más información.
Nițu Alexandru
44
@ NițuAlexandru, "SQL es muy rápido al procesar xml". Ni siquiera cerca.
nothrow

Respuestas:

279

DataTable, DbDataReaderu IEnumerable<SqlDataRecord>objetos se pueden usar para completar un parámetro con valores de tabla según el artículo de MSDN Parámetros con valores de tabla en SQL Server 2008 (ADO.NET) .

El siguiente ejemplo ilustra el uso de a DataTableo an IEnumerable<SqlDataRecord>:

Código SQL :

CREATE TABLE dbo.PageView
(
    PageViewID BIGINT NOT NULL CONSTRAINT pkPageView PRIMARY KEY CLUSTERED,
    PageViewCount BIGINT NOT NULL
);
CREATE TYPE dbo.PageViewTableType AS TABLE
(
    PageViewID BIGINT NOT NULL
);
CREATE PROCEDURE dbo.procMergePageView
    @Display dbo.PageViewTableType READONLY
AS
BEGIN
    MERGE INTO dbo.PageView AS T
    USING @Display AS S
    ON T.PageViewID = S.PageViewID
    WHEN MATCHED THEN UPDATE SET T.PageViewCount = T.PageViewCount + 1
    WHEN NOT MATCHED THEN INSERT VALUES(S.PageViewID, 1);
END

Código C # :

private static void ExecuteProcedure(bool useDataTable, 
                                     string connectionString, 
                                     IEnumerable<long> ids) 
{
    using (SqlConnection connection = new SqlConnection(connectionString)) 
    {
        connection.Open();
        using (SqlCommand command = connection.CreateCommand()) 
        {
            command.CommandText = "dbo.procMergePageView";
            command.CommandType = CommandType.StoredProcedure;

            SqlParameter parameter;
            if (useDataTable) {
                parameter = command.Parameters
                              .AddWithValue("@Display", CreateDataTable(ids));
            }
            else 
            {
                parameter = command.Parameters
                              .AddWithValue("@Display", CreateSqlDataRecords(ids));
            }
            parameter.SqlDbType = SqlDbType.Structured;
            parameter.TypeName = "dbo.PageViewTableType";

            command.ExecuteNonQuery();
        }
    }
}

private static DataTable CreateDataTable(IEnumerable<long> ids) 
{
    DataTable table = new DataTable();
    table.Columns.Add("ID", typeof(long));
    foreach (long id in ids) 
    {
        table.Rows.Add(id);
    }
    return table;
}

private static IEnumerable<SqlDataRecord> CreateSqlDataRecords(IEnumerable<long> ids) 
{
    SqlMetaData[] metaData = new SqlMetaData[1];
    metaData[0] = new SqlMetaData("ID", SqlDbType.BigInt);
    SqlDataRecord record = new SqlDataRecord(metaData);
    foreach (long id in ids) 
    {
        record.SetInt64(0, id);
        yield return record;
    }
}
Ryan Prechel
fuente
24
+1 Excelente ejemplo. Las conclusiones son: envíe un DataTablevalor de parámetro, establecido SqlDbTypeen Structuredy TypeNamepara el nombre UDT de la base de datos.
lc.
10
Si va a reutilizar una instancia de un tipo de referencia en un bucle (SqlDataRecord en su ejemplo), agregue un comentario sobre por qué es seguro hacerlo en esta instancia en particular.
Søren Boisen
2
Este código es incorrecto: los parámetros con valores de tabla vacía deben tener su valor establecido en null. CreateSqlDataRecordsnunca volverá nullsi se le da un idsparámetro vacío .
ta.speot.is
44
@Crono: DataTable(o DataSet) solo lo implementan porque tienen que surtir capacidades de arrastrar y soltar en Visual-Studio, por lo que implementan IComponentqué implementos IDisposable. Si no usa el diseñador pero lo crea manualmente, no hay razón para deshacerse de él (o para usar la usingdeclaración). Así que esta es una de las excepciones de la regla de oro "disponer todo lo que implemente IDisposable".
Tim Schmelter
2
@TimSchmelter Como regla general, siempre llamo a Disposemétodos, incluso si es solo para que Code Analysis no me avise si no lo hago. Pero estoy de acuerdo en que en este escenario específico donde se usan la base DataSety las DataTableinstancias, llamar Disposeno haría nada.
Crono
31

Con la respuesta de Ryan que también tendrá que establecer el DataColumn's Ordinalpropiedad si se trata de una table-valued parametercon múltiples columnas cuyos ordinales son no en orden alfabético.

Como ejemplo, si tiene el siguiente valor de tabla que se utiliza como parámetro en SQL:

CREATE TYPE NodeFilter AS TABLE (
  ID int not null
  Code nvarchar(10) not null,
);

Debería ordenar sus columnas como tales en C #:

table.Columns["ID"].SetOrdinal(0);
// this also bumps Code to ordinal of 1
// if you have more than 2 cols then you would need to set more ordinals

Si no lo hace, obtendrá un error de análisis, no se pudo convertir nvarchar a int.

Scotty.NET
fuente
15

Genérico

   public static DataTable ToTableValuedParameter<T, TProperty>(this IEnumerable<T> list, Func<T, TProperty> selector)
    {
        var tbl = new DataTable();
        tbl.Columns.Add("Id", typeof(T));

        foreach (var item in list)
        {
            tbl.Rows.Add(selector.Invoke(item));

        }

        return tbl;

    }
Martea
fuente
¿Podría decirme qué paso como parámetro? Selector de funciones <T, TProperty>? ¿No puede ser simplemente tbl.Rows.Add (elemento) y no es necesario ese parámetro?
GDroid
el selector. Invoke (item) selecciona la propiedad en el ítem la mayoría de los casos es un int, pero también le permite seleccionar una propiedad de cadena
Martea
¿Puedes dar un ejemplo de cómo pongo el selector allí? Tengo una lista <GUID> para pasar al procedimiento almacenado ...
GDroid
guidList.ToTabledValuedParameter (x => x), dado que x es el guid en su caso, la devolución será un DataTable con una columna (id) con una lista de guías,
Martea
5

La forma más limpia de trabajar con él. Suponiendo que su tabla es una lista de enteros llamada "dbo.tvp_Int" (Personalizar para su propio tipo de tabla)

Cree este método de extensión ...

public static void AddWithValue_Tvp_Int(this SqlParameterCollection paramCollection, string parameterName, List<int> data)
{
   if(paramCollection != null)
   {
       var p = paramCollection.Add(parameterName, SqlDbType.Structured);
       p.TypeName = "dbo.tvp_Int";
       DataTable _dt = new DataTable() {Columns = {"Value"}};
       data.ForEach(value => _dt.Rows.Add(value));
       p.Value = _dt;
   }
}

Ahora puede agregar un parámetro con valores de tabla en una línea en cualquier lugar simplemente haciendo esto:

cmd.Parameters.AddWithValueFor_Tvp_Int("@IDValues", listOfIds);
Shahzad Qureshi
fuente
1
¿Qué pasa si paramCollection es NULL? ¿Cómo pasar el tipo vacío?
Muflix
2
@Muflix Es evidente que los métodos de extensión realmente funcionan contra instancias nulas. Así que agregar un if(paramCollection != null)cheque simple en la parte superior del método estará bien
Rhumborl
1
Respuesta actualizada con cheque -if- inicial
Shahzad Qureshi
2
Tal vez sea un poco pedante, pero lo usaría en IEnumerablelugar de Listen la firma, de esa manera puede pasar cualquier cosa que sea IEnumerable, no solo listas, ya que no está usando ninguna función específica para List, realmente no veo una razón para no hacerlo nosotrosIEnumerable
Francis Lord
Usar List le permite usar los datos abreviados. ForEach (), de lo contrario, tendría que escribir un bucle foreach. Lo que podría funcionar también, pero me gusta escribir cosas lo más cortas posible.
Shahzad Qureshi
0

Use este código para crear parámetros adecuados a partir de su tipo:

private SqlParameter GenerateTypedParameter(string name, object typedParameter)
{
    DataTable dt = new DataTable();

    var properties = typedParameter.GetType().GetProperties().ToList();
    properties.ForEach(p =>
    {
        dt.Columns.Add(p.Name, Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType);
    });
    var row = dt.NewRow();
    properties.ForEach(p => { row[p.Name] = (p.GetValue(typedParameter) ?? DBNull.Value); });
    dt.Rows.Add(row);

    return new SqlParameter
    {
        Direction = ParameterDirection.Input,
        ParameterName = name,
        Value = dt,
        SqlDbType = SqlDbType.Structured
    };
}
lado B
fuente