Esiste un modo migliore per creare dinamicamente una clausola SQL WHERE rispetto all'utilizzo di 1 = 1 all'inizio?


110

Sto costruendo alcune query SQL in C #. Sarà diverso a seconda di alcune condizioni memorizzate come variabili nel codice.

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

Funziona, ma testare 1 = 1 non sembra elegante. Se non lo usassi, dovrei ricordare e controllare ogni volta se la parola chiave "dove" è già stata aggiunta o meno alla query.

C'è una soluzione migliore?


118
Ad essere sincero, lo farei anche io, ma 42 = 42
userei

5
In realtà scrivo sempre le mie domande in questo modo. Rende più facile commentare una condizione
Deruijter

4
@catfood Il primo progetto a cui ho partecipato come stagista è stato scrivere strumenti per analizzare le query sulle prestazioni rispetto ai nostri server Sybase. Una scoperta divertente sono state le centinaia di migliaia di Select 42richieste che stavamo ricevendo. (non è stato divertente cercare di rintracciare la 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- Ecco perché usi 1 = 1. Il motore di database lo ottimizza comunque, quindi anche se potrebbe sembrare brutto, è di gran lunga il modo più semplice per risolvere il problema.
Robert Harvey,

4
Sebbene le risposte fornite siano molto belle, penso che il tuo codice originale sia il più facile da leggere.
Uooo

Risposte:


157

Salva le condizioni in un elenco:

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

if (condition1) conditions.Add("Col1=0");
//...
if (conditions.Any())
    Query += " WHERE " + string.Join(" AND ", conditions.ToArray());

24
Buona soluzione, ma ToArray()non è necessaria con .NET 4 in quanto esiste un sovraccarico che accetta qualsiasi IEnumerable<string>.
venerdì

101
Sono entusiasta di tutte le opportunità di SQL injection che questo fornisce.
asteri

12
@ Jeff Se non si stanno codificando i valori nella clausola where, è possibile avere anche un secondo elenco con SqlParameters. Hai solo bisogno di popolare quell'elenco contemporaneamente all'elenco delle condizioni e chiamare AddRange (parameters.ToArray ()) alla fine.
Scott Chamberlain

5
@ScottChamberlain Sì, potresti anche semplicemente sfuggire alle stringhe di input prima di inserirle nell'elenco. Per lo più stavo solo mettendo in guardia contro un possibile attacco usando umorismo faceto.
asteri

4
@Jeff è vulnerabile all'iniezione SQL solo se le condizioni includono l'input dell'utente (l'esempio originale non lo fa)
D Stanley

85

Una soluzione è semplicemente non scrivere query manualmente aggiungendo stringhe. È possibile utilizzare un ORM, come Entity Framework , e con LINQ to Entities utilizzare le funzionalità che il linguaggio e il framework offrono:

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

@vaheeds Non capisco questa domanda. Entrambi sono ORM diversi.
CodeCaster

Scusa, stavo cercando di confrontare le prestazioni di dapper con altri ORM e sono arrivato qui da Google, quindi ho pensato che la PrintResults(query)query generata verrà utilizzata in dapper come query !!
vaheeds

@vaheeds va bene, ma non capire una risposta non garantisce un voto negativo. Se eri tu, cosa che per coincidenza è accaduta contemporaneamente al tuo commento.
CodeCaster

hai ragione, è stato un malinteso. Soffro di linq a entità cattive prestazioni su query complicate. Ho compensato il voto
negativo

Questa non è una risposta alla domanda
HGMamaci

17

Un po 'di eccessivo in questo semplice caso, ma ho usato un codice simile a questo in passato.

Crea una funzione

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

Usalo in questo modo

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

In questo modo, se non vengono trovate condizioni, non ti preoccupi nemmeno di caricare un'istruzione where nella query e di risparmiare al server sql un micro-secondo di elaborazione della clausola junk where quando analizza l'istruzione sql.


Non vedo come questo lo renda più elegante. Non è certo più chiaro cosa stia succedendo qui. Posso vedere l'uso di quella funzione di utilità, ma non è più elegante.
usr

ti ha dato un voto per averci illuminato sull'importanza di un micro-secondo
user1451111

15

C'è un'altra soluzione, che potrebbe anche non essere elegante, ma funziona e risolve il 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 "
}

Per:

  • elenco delle condizioni vuoto, il risultato sarà semplicemente SELECT * FROM Table1,
  • una sola condizione sarà SELECT * FROM Table1 WHERE cond1
  • ogni condizione seguente genererà ulteriori AND condN

6
Ciò lascia penzoloni WHEREse non ci sono predicati; l'1 = 1 esiste specificamente per evitarlo.
Gaio

Quindi passare a String query = "SELECT * FROM Table1";e string jointer = " WHERE ";?
Brendan Long,

@BrendanLong Poi WHEREvengono la ANDs da interporre tra le condizioni?
PenguinCoder

@PenguinCoder È difficile mostrare il codice completo in un commento. Intendevo sostituire la string joinerlinea con string joiner = " WHERE ";e lasciare la joiner = " AND ";linea da sola.
Brendan Long,

@ Gaio ho pensato che le codizioni non fossero vuote, ma mettere WHERE in joiner dovrebbe funzionare. Grazie per l'osservazione!
Dariusz

11

Basta fare qualcosa del genere:

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

È sicuro di SQL injection e IMHO , è abbastanza pulito. Il Remove()rimuove semplicemente l'ultima AND;

Funziona sia se non sono state impostate condizioni, sia se ne è stata impostata una o se ne sono state impostate più.


1
Non sono sicuro (non usare C # da solo) ma direi che conditions != nullè sempre true, poiché lo si inizializza con ""(a meno che in C # "" == null). Probabilmente dovrebbe essere un assegno, se conditionsnon è vuoto… ;-)
siegi

9

Aggiungi solo due righe sul retro.

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

Per esempio

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

diventerà a

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

Mentre

SELECT * FROM Table1 WHERE 1=1 

diventerà a

SELECT * FROM Table1

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

Grazie per aver segnalato un difetto di questa soluzione:

"Ciò potrebbe interrompere la query se, per qualsiasi motivo, una delle condizioni contiene il testo" 1 = 1 AND "o" WHERE 1 = 1 ". Questo potrebbe essere il caso se la condizione contiene una sottoquery o tenta di verificare se alcuni la colonna contiene questo testo, ad esempio. Forse questo non è un problema nel tuo caso, ma dovresti tenerlo presente ... "

Per eliminare questo problema, dobbiamo distinguere il "main" WHERE 1 = 1 e quelli della sottoquery, il che è facile:

Rendi semplicemente "principale" WHERE speciale: aggiungerei un segno "$"

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

Quindi aggiungi ancora due righe:

Query.Replace("WHERE$ 1=1 AND ", "WHERE ");
Query.Replace(" WHERE$ 1=1 ", "");

1
Ciò potrebbe interrompere la query se, per qualsiasi motivo, una delle condizioni contiene il testo "1=1 AND "o " WHERE 1=1 ". Questo potrebbe essere il caso se la condizione contiene una sottoquery o cerca di controllare se alcune colonne contengono questo testo, ad esempio. Forse questo non è un problema nel tuo caso, ma dovresti tenerlo presente ...
siegi

8

Usa questo:

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

Questa risposta funzionerà e non c'è niente di veramente sbagliato in essa, ma non penso che sia più pulita e semplice della domanda originale. La ricerca delle stringhe QuerySubsecondo me non è né migliore né peggiore dell'utilizzo where 1=1dell'hack. Ma è un contributo premuroso.
cibo per gatti

3
C'era un errore. Corretto. La mia domanda sarebbe bombardata se nessuna delle condizioni fosse presente :-P Devo comunque dire che Ahmed o CodeCaster per me sono le migliori soluzioni. Ho presentato solo un'alternativa per voi ragazzi!
Anshuman,

Questo è ancora sbagliato, in generale. Supponiamo che lo fosse ... FROM SOMETABLE WHERE ; quindi lo TrimEndridurrebbe effettivamente a ... FROM SOMETABL. Se questo fosse effettivamente un StringBuilder(che dovrebbe essere se hai così tante manipolazioni delle stringhe o più) puoi semplicemente Query.Length -= "WHERE ".Length;.
Mark Hurd

Mark, funziona. L'ho provato in molti progetti. Provalo e scoprirai che lo fa!
Anshuman,

8
brutto da morire :) in più può creare fino a 7 stringhe se contato correttamente
Piotr Perak

5

Perché non utilizzare un Query Builder esistente? Qualcosa come Sql Kata .

Supporta condizioni complesse, join e sottoquery.

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

funziona con Sql Server, MySql e PostgreSql.


4

La soluzione letterale più rapida a ciò che stai chiedendo a cui posso pensare è questa:

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

Non sembra elegante, certo, a cui ti rimando alla raccomandazione di CodeCaster di utilizzare un ORM. Ma se pensi a quello che sta facendo qui, non sei davvero preoccupato di "sprecare" 4 caratteri di memoria, ed è davvero veloce per un computer spostare un puntatore di 4 posizioni.

Se hai il tempo di imparare a usare un ORM, potrebbe davvero ripagarti. Ma per quanto riguarda questo, se stai cercando di impedire a quella condizione aggiuntiva di colpire il database SQL, questo lo farà per te.


4

Se questo è SQL Server , puoi rendere questo codice molto più pulito.

Ciò presuppone anche un numero noto di parametri, che può essere una cattiva ipotesi quando penso alle possibilità.

In C # useresti:

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 poi sul lato 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

Nascondere le colonne all'interno di un'espressione può impedire l'uso degli indici e questa tecnica è sconsigliata per questo motivo qui .
bbsimonbb

è una scoperta interessante. Grazie per queste informazioni. si aggiornerà
mckeejm

3

A seconda della condizione, potrebbe essere possibile utilizzare la logica booleana nella query. Qualcosa come questo :

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

3

Mi piace l'interfaccia fluida di stringbuilder, quindi ho creato alcuni 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;
}

2

IMHO, penso che il tuo approccio sia sbagliato:

Interrogare il database concatenando la stringa non è MAI una buona idea (il rischio di iniezione SQL e il codice può essere facilmente interrotto se si apportano modifiche altrove).

Puoi usare un ORM (io uso NHibernate ) o almeno usareSqlCommand.Parameters

Se vuoi assolutamente usare la concatenazione di stringhe, io userei a StringBuilder(è l'oggetto giusto per la concatenazione di stringhe):

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;

Come l'ultimo pensiero, Where 1=1è davvero brutto ma SQL Server lo ottimizzerà comunque.


SELECT * FROM Table1 WHERE AND Col1=0non sembra corretto, che è l'intero punto di WHERE 1=1.
Mormegil

2

Dapper SqlBuilder è un'opzione piuttosto buona. Viene persino utilizzato nella produzione su StackOverflow.

Leggi il post sul blog di Sam a riguardo .

Per quanto ne so, non fa parte di alcun pacchetto Nuget, quindi dovrai copiare e incollare il suo codice nel tuo progetto o scaricare il sorgente Dapper e costruire il progetto SqlBuilder. In ogni caso, dovrai anche fare riferimento a Dapper per la DynamicParametersclasse.


1
Non penso che SqlBuilder di Dapper sia incluso in quel pacchetto.
Ronnie Overby

1

Vedo questo utilizzato tutto il tempo in Oracle durante la creazione di SQL dinamico all'interno di stored procedure . Lo uso nelle query mentre esploro anche i problemi dei dati solo per rendere più veloce il passaggio tra diversi filtri di dati ... È sufficiente commentare una condizione o aggiungerla di nuovo facilmente.

Trovo che sia abbastanza comune e abbastanza facile da capire a qualcuno che rivede il tuo codice.


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

Realizzazione con metodi di estensione.

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

ANDANDO CONTRO IL GRANO!
Ronnie Overby

Scusami? Cosa vuoi dire?
Maxim Zhukov

0

Usando la stringfunzione puoi farlo anche in questo modo:

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 ritengo facile rimuovere gli elementi condizionali alla fine, poiché la loro posizione è facile da prevedere.


0

Ho pensato a una soluzione che, beh, forse è un po 'più leggibile:

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

Non sono sicuro se l'interprete SQL ottimizzerà anche la Col1 = Col1condizione (stampata quando condition1è falsa).


0

Ecco un modo più 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;
    }

0

Come è stato affermato, la creazione di SQL mediante concatenazione non è mai una buona idea . Non solo a causa dell'iniezione SQL. Soprattutto perché è solo brutto, difficile da mantenere e totalmente inutile . Devi eseguire il tuo programma con trace o debug per vedere quale SQL genera. Se usi QueryFirst (disclaimer: che ho scritto) la tentazione infelice viene rimossa e puoi iniziare subito a farlo in SQL.

Questa pagina offre una copertura completa delle opzioni TSQL per l'aggiunta dinamica di predicati di ricerca. La seguente opzione è utile per le situazioni in cui si desidera lasciare all'utente la scelta delle combinazioni di predicati di ricerca.

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 ti dà da C # null a db NULL, quindi chiami il metodo Execute () con null quando appropriato, e tutto funziona. <opinione> Perché gli sviluppatori C # sono così riluttanti a fare cose in SQL, anche quando è più semplice. La mente sbalordisce. </opinion>


0

Per passaggi di filtraggio più lunghi StringBuilder è l'approccio migliore come molti dicono.

nel tuo caso andrei 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 "

0

Conciso, elegante e dolce, come mostrato nell'immagine qui sotto.

inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.