Parâmetro de matriz de passagem no SqlCommand

144

Eu estou tentando passar o parâmetro de matriz para SQL commnd em c # como abaixo, mas não funciona. Alguém conhece isso antes?

string sqlCommand = "SELECT * from TableA WHERE Age IN (@Age)";
SqlConnection sqlCon = new SqlConnection(connectString);
SqlCommand sqlComm = new SqlCommand();
sqlComm.Connection = sqlCon;
sqlComm.CommandType = System.Data.CommandType.Text;
sqlComm.CommandText = sqlCommand;
sqlComm.CommandTimeout = 300;
sqlComm.Parameters.Add("@Age", SqlDbType.NVarChar);
StringBuilder sb = new StringBuilder();
foreach (ListItem item in ddlAge.Items)
{
     if (item.Selected)
     {
         sb.Append(item.Text + ",");
     }
}

sqlComm.Parameters["@Age"].Value = sb.ToString().TrimEnd(',');
Yongwei Xing
fonte
11
Não é realmente o assunto, mas me parece que ter Age como uma coluna em uma tabela é uma má ideia, pois precisará ser atualizado constantemente. As pessoas envelhecem certo? Talvez você deva considerar ter uma coluna DateOfBirth?
Kjetil Watnedal
pergunta com boa resposta aqui: stackoverflow.com/questions/83471/…
Adam Butler

Respostas:

168

Você precisará adicionar os valores na matriz, um de cada vez.

var parameters = new string[items.Length];
var cmd = new SqlCommand();
for (int i = 0; i < items.Length; i++)
{
    parameters[i] = string.Format("@Age{0}", i);
    cmd.Parameters.AddWithValue(parameters[i], items[i]);
}

cmd.CommandText = string.Format("SELECT * from TableA WHERE Age IN ({0})", string.Join(", ", parameters));
cmd.Connection = new SqlConnection(connStr);

ATUALIZAÇÃO: Aqui está uma solução estendida e reutilizável que usa a resposta de Adam junto com sua edição sugerida. Eu o melhorei um pouco e o tornei um método de extensão para facilitar ainda mais a chamada.

public static class SqlCommandExt
{

    /// <summary>
    /// This will add an array of parameters to a SqlCommand. This is used for an IN statement.
    /// Use the returned value for the IN part of your SQL call. (i.e. SELECT * FROM table WHERE field IN ({paramNameRoot}))
    /// </summary>
    /// <param name="cmd">The SqlCommand object to add parameters to.</param>
    /// <param name="paramNameRoot">What the parameter should be named followed by a unique value for each value. This value surrounded by {} in the CommandText will be replaced.</param>
    /// <param name="values">The array of strings that need to be added as parameters.</param>
    /// <param name="dbType">One of the System.Data.SqlDbType values. If null, determines type based on T.</param>
    /// <param name="size">The maximum size, in bytes, of the data within the column. The default value is inferred from the parameter value.</param>
    public static SqlParameter[] AddArrayParameters<T>(this SqlCommand cmd, string paramNameRoot, IEnumerable<T> values, SqlDbType? dbType = null, int? size = null)
    {
        /* An array cannot be simply added as a parameter to a SqlCommand so we need to loop through things and add it manually. 
         * Each item in the array will end up being it's own SqlParameter so the return value for this must be used as part of the
         * IN statement in the CommandText.
         */
        var parameters = new List<SqlParameter>();
        var parameterNames = new List<string>();
        var paramNbr = 1;
        foreach (var value in values)
        {
            var paramName = string.Format("@{0}{1}", paramNameRoot, paramNbr++);
            parameterNames.Add(paramName);
            SqlParameter p = new SqlParameter(paramName, value);
            if (dbType.HasValue)
                p.SqlDbType = dbType.Value;
            if (size.HasValue)
                p.Size = size.Value;
            cmd.Parameters.Add(p);
            parameters.Add(p);
        }

        cmd.CommandText = cmd.CommandText.Replace("{" + paramNameRoot + "}", string.Join(",", parameterNames));

        return parameters.ToArray();
    }

}

É assim chamado ...

var cmd = new SqlCommand("SELECT * FROM TableA WHERE Age IN ({Age})");
cmd.AddArrayParameters("Age", new int[] { 1, 2, 3 });

Observe que "{Age}" na instrução sql é igual ao nome do parâmetro que estamos enviando para AddArrayParameters. AddArrayParameters substituirá o valor pelos parâmetros corretos.

Brian
fonte
11
Esse método tem um problema de segurança, como a injeção de sql?
Yongwei Xing
7
Como você está colocando os valores em parâmetros, não há risco de injeção de sql.
Brian
Era isso que eu estava procurando, mas eu tinha uma pergunta. Se o OP tivesse várias das mesmas colunas para adicionar ao SQL, como faríamos isso? Exemplo. SELECT * FROM TableA WHERE Age = ({0}) OR Age = ({1}). (como poderíamos fazê-lo com os cmd.Parameters)
Cocoa Dev
1
@ T2Tom, Ops. Eu consertei isso. Obrigado.
Brian
1
Eu gosto disso e só fiz a seguinte modificação depois de extrair a string do espaço reservado para uma variável: var paramPlaceholder = "{" & paramNameRoot & "}"; Debug.Assert(cmd.CommandText.Contains(paramPlaceholder), "Parameter Name Root must exist in the Source Query"); Isso deve ajudar os desenvolvedores se eles esquecerem de corresponder paramNameRoot à consulta.
MCattle
37

Eu queria expandir a resposta que Brian contribuiu para tornar isso facilmente utilizável em outros lugares.

/// <summary>
/// This will add an array of parameters to a SqlCommand. This is used for an IN statement.
/// Use the returned value for the IN part of your SQL call. (i.e. SELECT * FROM table WHERE field IN (returnValue))
/// </summary>
/// <param name="sqlCommand">The SqlCommand object to add parameters to.</param>
/// <param name="array">The array of strings that need to be added as parameters.</param>
/// <param name="paramName">What the parameter should be named.</param>
protected string AddArrayParameters(SqlCommand sqlCommand, string[] array, string paramName)
{
    /* An array cannot be simply added as a parameter to a SqlCommand so we need to loop through things and add it manually. 
     * Each item in the array will end up being it's own SqlParameter so the return value for this must be used as part of the
     * IN statement in the CommandText.
     */
    var parameters = new string[array.Length];
    for (int i = 0; i < array.Length; i++)
    {
        parameters[i] = string.Format("@{0}{1}", paramName, i);
        sqlCommand.Parameters.AddWithValue(parameters[i], array[i]);
    }

    return string.Join(", ", parameters);
}

Você pode usar esta nova função da seguinte maneira:

SqlCommand cmd = new SqlCommand();

string ageParameters = AddArrayParameters(cmd, agesArray, "Age");
sql = string.Format("SELECT * FROM TableA WHERE Age IN ({0})", ageParameters);

cmd.CommandText = sql;


Edit: Aqui está uma variação genérica que funciona com uma matriz de valores de qualquer tipo e é utilizável como um método de extensão:

public static class Extensions
{
    public static void AddArrayParameters<T>(this SqlCommand cmd, string name, IEnumerable<T> values) 
    { 
        name = name.StartsWith("@") ? name : "@" + name;
        var names = string.Join(", ", values.Select((value, i) => { 
            var paramName = name + i; 
            cmd.Parameters.AddWithValue(paramName, value); 
            return paramName; 
        })); 
        cmd.CommandText = cmd.CommandText.Replace(name, names); 
    }
}

Você pode usar esse método de extensão da seguinte maneira:

var ageList = new List<int> { 1, 3, 5, 7, 9, 11 };
var cmd = new SqlCommand();
cmd.CommandText = "SELECT * FROM MyTable WHERE Age IN (@Age)";    
cmd.AddArrayParameters("Age", ageList);

Certifique-se de definir o CommandText antes de chamar AddArrayParameters.

Além disso, verifique se o nome do parâmetro não corresponde parcialmente a mais nada em sua declaração (por exemplo, @AgeOfChild)

J Adam Rogers
fonte
1
Aqui está uma variação genérica que funciona com uma matriz de valores de qualquer tipo e é utilizável como método de extensão: public static void AddArrayParameters <T> (esse cmd SqlCommand, nome da string, valores IEnumerable <T>) {var names = string.Join (",", values.Select ((value, i) => {var paramName = nome + i; cmd.Parameters.AddWithValue (paramName, valor); retornar paramName;})); cmd.CommandText = cmd.CommandText.Replace (nome, nomes); }
Adam Nemitoff 13/01/14
O menor problema com esta resposta está na AddWithValuefunção, alguma chance de você consertar isso?
DavidG
Esta resposta está errada porque tem baixa escalabilidade e desempenho e promove práticas de codificação ruins.
Igor Levicki 22/03
24

Se você pode usar uma ferramenta como "dapper", isso pode ser simplesmente:

int[] ages = { 20, 21, 22 }; // could be any common list-like type
var rows = connection.Query<YourType>("SELECT * from TableA WHERE Age IN @ages",
          new { ages }).ToList();

O Dapper tratará de desembrulhar isso em parâmetros individuais para você .

Marc Gravell
fonte
Dapper puxa muitas dependências :(
mlt 19/02
@mlt huh? não, não faz; no netfx: "sem dependências"; no ns2.0, apenas "System.Reflection.Emit.Lightweight" - e provavelmente poderíamos removê-lo se adicionássemos um destino necroreapp
Marc Gravell
Não pretendia seqüestrar a discussão, mas sim ... Até agora, uso o Npgsql que lida com matrizes bem como nos '{1,2,3}'argumentos de estilo de uma função (não uma cláusula WHERE IN), mas prefiro usar ODBC comum, se não aborrecimento da matriz. Presumo que precisaria do Dapper ODBC também neste caso. Aqui está o que ele quer puxar. snipboard.io/HU0RpJ.jpg . Talvez eu devesse ler mais sobre Dapper ...
mlt 20/02
16

Se você estiver usando o MS SQL Server 2008 e acima, poderá usar parâmetros com valor de tabela, como descrito aqui http://www.sommarskog.se/arrays-in-sql-2008.html .

1. Crie um tipo de tabela para cada tipo de parâmetro que você usará

O comando a seguir cria um tipo de tabela para números inteiros:

create type int32_id_list as table (id int not null primary key)

2. Implementar métodos auxiliares

public static SqlCommand AddParameter<T>(this SqlCommand command, string name, IEnumerable<T> ids)
{
  var parameter = command.CreateParameter();      

  parameter.ParameterName = name;
  parameter.TypeName = typeof(T).Name.ToLowerInvariant() + "_id_list";
  parameter.SqlDbType = SqlDbType.Structured;
  parameter.Direction = ParameterDirection.Input;

  parameter.Value = CreateIdList(ids);

  command.Parameters.Add(parameter);
  return command;
}

private static DataTable CreateIdList<T>(IEnumerable<T> ids)
{
  var table = new DataTable();
  table.Columns.Add("id", typeof (T));

  foreach (var id in ids)
  {
    table.Rows.Add(id);
  }

  return table;
}

3. Use-o assim

cmd.CommandText = "select * from TableA where Age in (select id from @age)"; 
cmd.AddParameter("@age", new [] {1,2,3,4,5});
Gregor Slavec
fonte
1
A linha table.Rows.Add(id);resulta em um pequeno cheiro de código ao usar o SonarQube. Eu usei essa alternativa dentro do foreach: var row = table.NewRow(); row["id"] = id; table.Rows.Add(row);.
Pogosama
1
Essa deve ser a resposta aceita, especialmente se foi adaptada para aceitar mais colunas.
Igor Levicki em 21/03
10

Como existe um método em

SqlCommand.Parameters.AddWithValue(parameterName, value)

pode ser mais conveniente criar um método que aceite um parâmetro (nome) para substituir e uma lista de valores. Não está no nível Parâmetros (como AddWithValue ), mas no próprio comando, por isso é melhor chamá-lo de AddParametersWithValues e não apenas AddWithValues :

inquerir:

SELECT * from TableA WHERE Age IN (@age)

uso:

sqlCommand.AddParametersWithValues("@age", 1, 2, 3);

o método de extensão:

public static class SqlCommandExtensions
{
    public static void AddParametersWithValues<T>(this SqlCommand cmd,  string parameterName, params T[] values)
    {
        var parameterNames = new List<string>();
        for(int i = 0; i < values.Count(); i++)
        {
            var paramName = @"@param" + i;
            cmd.Parameters.AddWithValue(paramName, values.ElementAt(i));
            parameterNames.Add(paramName);
        }

        cmd.CommandText = cmd.CommandText.Replace(parameterName, string.Join(",", parameterNames));
    }
}
tridy
fonte
1
Parece que várias iterações desse método de extensão existem em algumas respostas. Eu usei este, embora, por isso estou votando-lo :-)
Dan Forbes
é melhor usar um índice estático para o nome do parâmetro
shmnff 28/01
6

Quero propor outra maneira, como resolver a limitação com o operador IN.

Por exemplo, temos a seguinte consulta

select *
from Users U
WHERE U.ID in (@ids)

Queremos passar vários IDs para filtrar usuários. Infelizmente, não é possível fazer com o C # de maneira fácil. Mas eu tenho uma solução alternativa para isso usando a função "string_split". Precisamos reescrever um pouco nossa consulta para seguir.

declare @ids nvarchar(max) = '1,2,3'

SELECT *
FROM Users as U
CROSS APPLY string_split(@ids, ',') as UIDS
WHERE U.ID = UIDS.value

Agora podemos passar facilmente uma enumeração de parâmetros com valores separados por vírgula.

user2399170
fonte
a melhor e mais limpa maneira que encontrei, desde que sua compatibilidade seja atual.
user1040975 8/01
4

Passar uma matriz de itens como um parâmetro recolhido para a cláusula WHERE..IN falhará, pois a consulta assumirá a forma de WHERE Age IN ("11, 13, 14, 16").

Mas você pode passar seu parâmetro como uma matriz serializada para XML ou JSON:

Usando o nodes()método:

StringBuilder sb = new StringBuilder();

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    sb.Append("<age>" + item.Text + "</age>"); // actually it's xml-ish

sqlComm.CommandText = @"SELECT * from TableA WHERE Age IN (
    SELECT Tab.col.value('.', 'int') as Age from @Ages.nodes('/age') as Tab(col))";
sqlComm.Parameters.Add("@Ages", SqlDbType.NVarChar);
sqlComm.Parameters["@Ages"].Value = sb.ToString();

Usando o OPENXMLmétodo:

using System.Xml.Linq;
...
XElement xml = new XElement("Ages");

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    xml.Add(new XElement("age", item.Text);

sqlComm.CommandText = @"DECLARE @idoc int;
    EXEC sp_xml_preparedocument @idoc OUTPUT, @Ages;
    SELECT * from TableA WHERE Age IN (
    SELECT Age from OPENXML(@idoc, '/Ages/age') with (Age int 'text()')
    EXEC sp_xml_removedocument @idoc";
sqlComm.Parameters.Add("@Ages", SqlDbType.Xml);
sqlComm.Parameters["@Ages"].Value = xml.ToString();

Isso é um pouco mais do lado SQL e você precisa de um XML adequado (com raiz).

Usando o OPENJSONmétodo (SQL Server 2016 ou superior):

using Newtonsoft.Json;
...
List<string> ages = new List<string>();

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    ages.Add(item.Text);

sqlComm.CommandText = @"SELECT * from TableA WHERE Age IN (
    select value from OPENJSON(@Ages))";
sqlComm.Parameters.Add("@Ages", SqlDbType.NVarChar);
sqlComm.Parameters["@Ages"].Value = JsonConvert.SerializeObject(ages);

Observe que, para o último método, você também precisa ter o Nível de compatibilidade em mais de 130.

Lukasz Matysiak
fonte
0

Visão geral: use o DbType para definir o tipo de parâmetro.

var parameter = new SqlParameter();
parameter.ParameterName = "@UserID";
parameter.DbType = DbType.Int32;
parameter.Value = userID.ToString();

var command = conn.CreateCommand()
command.Parameters.Add(parameter);
var reader = await command.ExecuteReaderAsync()
Leão dourado
fonte
-1

Use .AddWithValue(), então:

sqlComm.Parameters.AddWithValue("@Age", sb.ToString().TrimEnd(','));

Como alternativa, você pode usar isso:

sqlComm.Parameters.Add(
    new SqlParameter("@Age", sb.ToString().TrimEnd(',')) { SqlDbType = SqlDbType. NVarChar }
    );

Seu exemplo de código total analisará a seguir:

string sqlCommand = "SELECT * from TableA WHERE Age IN (@Age)";
SqlConnection sqlCon = new SqlConnection(connectString);
SqlCommand sqlComm = new SqlCommand();
sqlComm.Connection = sqlCon;
sqlComm.CommandType = System.Data.CommandType.Text;
sqlComm.CommandText = sqlCommand;
sqlComm.CommandTimeout = 300;

StringBuilder sb = new StringBuilder();
foreach (ListItem item in ddlAge.Items)
{
     if (item.Selected)
     {
         sb.Append(item.Text + ",");
     }
}

sqlComm.Parameters.AddWithValue("@Age", sb.ToString().TrimEnd(','));

// OR

// sqlComm.Parameters.Add(new SqlParameter("@Age", sb.ToString().TrimEnd(',')) { SqlDbType = SqlDbType. NVarChar });
Kyle Rosendo
fonte
O tipo do campo Age é nvchar, não int. Isso importa?
Yongwei Xing
Não deveria. Especialmente com o segundo método. Você especifica o tipo explicitamente.
Kyle Rosendo
Eu uso os dois métodos, ainda não funciona. Eu não quero para manipular a string que podem levou problema de segurança
Yongwei Xing
Eu realmente não estou entendendo você. Quando você diz que não funciona, isso gera uma exceção? O que isso faz?
Kyle Rosendo
1
não lança exceção, não retorna nada. Mas eu executo o T-SQL no Studio Management, ele retorna muitos resultados.
Yongwei Xing
-1

Aqui está uma variante menor da resposta de Brian que outra pessoa pode achar útil. Pega uma lista de chaves e a coloca na lista de parâmetros.

//keyList is a List<string>
System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand();
string sql = "SELECT fieldList FROM dbo.tableName WHERE keyField in (";
int i = 1;
foreach (string key in keyList) {
    sql = sql + "@key" + i + ",";
    command.Parameters.AddWithValue("@key" + i, key);
    i++;
}
sql = sql.TrimEnd(',') + ")";
Jeff
fonte
Esta resposta está errada porque tem baixa escalabilidade e desempenho e promove práticas de codificação ruins.
Igor Levicki 22/03
-3

experimentar

sqlComm.Parameters["@Age"].Value = sb.ToString().Replace(","," ");
Ballin
fonte
-5

tente assim

StringBuilder sb = new StringBuilder(); 
foreach (ListItem item in ddlAge.Items) 
{ 
     if (item.Selected) 
     { 
          string sqlCommand = "SELECT * from TableA WHERE Age IN (@Age)"; 
          SqlConnection sqlCon = new SqlConnection(connectString); 
          SqlCommand sqlComm = new SqlCommand(); 
          sqlComm.Connection = sqlCon; 
          sqlComm.CommandType = System.Data.CommandType.Text; 
          sqlComm.CommandText = sqlCommand; 
          sqlComm.CommandTimeout = 300; 
          sqlComm.Parameters.Add("@Age", SqlDbType.NVarChar);
          sb.Append(item.Text + ","); 
          sqlComm.Parameters["@Age"].Value = sb.ToString().TrimEnd(',');
     } 
} 
Ballin
fonte
2
Por que colocar SqlConnection e SqlCommnad no loop?
Yongwei Xing