¿Hay una mejor manera de construir dinámicamente una cláusula WHERE de SQL que usando 1 = 1 al principio?

110

Estoy construyendo una consulta SQL en C #. Diferirá según algunas condiciones almacenadas como variables en el código.

string Query="SELECT * FROM Table1 WHERE 1=1 ";
if (condition1) 
    Query += "AND Col1=0 ";
if (condition2) 
    Query += "AND Col2=1 ";
if (condition3) 
    Query += "AND Col3=2 ";

Funciona, pero probar 1 = 1 no parece elegante. Si no lo usé, tendría que recordar y verificar cada vez si la palabra clave "donde" ya se agregó o no a la consulta.

¿Existe una mejor solución?

RRM
fuente
118
Para ser honesto, también lo haría así, pero usaría 42 = 42;-)
fero
5
De hecho, siempre escribo mis consultas así. Hace que sea más fácil comentar una condición
Deruijter
4
@catfood El primer proyecto en el que participé como pasante fue escribir herramientas para ayudar a analizar las consultas de rendimiento en nuestros servidores Sybase. Un descubrimiento divertido fueron los cientos de miles de Select 42consultas que estábamos recibiendo. (no es divertido intentar rastrear la fuente)
Señor Mindor
24
If I didn't use it, I would have to remember and check every time if "where" keyword was already added or not to the query- Por eso usas 1 = 1. El motor de la base de datos lo optimiza de todos modos, por lo que, si bien puede verse feo, es, con mucho, la forma más fácil de resolver el problema.
Robert Harvey
4
Aunque las respuestas dadas son muy buenas, creo que su código original es el más fácil de leer.
Uooo

Respuestas:

157

Guarde las condiciones en una lista:

List<string> conditions = new List<string>();

if (condition1) conditions.Add("Col1=0");
//...
if (conditions.Any())
    Query += " WHERE " + string.Join(" AND ", conditions.ToArray());
Ahmed KRAIEM
fuente
24
Buena solución, pero ToArray()no es necesaria con .NET 4 ya que hay una sobrecarga que acepta cualquiera IEnumerable<string>.
fero
101
Estoy emocionado por todas las oportunidades de inyección SQL que esto brinda.
asteri
12
@Jeff Si no está codificando los valores en la cláusula where, también puede tener una segunda lista con SqlParameters. Solo necesita completar esa lista al mismo tiempo que la lista de condiciones y llamar a AddRange (parameters.ToArray ()) al final.
Scott Chamberlain
5
@ScottChamberlain Sí, también puede simplemente escapar de las cadenas de entrada antes de ponerlas en la lista. La mayoría de las veces solo estaba advirtiendo contra un posible ataque con humor gracioso.
asteri
4
@Jeff solo es vulnerable a la inyección de SQL si las condiciones incluyen la entrada del usuario (el ejemplo original no lo hace)
D Stanley
85

Una solución es simplemente no escribir consultas manualmente agregando cadenas. Puede usar un ORM, como Entity Framework , y con LINQ to Entities usar las características que el lenguaje y el marco le ofrecen:

using (var dbContext = new MyDbContext())
{
    IQueryable<Table1Item> query = dbContext.Table1;

    if (condition1)
    {
        query = query.Where(c => c.Col1 == 0);
    }
    if (condition2)
    {
        query = query.Where(c => c.Col2 == 1);
    }
    if (condition3)
    {
        query = query.Where(c => c.Col3 == 2);
    }   

    PrintResults(query);
}
CodeCaster
fuente
@vaheeds No entiendo esa pregunta. Ambos son ORM diferentes.
CodeCaster
Lo siento, estaba buscando para comparar el rendimiento de Dapper con otros ORM, y llegué aquí por Google, ¡así que pensé que la PrintResults(query)consulta generada se usará en Dapper como consulta!
vaheeds
@vaheeds está bien, pero no entender una respuesta no justifica un voto negativo. Si fueras tú, lo que casualmente sucedió al mismo tiempo que tu comentario.
CodeCaster
su derecho, eso fue un malentendido. Sufro de linq a entidades de mal desempeño en consultas complicadas.
Compensé el
Esa no es una respuesta a la pregunta
HGMamaci
17

Un poco exagerado en este caso simple, pero he usado un código similar a este en el pasado.

Crea una función

string AddCondition(string clause, string appender, string condition)
{
    if (clause.Length <= 0)
    {
        return String.Format("WHERE {0}",condition);
    }
    return string.Format("{0} {1} {2}", clause, appender, condition);
}

Úselo así

string query = "SELECT * FROM Table1 {0}";
string whereClause = string.Empty;

if (condition 1)
    whereClause = AddCondition(whereClause, "AND", "Col=1");

if (condition 2)
    whereClause = AddCondition(whereClause, "AND", "Col2=2");

string finalQuery = String.Format(query, whereClause);

De esta manera, si no se encuentran condiciones, ni siquiera se molesta en cargar una declaración where en la consulta y le ahorra al servidor SQL un microsegundo de procesamiento de la cláusula where basura cuando analiza la declaración SQL.

Alan Barber
fuente
No veo cómo esto lo hace más elegante. Ciertamente, no está más claro lo que está sucediendo aquí. Puedo ver el uso de esa función de utilidad, pero no es más elegante.
usr
le dio un voto para
informarnos
15

Hay otra solución, que puede que tampoco sea elegante, pero funciona y resuelve el problema:

String query = "SELECT * FROM Table1";
List<string> conditions = new List<string>();
// ... fill the conditions
string joiner = " WHERE ";
foreach (string condition in conditions) {
  query += joiner + condition;
  joiner = " AND "
}

Por:

  • lista de condiciones vacía, el resultado será simplemente SELECT * FROM Table1,
  • una sola condición será SELECT * FROM Table1 WHERE cond1
  • cada condición siguiente generará AND condN
Dariusz
fuente
6
Eso deja colgando WHEREsi no hay predicados; el 1 = 1 existe específicamente para evitar eso.
Gaius
Entonces, ¿cambiar a String query = "SELECT * FROM Table1";y string jointer = " WHERE ";?
Brendan Long
@BrendanLong Entonces, WHERE¿ ANDdeben colocarse las s entre condiciones?
PenguinCoder
@PenguinCoder Es difícil mostrar el código completo en un comentario. Me refería a reemplazar la string joinerlínea con string joiner = " WHERE ";y dejar la joiner = " AND ";línea en paz.
Brendan Long
@Gaius Supuse que las codiciones no están vacías, pero poner WHERE en joiner debería ser suficiente. ¡Gracias por el comentario!
Dariusz
11

Solo haz algo como esto:

using (var command = connection.CreateCommand())
{
    command.CommandText = "SELECT * FROM Table1";

    var conditions = "";
    if (condition1)
    {    
        conditions += "Col1=@val1 AND ";
        command.AddParameter("val1", 1);
    }
    if (condition2)
    {    
        conditions += "Col2=@val2 AND ";
        command.AddParameter("val2", 1);
    }
    if (condition3)
    {    
        conditions += "Col3=@val3 AND ";
        command.AddParameter("val3", 1);
    }
    if (conditions != "")
        command.CommandText += " WHERE " + conditions.Remove(conditions.Length - 5);
}

Es seguro para la inyección de SQL y en mi humilde opinión , es bastante limpio. El Remove()simplemente quita el último AND;

Funciona tanto si no se han establecido condiciones, si se ha establecido una o si se han establecido varias.

jgauffin
fuente
1
No estoy seguro (no use C # yo mismo) pero yo diría que conditions != nullsiempre es así true, ya que lo inicializa con ""(a menos que en C # "" == null). Probablemente debería ser un cheque, si conditionsno está vacío… ;-)
siegi
9

Solo agregue dos líneas al final.

string Query="SELECT * FROM Table1 WHERE 1=1 ";
if (condition1) Query+="AND Col1=0 ";
if (condition2) Query+="AND Col2=1 ";
if (condition3) Query+="AND Col3=2 ";
Query.Replace("1=1 AND ", "");
Query.Replace(" WHERE 1=1 ", "");

P.ej

SELECT * FROM Table1 WHERE 1=1 AND Col1=0 AND Col2=1 AND Col3=2 

se convertirá en

SELECT * FROM Table1 WHERE Col1=0 AND Col2=1 AND Col3=2 

Mientras

SELECT * FROM Table1 WHERE 1=1 

se convertirá en

SELECT * FROM Table1

=====================================

Gracias por señalar un defecto de esta solución:

"Esto podría interrumpir la consulta si, por cualquier motivo, una de las condiciones contiene el texto" 1 = 1 Y "o" DONDE 1 = 1 ". Este podría ser el caso si la condición contiene una subconsulta o intenta verificar si alguna columna contiene este texto, por ejemplo. Tal vez esto no sea un problema en su caso, pero debe tenerlo en cuenta ... "

Para deshacernos de este problema, necesitamos distinguir el "principal" DONDE 1 = 1 y los de la subconsulta, lo cual es fácil:

Simplemente haga que el DONDE "principal" sea especial: agregaría un signo "$"

string Query="SELECT * FROM Table1 WHERE$ 1=1 ";
if (condition1) Query+="AND Col1=0 ";
if (condition2) Query+="AND Col2=1 ";
if (condition3) Query+="AND Col3=2 ";

Entonces aún agregue dos líneas:

Query.Replace("WHERE$ 1=1 AND ", "WHERE ");
Query.Replace(" WHERE$ 1=1 ", "");
Milesma
fuente
1
Esto podría romper la consulta si, por cualquier motivo, una de las condiciones contiene el texto "1=1 AND "o " WHERE 1=1 ". Este podría ser el caso si la condición contiene una subconsulta o intenta verificar si alguna columna contiene este texto, por ejemplo. Tal vez esto no sea un problema en su caso, pero debe tenerlo en cuenta ...
siegi
8

Utilizar este:

string Query="SELECT * FROM Table1 WHERE ";
string QuerySub;
if (condition1) QuerySub+="AND Col1=0 ";
if (condition2) QuerySub+="AND Col2=1 ";
if (condition3) QuerySub+="AND Col3=2 ";

if (QuerySub.StartsWith("AND"))
    QuerySub = QuerySub.TrimStart("AND".ToCharArray());

Query = Query + QuerySub;

if (Query.EndsWith("WHERE "))
    Query = Query.TrimEnd("WHERE ".ToCharArray());
Anshuman
fuente
Esta respuesta funcionará y no hay nada realmente malo en ella, pero no creo que sea más limpia y simple que la pregunta original. QuerySubEn mi opinión, la búsqueda de cadenas no es mejor ni peor que usar el where 1=1truco. Pero es una contribución reflexiva.
comida para gatos
3
Hubo un error. Lo corrigió. Mi consulta habría fracasado si ninguna de las condiciones estuviera presente :-P Aún debo decir que Ahmed o CodeCaster para mí son las mejores soluciones. ¡Solo les presenté una alternativa para ustedes!
Anshuman
Esto todavía está mal, en general. Supongamos que lo fuera ... FROM SOMETABLE WHERE ; entonces el TrimEndrealmente reduciría esto a ... FROM SOMETABL. Si esto fue realmente un StringBuilder(que debería ser si tiene tanta manipulación de cadenas o más) puede simplemente Query.Length -= "WHERE ".Length;.
Mark Hurd
Mark, funciona. He probado esto en muchos proyectos. Pruébelo y descubrirá que lo hace.
Anshuman
8
feo como el infierno :) además puede crear hasta 7 cadenas si conté correctamente
Piotr Perak
5

¿Por qué no utilizar un generador de consultas existente? Algo como Sql Kata .

Admite condiciones complejas, uniones y subconsultas.

var query = new Query("Users").Where("Score", ">", 100).OrderByDesc("Score").Limit(100);

if(onlyActive)
{
   query.Where("Status", "active")
}

// or you can use the when statement

query.When(onlyActive, q => q.Where("Status", "active"))

funciona con Sql Server, MySql y PostgreSql.

amd
fuente
4

La solución literal más rápida a lo que estás preguntando que puedo pensar es esta:

string Query="SELECT * FROM Table1";
string Conditions = "";

if (condition1) Conditions+="AND Col1=0 ";
if (condition2) Conditions+="AND Col2=1 ";
if (condition3) Conditions+="AND Col3=2 ";

if (Conditions.Length > 0) 
  Query+=" WHERE " + Conditions.Substring(3);

No parece elegante, seguro, a lo que le recomendaría que consulte la recomendación de CodeCaster de usar un ORM. Pero si piensa en lo que esto está haciendo aquí, realmente no le preocupa "desperdiciar" 4 caracteres de memoria, y es muy rápido para una computadora mover un puntero 4 lugares.

Si tiene tiempo para aprender a usar un ORM, realmente podría valer la pena. Pero con respecto a esto, si está tratando de evitar que esa condición adicional golpee la base de datos SQL, esto lo hará por usted.

trevorgrayson
fuente
4

Si se trata de SQL Server , puede hacer que este código sea mucho más limpio.

Esto también asume un número conocido de parámetros, lo que puede ser una suposición pobre cuando pienso en las posibilidades.

En C #, usaría:

using (SqlConnection conn = new SqlConnection("connection string"))
{
    conn.Open();
    SqlCommand command = new SqlCommand()
    {
        CommandText = "dbo.sample_proc",
        Connection = conn,
        CommandType = CommandType.StoredProcedure
    };

    if (condition1)
        command.Parameters.Add(new SqlParameter("Condition1", condition1Value));
    if (condition2)
        command.Parameters.Add(new SqlParameter("Condition2", condition2Value));
    if (condition3)
        command.Parameters.Add(new SqlParameter("Condition3", condition3Value));

    IDataReader reader = command.ExecuteReader();

    while(reader.Read())
    {
    }

    conn.Close();
}

Y luego en el lado de SQL:

CREATE PROCEDURE dbo.sample_proc
(
    --using varchar(50) generically
    -- "= NULL" makes them all optional parameters
    @Condition1 varchar(50) = NULL
    @Condition2 varchar(50) = NULL
    @Condition3 varchar(50) = NULL
)
AS
BEGIN
    /*
    check that the value of the parameter 
    matches the related column or that the 
    parameter value was not specified.  This
    works as long as you are not querying for 
    a specific column to be null.*/
    SELECT *
    FROM SampleTable
    WHERE (Col1 = @Condition1 OR @Condition1 IS NULL)
    AND   (Col2 = @Condition2 OR @Condition2 IS NULL)
    AND   (Col3 = @Condition3 OR @Condition3 IS NULL)
    OPTION (RECOMPILE)
    --OPTION(RECOMPILE) forces the query plan to remain effectively uncached
END
mckeejm
fuente
Ocultar sus columnas dentro de una expresión puede evitar el uso de índices, y esta técnica no se recomienda por esta razón aquí .
bbsimonbb
eso es un hallazgo interesante. Gracias por esa información. se actualizará
mckeejm
3

Dependiendo de la condición, podría ser posible utilizar lógica booleana en la consulta. Algo como esto :

string Query="SELECT * FROM Table1  " +
             "WHERE (condition1 = @test1 AND Col1=0) "+
             "AND (condition2 = @test2 AND Col2=1) "+
             "AND (condition3 = @test3 AND Col3=2) ";
Rémi
fuente
3

Me gusta la interfaz fluida de Stringbuilder, así que hice algunos ExtensionMethods.

var query = new StringBuilder()
    .AppendLine("SELECT * FROM products")
    .AppendWhereIf(!String.IsNullOrEmpty(name), "name LIKE @name")
    .AppendWhereIf(category.HasValue, "category = @category")
    .AppendWhere("Deleted = @deleted")
    .ToString();

var p_name = GetParameter("@name", name);
var p_category = GetParameter("@category", category);
var p_deleted = GetParameter("@deleted", false);
var result = ExecuteDataTable(query, p_name, p_category, p_deleted);


// in a seperate static class for extensionmethods
public StringBuilder AppendLineIf(this StringBuilder sb, bool condition, string value)
{
    if(condition)
        sb.AppendLine(value);
    return sb;
}

public StringBuilder AppendWhereIf(this StringBuilder sb, bool condition, string value)
{
    if (condition)
        sb.AppendLineIf(condition, sb.HasWhere() ? " AND " : " WHERE " + value);
    return sb;
}

public StringBuilder AppendWhere(this StringBuilder sb, string value)
{
    sb.AppendWhereIf(true, value);
    return sb;
}

public bool HasWhere(this StringBuilder sb)
{
    var seperator = new string [] { Environment.NewLine };
    var lines = sb.ToString().Split(seperator, StringSplitOptions.None);
    return lines.Count > 0 && lines[lines.Count - 1].Contains("where", StringComparison.InvariantCultureIgnoreCase);
}

// http://stackoverflow.com/a/4217362/98491
public static bool Contains(this string source, string toCheck, StringComparison comp)
{
    return source.IndexOf(toCheck, comp) >= 0;
}
Jürgen Steinblock
fuente
2

En mi humilde opinión, creo que su enfoque es incorrecto:

Consultar la base de datos concatenando cadenas NUNCA es una buena idea (riesgo de inyección de SQL y el código puede romperse fácilmente si realiza algunos cambios en otro lugar).

Puedes usar un ORM (yo uso NHibernate ) o al menos usarSqlCommand.Parameters

Si absolutamente desea usar la concatenación de cadenas, usaría un StringBuilder(es el objeto correcto para la concatenación de cadenas):

var query = new StringBuilder("SELECT * FROM Table1 WHERE");
int qLength = query.Length;//if you don't want to count :D
if (Condition1) query.Append(" Col1=0 AND");
if (Condition2) query.Append(" Col2=0 AND");
....
//if no condition remove WHERE or AND from query
query.Length -= query.Length == qLength ? 6 : 4;

Como último pensamiento, Where 1=1es realmente feo pero SQL Server lo optimizará de todos modos.

giammin
fuente
SELECT * FROM Table1 WHERE AND Col1=0no parece correcto, que es el punto de WHERE 1=1.
Mormegil
2

Dapper SqlBuilder es una opción bastante buena. Incluso se usa en producción en StackOverflow.

Lea la entrada del blog de Sam al respecto. .

Hasta donde yo sé, no es parte de ningún paquete de Nuget, por lo que deberá copiar y pegar su código en su proyecto o descargar la fuente de Dapper y compilar el proyecto SqlBuilder. De cualquier manera, también deberá hacer referencia a Dapper para la DynamicParametersclase.

Ronnie Overby
fuente
1
No creo que SqlBuilder de Dapper esté incluido en ese paquete.
Ronnie Overby
1

Veo que esto se usa todo el tiempo en Oracle mientras se construye SQL dinámico dentro de los procedimientos almacenados . Lo uso en consultas mientras exploro problemas de datos también para hacer que el cambio entre diferentes filtros de datos sea más rápido ... Simplemente comente una condición o vuelva a agregarla fácilmente.

Encuentro que es bastante común y bastante fácil de entender para alguien que revisa su código.

Don Boling
fuente
1
public static class Ext
{
    public static string addCondition(this string str, bool condition, string statement)
    {
        if (!condition)
            return str;

        return str + (!str.Contains(" WHERE ") ? " WHERE " : " ") + statement;
    }

    public static string cleanCondition(this string str)
    {
        if (!str.Contains(" WHERE "))
            return str;

        return str.Replace(" WHERE AND ", " WHERE ").Replace(" WHERE OR ", " WHERE ");
    }
}

Realización con métodos de extensión.

    static void Main(string[] args)
    {
        string Query = "SELECT * FROM Table1";

        Query = Query.addCondition(true == false, "AND Column1 = 5")
            .addCondition(18 > 17, "AND Column2 = 7")
            .addCondition(42 == 1, "OR Column3 IN (5, 7, 9)")
            .addCondition(5 % 1 > 1 - 4, "AND Column4 = 67")
            .addCondition(Object.Equals(5, 5), "OR Column5 >= 0")
            .cleanCondition();

        Console.WriteLine(Query);
    }
Maxim Zhukov
fuente
¡CONTRA EL GRANO!
Ronnie Overby
¿Disculpe? ¿Que quieres decir?
Maxim Zhukov
0

Usando la stringfunción también puede hacerlo de esta manera:

string Query = "select * from Table1";

if (condition1) WhereClause += " Col1 = @param1 AND "; // <---- put conditional operator at the end
if (condition2) WhereClause += " Col1 = @param2 OR ";

WhereClause = WhereClause.Trim();

if (!string.IsNullOrEmpty(WhereClause))
    Query = Query + " WHERE " + WhereClause.Remove(WhereClause.LastIndexOf(" "));
// else
// no condition meets the criteria leave the QUERY without a WHERE clause  

Personalmente, me parece fácil eliminar los elementos condicionales al final, ya que su posición es fácil de predecir.

Nunca sin esperanza
fuente
0

Pensé en una solución que, bueno, quizás sea algo más legible:

string query = String.Format("SELECT * FROM Table1 WHERE "
                             + "Col1 = {0} AND "
                             + "Col2 = {1} AND "
                             + "Col3 = {2}",
                            (!condition1 ? "Col1" : "0"),
                            (!condition2 ? "Col2" : "1"),
                            (!condition3 ? "Col3" : "2"));

Simplemente no estoy seguro de si el intérprete de SQL también optimizará la Col1 = Col1condición (impresa cuando condition1es falsa).

CodeCaster
fuente
0

Aquí hay una forma más elegante:

    private string BuildQuery()
    {
        string MethodResult = "";
        try
        {
            StringBuilder sb = new StringBuilder();

            sb.Append("SELECT * FROM Table1");

            List<string> Clauses = new List<string>();

            Clauses.Add("Col1 = 0");
            Clauses.Add("Col2 = 1");
            Clauses.Add("Col3 = 2");

            bool FirstPass = true;

            if(Clauses != null && Clauses.Count > 0)
            {
                foreach(string Clause in Clauses)
                {
                    if (FirstPass)
                    {
                        sb.Append(" WHERE ");

                        FirstPass = false;

                    }
                    else
                    {
                        sb.Append(" AND ");

                    }

                    sb.Append(Clause);

                }

            }

            MethodResult = sb.ToString();

        }
        catch //(Exception ex)
        {
            //ex.HandleException()
        }
        return MethodResult;
    }
WonderWorker
fuente
0

Como se ha dicho, la creación de SQL mediante concatenación nunca es una buena idea . No solo por la inyección SQL. Sobre todo porque es feo, difícil de mantener y totalmente innecesario . Tienes que ejecutar tu programa con rastreo o depuración para ver qué SQL genera. Si usa QueryFirst (descargo de responsabilidad: que escribí), la tentación infeliz se elimina y puede comenzar directamente a hacerlo en SQL.

Esta página tiene una cobertura completa de las opciones de TSQL para agregar predicados de búsqueda de forma dinámica. La siguiente opción es útil para situaciones en las que desea dejar la elección de combinaciones de predicados de búsqueda a su usuario.

select * from table1
where (col1 = @param1 or @param1 is null)
and (col2 = @param2 or @param2 is null)
and (col3 = @param3 or @param3 is null)
OPTION (RECOMPILE)

QueryFirst le da C # nulo a db NULL, por lo que simplemente llama al método Execute () con nulos cuando sea apropiado, y todo simplemente funciona. <opinion> ¿Por qué los desarrolladores de C # son tan reacios a hacer cosas en SQL, incluso cuando es más simple? Mente aturdida. </opinion>

bbsimonbb
fuente
0

Para pasos de filtrado más largos, StringBuilder es el mejor enfoque, como muchos dicen.

en tu caso iría con:

StringBuilder sql = new StringBuilder();

if (condition1) 
    sql.Append("AND Col1=0 ");
if (condition2) 
    sql.Append("AND Col2=1 ");
if (condition3) 
    sql.Append("AND Col3=2 ");

string Query = "SELECT * FROM Table1 ";
if(sql.Length > 0)
 Query += string.Concat("WHERE ", sql.ToString().Substring(4)); //avoid first 4 chars, which is the 1st "AND "
HGMamaci
fuente
0

Conciso, elegante y dulce, como se muestra en la imagen de abajo.

ingrese la descripción de la imagen aquí

usuario1451111
fuente