Existe uma maneira melhor de construir dinamicamente uma cláusula SQL WHERE do que usando 1 = 1 em seu início?

110

Estou construindo alguma consulta SQL em C #. Ele será diferente dependendo de algumas condições armazenadas como variáveis ​​no 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, mas testar 1 = 1 não parece elegante. Se eu não usasse, teria que lembrar e verificar sempre se a palavra-chave "where" já foi adicionada ou não à consulta.

Existe uma solução mais agradável?

RRM
fonte
118
Para ser honesto - eu faria isso como este, também, mas eu usaria 42 = 42;-)
fero
5
Na verdade, sempre escrevo minhas perguntas assim. Torna mais fácil comentar uma condição
Deruijter
4
@catfood O primeiro projeto em que participei como estagiário foi escrever ferramentas para ajudar a analisar as consultas de desempenho em nossos servidores Sybase. Uma descoberta divertida foram as centenas de milhares de Select 42perguntas que estávamos recebendo. (não era divertido tentar rastrear a fonte)
Mr.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 isso que você usa 1 = 1. O mecanismo de banco de dados o otimiza de qualquer maneira, portanto, embora possa parecer feio, é de longe a maneira mais fácil de resolver o problema.
Robert Harvey
4
Embora as respostas fornecidas sejam muito boas, acho que seu código original é o mais fácil de ler.
Uooo

Respostas:

157

Salve as condições em uma 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
fonte
24
Boa solução, mas ToArray()não é necessário com o .NET 4, pois há uma sobrecarga que aceita qualquer um IEnumerable<string>.
fero
101
Estou animado com todas as oportunidades de injeção de SQL que isso oferece.
asteri
12
@Jeff Se você não estiver codificando os valores na cláusula where, você também pode ter uma segunda lista com SqlParameters. Você só precisa preencher essa lista ao mesmo tempo que a lista de condições e chamar AddRange (parameters.ToArray ()) no final.
Scott Chamberlain
5
@ScottChamberlain Sim, você também pode simplesmente escapar das strings de entrada antes de colocá-las na lista. Eu estava apenas alertando contra um possível ataque usando humor jocoso.
asteri
4
@Jeff só é vulnerável à injeção de SQL se as condições incluírem entrada do usuário (o exemplo original não inclui)
D Stanley
85

Uma solução é simplesmente não escrever consultas manualmente anexando strings. Você poderia usar um ORM, como Entity Framework , e com LINQ to Entities usar os recursos que a linguagem e a estrutura oferecem a você:

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
fonte
@vaheeds Não entendo essa pergunta. Ambos são ORMs diferentes.
CodeCaster
Desculpe, eu estava pesquisando para comparar o desempenho do dapper com outros ORMs, e cheguei aqui pelo google, então pensei que a PrintResults(query)consulta gerada então usaria no dapper como consulta !!
vaheeds
@vaheeds tudo bem, mas não entender uma resposta não garante uma votação negativa. Se fosse você, o que coincidentemente aconteceu ao mesmo tempo que seu comentário.
CodeCaster
seu direito, isso foi um mal-entendido. Eu sofro de linq para entidades de mau desempenho em consultas complicadas.
Compensei
Isso não é uma resposta para a pergunta
HGMamaci
17

Um pouco de exagero neste caso simples, mas já usei um código semelhante a este no passado.

Crie uma função

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

Use assim

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

Dessa forma, se nenhuma condição for encontrada, você nem mesmo se preocupa em carregar uma instrução where na consulta e salva o servidor sql um microssegundo de processamento da cláusula where do lixo quando ele analisa a instrução sql.

Alan Barber
fonte
Não vejo como isso o torna mais elegante. Certamente não está mais claro o que está acontecendo aqui. Posso ver o uso dessa função de utilidade, mas não é mais elegante.
usr
deu a você um voto para nos esclarecer sobre a importância de um microssegundo
usuário1451111
15

Existe outra solução, que também pode não ser elegante, mas funciona e resolve o 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 "
}

Para:

  • lista de condições vazia, o resultado será simplesmente SELECT * FROM Table1,
  • uma única condição será SELECT * FROM Table1 WHERE cond1
  • cada condição a seguir irá gerar AND condN
Dariusz
fonte
6
Isso deixa uma dúvida WHEREse não houver predicados; o 1 = 1 existe especificamente para evitar isso.
Gaius
Então mude para String query = "SELECT * FROM Table1";e string jointer = " WHERE ";?
Brendan Long
@BrendanLong Então WHEREos ANDs devem ser colocados entre as condições?
PenguinCoder
@PenguinCoder É difícil mostrar o código completo em um comentário. Eu quis dizer substituir a string joinerlinha por string joiner = " WHERE ";e deixar a joiner = " AND ";linha sozinha.
Brendan Long
@Gaius Presumi que as codições não estão vazias, mas colocar WHERE no joiner deve resolver. Obrigado pela observação!
Dariusz
11

Basta fazer algo assim:

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

É seguro para injeção de SQL e IMHO , é bastante limpo. O Remove()simplesmente remove o último AND;

Funciona tanto se nenhuma condição tiver sido definida, se uma tiver sido definida ou se várias forem definidas.

jgauffin
fonte
1
Não tenho certeza (não uso C # sozinho), mas diria que conditions != nullé sempre true, conforme você inicializa com ""(a menos que em C # "" == null). Provavelmente deveria ser um cheque, se conditionsnão estiver vazio ... ;-)
siegi
9

Basta anexar duas linhas no 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 ", "");

Por exemplo

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

se tornará para

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

Enquanto

SELECT * FROM Table1 WHERE 1=1 

se tornará para

SELECT * FROM Table1

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

Obrigado por apontar uma falha desta solução:

"Isso pode interromper a consulta se, por qualquer motivo, uma das condições contiver o texto" 1 = 1 AND "ou" WHERE 1 = 1 ". Este pode ser o caso se a condição contiver uma subconsulta ou tentar verificar se algum coluna contém este texto, por exemplo. Talvez isso não seja um problema no seu caso, mas você deve ter isso em mente ... "

Para nos livrarmos desse problema, precisamos distinguir o "principal" WHERE 1 = 1 e os da subconsulta, o que é fácil:

Basta fazer o "principal" ONDE especial: Eu acrescentaria um sinal "$"

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

Em seguida, ainda acrescente duas linhas:

Query.Replace("WHERE$ 1=1 AND ", "WHERE ");
Query.Replace(" WHERE$ 1=1 ", "");
milesma
fonte
1
Isso pode interromper a consulta se, por qualquer motivo, uma das condições contiver o texto "1=1 AND "ou " WHERE 1=1 ". Pode ser o caso se a condição contiver uma subconsulta ou tentar verificar se alguma coluna contém esse texto, por exemplo. Talvez isso não seja um problema no seu caso, mas você deve ter isso em mente ...
siegi
8

Usa isto:

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
fonte
Essa resposta funcionará e não há nada de errado com ela, mas não acho que seja mais clara e simples do que a pergunta original. A pesquisa de strings não QuerySubé, em minha opinião, nem melhor nem pior do que usar o where 1=1hack. Mas é uma contribuição atenciosa.
catfood
3
Havia um erro. Corrigido. Minha pergunta teria sido um fracasso se nenhuma das condições estivesse presente :-P Ainda assim, devo dizer que Ahmed ou CodeCaster para mim são as melhores soluções. Só apresentei uma alternativa para vocês!
Anshuman
Isso ainda está errado, em geral. Suponha que fosse ... FROM SOMETABLE WHERE ; então o TrimEndreduziria para ... FROM SOMETABL. Se este for realmente um StringBuilder(o que deveria ser se você tiver tanta manipulação de strings ou mais), você pode simplesmente Query.Length -= "WHERE ".Length;.
Mark Hurd
Mark, funciona. Eu tentei isso em muitos projetos. Experimente e você descobrirá que sim!
Anshuman
8
feio pra caramba :) além disso, ele pode criar até 7 cordas se eu contar corretamente
Piotr Perak
5

Por que não usar um Query Builder existente? Algo como Sql Kata .

Suporta condições onde complexas, junções e 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 com Sql Server, MySql e PostgreSql.

amd
fonte
4

A solução literal mais rápida para o que você está perguntando é 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);

Não parece elegante, claro, para o qual eu recomendaria a recomendação da CodeCaster de usar um ORM. Mas se você pensar sobre o que isso está fazendo aqui, você realmente não está preocupado em 'desperdiçar' 4 caracteres de memória, e é muito rápido para um computador mover um ponteiro 4 lugares.

Se você tiver tempo para aprender como usar um ORM, pode realmente valer a pena. Mas com relação a isso, se você está tentando evitar que essa condição adicional atinja o banco de dados SQL, isso fará isso por você.

Trevorgrayson
fonte
4

Se este for o SQL Server , você pode tornar esse código muito mais limpo.

Isso também pressupõe um número conhecido de parâmetros, o que pode ser uma suposição ruim quando penso sobre as possibilidades.

Em C #, você usaria:

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

E então no lado do 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
fonte
Ocultar suas colunas dentro de uma expressão pode impedir o uso de índices, e essa técnica é desencorajada por esse motivo aqui .
bbsimonbb
esse é um achado interessante. Obrigado por essa informação. será atualizado
mckeejm
3

Dependendo da condição, pode ser possível usar a lógica booleana na consulta. Algo assim :

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
fonte
3

Eu gosto da interface fluente do stringbuilder, então fiz alguns 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
fonte
2

IMHO, acho que sua abordagem está errada:

Consultar o banco de dados concatenando string NUNCA é uma boa ideia (risco de injeção de SQL e o código pode ser facilmente quebrado se você fizer algumas alterações em outro lugar).

Você pode usar um ORM (eu uso NHibernate ) ou pelo menos usarSqlCommand.Parameters

Se você realmente deseja usar concatenação de string, eu usaria um StringBuilder(é o objeto certo para concatenação de string):

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 o último pensamento, Where 1=1é realmente feio, mas o SQL Server irá otimizá-lo de qualquer maneira.

giammin
fonte
SELECT * FROM Table1 WHERE AND Col1=0não parece correto, e esse é o ponto principal WHERE 1=1.
Mormegil
2

O Dapper SqlBuilder é uma opção muito boa. É até usado na produção no StackOverflow.

Leia a entrada do blog de Sam sobre isso .

Até onde eu sei, não faz parte de nenhum pacote Nuget, então você precisará copiar e colar seu código em seu projeto ou baixar o código-fonte do Dapper e construir o projeto SqlBuilder. De qualquer forma, você também precisará fazer referência a Dapper para a DynamicParametersclasse.

Ronnie Overby
fonte
1
Não acho que o SqlBuilder do Dapper esteja incluído nesse pacote.
Ronnie Overby
1

Vejo que isso é usado o tempo todo no Oracle ao construir SQL dinâmico em procedimentos armazenados . Eu o uso em consultas enquanto exploro problemas de dados também para tornar mais rápida a alternância entre diferentes filtros de dados ... Basta comentar uma condição ou adicioná-la novamente facilmente.

Acho que é bastante comum e fácil de entender para alguém que está revisando seu código.

Don Boling
fonte
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 ");
    }
}

Realização com métodos de extensão.

    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
fonte
CONTRA O GRÃO!
Ronnie Overby
Com licença? O que quer dizer?
Maxim Zhukov
0

Usando a stringfunção, você também pode fazer desta forma:

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  

Eu pessoalmente acho fácil remover o (s) elemento (s) condicional (is) no final, uma vez que sua posição é fácil de prever.

NeverHopeless
fonte
0

Pensei em uma solução que, bem, talvez seja um pouco mais legível:

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

Só não tenho certeza se o interpretador SQL também otimizará a Col1 = Col1condição (impresso quando condition1é falso).

CodeCaster
fonte
0

Aqui está uma maneira mais 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
fonte
0

Como já foi dito, criar SQL por concatenação nunca é uma boa ideia . Não apenas por causa da injeção de SQL. Principalmente porque é simplesmente feio, difícil de manter e totalmente desnecessário . Você tem que executar seu programa com rastreamento ou depuração para ver qual SQL ele gera. Se você usar QueryFirst (aviso: que eu escrevi) a tentação infeliz é removida, e você pode ir direto ao fazer em SQL.

Esta página tem uma cobertura abrangente de opções de TSQL para adicionar predicados de pesquisa dinamicamente. A opção a seguir é útil para situações em que você deseja deixar a escolha de combinações de predicados de pesquisa para seu usuário.

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 fornece C # null a db NULL, então você apenas chama o método Execute () com valores nulos quando apropriado e tudo funciona. <opinion> Por que os desenvolvedores de C # são tão relutantes em fazer coisas em SQL, mesmo quando é mais simples. Incrivelmente. </opinion>

bbsimonbb
fonte
0

Para etapas de filtragem mais longas, StringBuilder é a melhor abordagem, como muitos dizem.

no seu caso eu iria com:

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
fonte
0

Conciso, elegante e doce, como mostra a imagem abaixo.

insira a descrição da imagem aqui

user1451111
fonte