Como passar parâmetros de valor de tabela para o procedimento armazenado do código .net

171

Eu tenho um banco de dados SQL Server 2005. Em alguns procedimentos, tenho parâmetros de tabela que passo para um processo armazenado nvarchar(separado por vírgulas) e divido internamente em valores únicos. Eu o adiciono à lista de parâmetros de comando SQL como esta:

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

Eu tenho que migrar o banco de dados para o SQL Server 2008. Sei que existem parâmetros de valor de tabela e sei como usá-los em procedimentos armazenados. Mas não sei como passar um para a lista de parâmetros em um comando SQL.

Alguém sabe a sintaxe correta do Parameters.Addprocedimento? Ou existe outra maneira de passar esse parâmetro?

Marek Kwiendacz
fonte
Confira esta solução: Procedimento armazenado com parâmetro com valor de tabela no EF. code.msdn.microsoft.com/Stored-Procedure-with-6c194514
Carl Prothman
Em um caso como esse, geralmente concatenamos as strings e as divido no lado do servidor ou passo até um xml se tiver várias colunas. Sql é muito rápido ao processar xml. Você pode experimentar todos os métodos, verificar o tempo de processamento e, depois, escolher o melhor método. Um XML se pareceria com <Items> <Item value = "sdadas" /> <Item value = "sadsad" /> ... </Items>. O processo no Sql Server também é simples. Usando esse método, você sempre pode adicionar um novo atributo ao <item> se precisar de mais informações.
Nițu Alexandru
4
@ NițuAlexandru, "Sql é muito rápido ao processar xml.". Nem mesmo perto.
nothrow

Respostas:

278

DataTable, DbDataReader, Ou IEnumerable<SqlDataRecord>objetos podem ser usados para preencher um parâmetro com valor de tabela pelo artigo do MSDN Parâmetros com valor de tabela no SQL Server 2008 (ADO.NET) .

O exemplo a seguir ilustra o uso de um DataTableou um 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
fonte
24
+1 Excelente exemplo. Os tópicos a seguir são: envie a DataTablecomo o valor do parâmetro, defina SqlDbTypepara Structurede TypeNamepara o nome UDT do banco de dados.
lc.
10
Se você deseja reutilizar uma instância de um tipo de referência em um loop (SqlDataRecord no seu exemplo), adicione um comentário sobre por que é seguro fazê-lo nesta instância específica.
Søren Boisen
2
Este código está errado: os parâmetros com valor de tabela vazia devem ter seu valor definido como null. CreateSqlDataRecordsnunca retornará nullse for dado um idsparâmetro vazio .
ta.speot.is
4
@Crono: DataTable(ou DataSet) implementa-o apenas porque eles têm que suprimir recursos de arrastar e soltar no Visual-Studio, para que implementem IComponentquais implementos IDisposable. Se você não usar o designer, mas criá-lo manualmente, não há motivo para descartá-lo (ou usar a usingdeclaração). Portanto, essa é uma das exceções da regra de ouro "descarte tudo o que implementa IDisposable".
21816 Tim Schmelter
2
@ TimSchmelter Como regra geral, eu sempre chamo Disposemétodos, mesmo que seja apenas para que a Análise de Código não me avise se não o fizer. Mas concordo que nesse cenário específico em que base DataSete DataTableinstâncias são usadas, a chamada Disposenão faria nada.
Crono
31

Na sequência da resposta de Ryan você também precisará definir o DataColumn's Ordinalpropriedade, se você está lidando com um table-valued parametercom múltiplas colunas cujos ordinais são não em ordem alfabética.

Como exemplo, se você tiver o seguinte valor de tabela usado como parâmetro no SQL:

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

Você precisaria ordenar suas colunas como tal em 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

Se você não conseguir fazer isso, receberá um erro de análise, falha ao converter nvarchar em int.

Scotty.NET
fonte
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
fonte
Você poderia me informar que o que eu passo como parâmetro? Func <T, TProperty> seletor? Não pode ser simplesmente tbl.Rows.Add (item) e não há necessidade desse parâmetro.
GDroid 18/03/2015
o selector.Invoke (item) seleciona a propriedade no item maioria dos casos é um int, mas também permite que você selecione uma propriedade string
Martea
você pode, por favor, fornecer um exemplo de como coloco o seletor ali? Eu tenho um List <Guid> para passar para proc armazenado ...
GDroid
guidList.ToTabledValuedParameter (x => x), uma vez que x é o guid no seu caso, o retorno será um DataTable com uma coluna (id) com uma lista de guids,
Martea
5

A maneira mais limpa de trabalhar com isso. Supondo que sua tabela seja uma lista de números inteiros chamada "dbo.tvp_Int" (personalize para seu próprio tipo de tabela)

Crie este método de extensão ...

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

Agora você pode adicionar um parâmetro com valor de tabela em uma linha em qualquer lugar, simplesmente fazendo o seguinte:

cmd.Parameters.AddWithValueFor_Tvp_Int("@IDValues", listOfIds);
Shahzad Qureshi
fonte
1
e se o paramCollection for NULL? Como passar o tipo vazio?
Muflix
2
@Muflix Obscuramente, os métodos de extensão realmente funcionam contra instâncias nulas. Assim, a adição de uma simples if(paramCollection != null)seleção no topo do método vai ficar bem
Rhumborl
1
Resposta atualizada com a inicial -if- check
Shahzad Qureshi
2
Talvez um pouco pedante, mas eu usaria em IEnumerablevez da Listassinatura, para que você possa passar qualquer coisa que seja IEnumerable, não apenas listas. Como você não está usando nenhuma função específica List, não vejo realmente um motivo para não nósIEnumerable
Francis Lord
Usar Lista permite que você use os dados abreviados.ForEach (), caso contrário, você teria que realmente escrever um loop foreach. O que poderia funcionar também, mas eu gosto de escrever as coisas o mais curto possível.
Shahzad Qureshi
0

Use este código para criar um parâmetro adequado do seu 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
fonte