Pase el parámetro de matriz en SqlCommand

144

Estoy tratando de pasar el parámetro de matriz al comando SQL en C # como a continuación, pero no funciona. ¿Alguien lo conoce 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
fuente
11
No es realmente el tema, pero me parece que tener Age como columna en una tabla es una mala idea, ya que tendrá que actualizarse constantemente. La gente envejece, ¿verdad? ¿Quizás debería considerar tener una columna DateOfBirth en su lugar?
Kjetil Watnedal
pregunta con buena respuesta aquí: stackoverflow.com/questions/83471/…
Adam Butler

Respuestas:

168

Deberá agregar los valores en la matriz de uno en uno.

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

ACTUALIZACIÓN: Aquí hay una solución extendida y reutilizable que utiliza la respuesta de Adam junto con su edición sugerida. Lo mejoré un poco y lo convertí en un método de extensión para que sea aún más fácil llamar.

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

}

Se llama así ...

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

Observe que "{Age}" en la instrucción sql es el mismo que el nombre del parámetro que estamos enviando a AddArrayParameters. AddArrayParameters reemplazará el valor con los parámetros correctos.

Brian
fuente
11
¿Este método tiene el problema de seguridad, como la inyección SQL?
Yongwei Xing
77
Debido a que está poniendo los valores en parámetros, no hay riesgo de inyección sql.
Brian
Esto es lo que estaba buscando pero tenía una pregunta. Si el OP tuviera varias de las mismas columnas para agregar al SQL, ¿cómo haríamos esto? Ejemplo. SELECCIONE * DE la tabla A DONDE Edad = ({0}) O Edad = ({1}). (¿Cómo lo haríamos con el cmd. Parámetros)
Cocoa Dev
1
@ T2Tom, Whoops. Lo arreglé. Gracias.
Brian
1
Me gusta esto, y solo hice la siguiente modificación después de extraer la cadena del marcador de posición a una variable: var paramPlaceholder = "{" & paramNameRoot & "}"; Debug.Assert(cmd.CommandText.Contains(paramPlaceholder), "Parameter Name Root must exist in the Source Query"); Esto debería ayudar a los desarrolladores si se olvidan de hacer coincidir paramNameRoot con la consulta.
MCattle
37

Quería ampliar la respuesta de que Brian contribuyó a hacer esto fácilmente utilizable en otros 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);
}

Puede usar esta nueva función de la siguiente manera:

SqlCommand cmd = new SqlCommand();

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

cmd.CommandText = sql;


Editar: Aquí hay una variación genérica que funciona con una matriz de valores de cualquier tipo y se puede usar como un método de extensión:

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

Luego puede usar este método de extensión de la siguiente manera:

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

Asegúrese de configurar CommandText antes de llamar a AddArrayParameters.

También asegúrese de que el nombre de su parámetro no coincida parcialmente con nada más en su declaración (es decir, @AgeOfChild)

J Adam Rogers
fuente
1
Aquí hay una variación genérica que funciona con una matriz de valores de cualquier tipo y se puede usar como un método de extensión: public static void AddArrayParameters <T> (este cmd SqlCommand, nombre de cadena, valores IEnumerable <T>) {var names = string.Join (",", values.Select ((value, i) => {var paramName = name + i; cmd.Parameters.AddWithValue (paramName, value); return paramName;})); cmd.CommandText = cmd.CommandText.Replace (nombre, nombres); }
Adam Nemitoff
Un problema menor con esta respuesta es con la AddWithValuefunción, ¿hay alguna posibilidad de que pueda solucionarlo?
DavidG
Esta respuesta es incorrecta porque tiene escasa escalabilidad y rendimiento y promueve malas prácticas de codificación.
Igor Levicki
24

Si puede usar una herramienta como "apuesto", esto puede ser simplemente:

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();

Dapper se encargará de desenvolver esto a parámetros individuales para usted .

Marc Gravell
fuente
Dapper extrae muchas dependencias :(
mlt
@mlt ¿eh? no, no lo hace; en netfx: "sin dependencias"; en ns2.0, solo "System.Reflection.Emit.Lightweight" - y probablemente podríamos eliminar eso si agregamos un objetivo necroreapp
Marc Gravell
No quise secuestrar la discusión, pero lo hice ... Hasta ahora uso Npgsql que maneja matrices bien como en '{1,2,3}'argumentos de estilo para una función (no una cláusula WHERE IN), pero prefiero usar ODBC simple si no problemas de matriz. Supongo que necesitaría Dapper ODBC también en este caso. Esto es lo que quiere sacar. snipboard.io/HU0RpJ.jpg . Tal vez debería leer más sobre Dapper ...
mlt
16

Si está utilizando MS SQL Server 2008 y superior, puede usar parámetros con valores de tabla como se describe aquí http://www.sommarskog.se/arrays-in-sql-2008.html .

1. Cree un tipo de tabla para cada tipo de parámetro que usará

El siguiente comando crea un tipo de tabla para enteros:

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

2. Implemente 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. Úselo así

cmd.CommandText = "select * from TableA where Age in (select id from @age)"; 
cmd.AddParameter("@age", new [] {1,2,3,4,5});
Gregor Slavec
fuente
1
La línea table.Rows.Add(id);produce un olor de código menor cuando se utiliza SonarQube. He utilizado esta alternativa dentro del foreach: var row = table.NewRow(); row["id"] = id; table.Rows.Add(row);.
pogosama
1
Esta debería ser la respuesta aceptada, especialmente si se adaptó para aceptar más columnas.
Igor Levicki
10

Como hay un método en

SqlCommand.Parameters.AddWithValue(parameterName, value)

Puede ser más conveniente crear un método que acepte un parámetro (nombre) para reemplazar y una lista de valores. No está en el nivel de Parámetros (como AddWithValue ) sino en el comando en sí mismo, por lo que es mejor llamarlo AddParametersWithValues y no solo AddWithValues :

consulta:

SELECT * from TableA WHERE Age IN (@age)

uso:

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

El método de extensión:

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
fuente
1
Parece que existen varias iteraciones de este método de extensión en algunas respuestas. Sin embargo, usé este, así que lo estoy votando :-)
Dan Forbes
es mejor usar un índice estático para el nombre del parámetro
shmnff
6

Quiero proponer otra forma, cómo resolver la limitación con el operador IN.

Por ejemplo tenemos la siguiente consulta

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

Queremos pasar varias ID para filtrar usuarios. Lamentablemente, no es posible hacer con C # de manera fácil. Pero he encontrado una solución alternativa para esto usando la función "string_split". Necesitamos reescribir un poco nuestra consulta a continuación.

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

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

Ahora podemos pasar fácilmente una enumeración de valores de parámetros separados por comas.

usuario2399170
fuente
La mejor y más limpia forma que he encontrado, siempre que su compatibilidad sea actual.
user1040975
4

Pasar una matriz de elementos como un parámetro colapsado a la cláusula WHERE..IN fallará ya que la consulta tomará forma de WHERE Age IN ("11, 13, 14, 16").

Pero puede pasar su parámetro como una matriz serializada a XML o JSON:

Utilizando el 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();

Utilizando el 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();

Eso es un poco más en el lado de SQL y necesita un XML adecuado (con root).

Usando el OPENJSONmétodo (SQL Server 2016+):

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

Tenga en cuenta que para el último método también debe tener un nivel de compatibilidad de más de 130.

Lukasz Matysiak
fuente
0

Descripción general: utilice DbType para establecer el 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()
Leon de Oro
fuente
-1

Uso .AddWithValue(), entonces:

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

Alternativamente, podría usar esto:

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

Su ejemplo de código total se verá a continuación:

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
fuente
El tipo de campo Edad es nvchar no int. ¿Importa?
Yongwei Xing
No debería Especialmente con el segundo método. Usted especifica el tipo explícitamente.
Kyle Rosendo
Yo uso ambos métodos, todavía no funciona. No quiero manipular la cadena que puede
causar
Realmente no te entiendo. Cuando dices que no funciona, ¿arroja una excepción? ¿Qué hace?
Kyle Rosendo
1
no arroja excepciones, no devuelve nada. Pero ejecuto el T-SQL en Studio Management, devuelve muchos resultados.
Yongwei Xing
-1

Aquí hay una variante menor de la respuesta de Brian que alguien más puede encontrar útil. Toma una lista de claves y la coloca en la 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
fuente
Esta respuesta es incorrecta porque tiene escasa escalabilidad y rendimiento y promueve malas prácticas de codificación.
Igor Levicki
-3

tratar

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

pruébalo así

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
fuente
2
¿Por qué poner SqlConnection y SqlCommnad en el bucle?
Yongwei Xing