Есть ли лучший способ динамически построить предложение SQL WHERE, чем использование 1 = 1 в его начале?


110

Я создаю SQL- запрос на C #. Он будет отличаться в зависимости от некоторых условий, хранящихся в качестве переменных в коде.

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

Это работает, но проверка 1 = 1 не выглядит элегантной. Если бы я не использовал его, мне пришлось бы каждый раз запоминать и проверять, было ли уже добавлено ключевое слово «where» в запрос или нет.

Есть ли лучшее решение?


118
Если честно - я бы тоже так сделал, но я бы использовал 42 = 42;-)
— fero

5
На самом деле я всегда пишу свои запросы вот так. Облегчает комментирование условия
— Deruijter

4
@catfood Первый проект, в котором я участвовал в качестве стажера, заключался в написании инструментов, помогающих анализировать запросы производительности на наших серверах Sybase. Забавным открытием стали сотни тысяч Select 42запросов, которые мы получали. (не забавно пытаться отследить источник)
— Мистер Миндор

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- Вот почему вы используете 1 = 1. Ядро базы данных все равно оптимизирует его, поэтому, хотя это может выглядеть некрасиво, это, безусловно, самый простой способ решить проблему.
— Роберт Харви,

4
Хотя данные ответы очень хороши, я думаю, что ваш исходный код легче всего читать.
— Uooo

Ответы:


157

Сохраните условия в списке:

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

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

24
Хорошее решение, но в ToArray().NET 4 нет необходимости, так как есть перегрузка, которая принимает любые IEnumerable<string>.
— fero

101
Я в восторге от всех возможностей SQL-инъекций, которые это дает.
— asteri

12
@Jeff Если вы не жестко кодируете значения в предложении where, вы можете просто иметь второй список с SqlParameters. Вам просто нужно заполнить этот список одновременно со списком условий и в конце вызвать AddRange (parameters.ToArray ()) .
— Скотт Чемберлен

5
@ScottChamberlain Да, вы также можете просто экранировать входные строки, прежде чем помещать их в список. В основном я просто предупреждал о возможной атаке с шутливым юмором.
— asteri

4
@Jeff, он уязвим для SQL-инъекции только в том случае, если условия включают ввод данных пользователем (в исходном примере нет)
— Д. Стэнли,

85

Одно из решений - просто не писать запросы вручную, добавляя строки. Вы можете использовать ORM, например Entity Framework , а с LINQ to Entities использовать функции, предлагаемые языком и платформой:

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 Я не понимаю этого вопроса. Оба являются разными ORM.
— CodeCaster 07

Извините, я искал, чтобы сравнить производительность dapper с другими ORM, и я попал сюда через google, поэтому я подумал, что сгенерированный PrintResults(query)запрос затем будет использоваться в dapper как запрос !!
— vaheeds 07

@vaheeds в порядке, но непонимание ответа не гарантирует отрицательного голоса. Если бы это были вы, что случайно произошло одновременно с вашим комментарием.
— CodeCaster 07

ваше право, это было недоразумением. Я страдаю от плохой производительности linq to entity по сложным запросам. Я компенсировал ваш голос "против" другими вашими ответами "за";)
— vaheeds

Это не ответ на вопрос
— HGMamaci

17

Небольшой перебор в этом простом случае, но я использовал код, похожий на этот, в прошлом.

Создать функцию

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

Используйте это так

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

Таким образом, если условия не найдены, вы даже не утруждаетесь загрузкой оператора where в запросе и сохраняете сервер sql микросекунду обработки предложения where нежелательной почты, когда он анализирует оператор sql.


Не понимаю, как это делает его элегантнее. Конечно, не более ясно, что здесь происходит. Я вижу использование этой служебной функции, но она не более элегантна.
— usr

дал вам один голос за то, что вы
— разъяснили

15

Есть еще одно решение, которое тоже может быть не изящным, но работает и решает проблему:

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

Для:

  • пустой список условий, результат будет просто SELECT * FROM Table1 ,
  • единственное условие это будет SELECT * FROM Table1 WHERE cond1
  • каждое следующее условие будет генерировать дополнительные AND condN

6
Это оставляет висячий , WHEREесли нет предикатов; 1 = 1 существует специально, чтобы этого избежать.
— Gaius

Так что переключитесь на String query = "SELECT * FROM Table1";и string jointer = " WHERE ";?
— Brendan Long

Затем @BrendanLong WHEREявляются ANDы быть помещены между условиями?
— PenguinCoder

@PenguinCoder В комментарии сложно показать полный код. Я имел в виду заменить string joinerстроку на string joiner = " WHERE ";и оставить joiner = " AND ";строку в покое.
— Брендан Лонг,

@Gaius Я предположил, что кодиции непустые, но включение WHERE в объединение должно помочь. Спасибо за замечание!
— Dariusz

11

Просто сделайте что-нибудь вроде этого:

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

Это безопасно для SQL-инъекций и, ИМХО , довольно чисто. Remove()Просто удаляет последний AND;

Он работает как в том случае, если условия не были установлены, если одно из них было установлено, так и если было задано несколько условий.


1
Я не уверен (сам не использую C #), но я бы сказал, что так conditions != nullбудет всегда true, когда вы инициализируете его ""(кроме C # "" == null). Вероятно, это должен быть чек, если conditionsон не пустой… ;-)
— siegi 04

9

Просто добавьте две строчки сзади.

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

Например

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

станет

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

Пока

SELECT * FROM Table1 WHERE 1=1 

станет

SELECT * FROM Table1

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

Спасибо, что указали на недостаток этого решения:

"Это может привести к сбою запроса, если по какой-либо причине одно из условий содержит текст" 1 = 1 И "или" WHERE 1 = 1 ". Это может иметь место, если условие содержит подзапрос или пытается проверить, есть ли столбец содержит этот текст, например. Возможно, в вашем случае это не проблема, но вы должны помнить об этом ... "

Чтобы избавиться от этой проблемы, нам нужно различать «основные» WHERE 1 = 1 и те, что из подзапроса, что очень просто:

Просто сделайте «главное» ГДЕ особенным: я бы добавил знак «$»

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("WHERE$ 1=1 AND ", "WHERE ");
Query.Replace(" WHERE$ 1=1 ", "");

1
Это может привести к сбою запроса, если по какой-либо причине одно из условий содержит текст "1=1 AND "или " WHERE 1=1 ". Это может быть в том случае, если условие содержит подзапрос или, например, пытается проверить, содержит ли какой-либо столбец этот текст. Может быть, в вашем случае это не проблема, но вы должны помнить об этом…
— siegi

8

Использовать это:

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

Этот ответ будет работать, и в нем нет ничего плохого, но я не думаю, что он более понятен и прост, чем исходный вопрос. На QuerySubмой взгляд, поиск по строкам не лучше и не хуже, чем использование where 1=1хака. Но это продуманный вклад.
— catfood

3
Это была ошибка. Исправил. Мой запрос был бы провален, если бы не было ни одного из условий :-P Тем не менее, я должен сказать, что Ahmed или CodeCaster для меня являются лучшими решениями. Я только представил вам альтернативу, ребята!
— Anshuman

В общем, это все равно неправильно. Предположим, это было ... FROM SOMETABLE WHERE ; затемTrimEnd фактически уменьшит это до ... FROM SOMETABL. Если бы это было на самом деле StringBuilder(что должно быть, если у вас столько манипуляций со строками или больше), вы можете просто Query.Length -= "WHERE ".Length;.
— Марк Херд

Марк, работает. Я пробовал это во многих проектах. Попробуйте, и вы убедитесь, что это так!
— Anshuman

8
чертовски уродливые :) плюс он может создать до 7 струн, если я правильно посчитал
— Петр Перак

5

Если это SQL Server , вы можете сделать этот код намного чище.

Это также предполагает известное количество параметров, что может быть плохим предположением, когда я думаю о возможностях.

В C # вы должны использовать:

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

А затем на стороне 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

Скрытие столбцов внутри выражения может предотвратить использование индексов, и по этой причине здесь не рекомендуется использовать этот метод .
— bbsimonbb

это интересная находка. Спасибо за эту информацию. будет обновлять
— mckeejm 03

5

Почему бы не использовать существующий построитель запросов? Что-то вроде Sql Kata .

Он поддерживает сложные условия, объединения и подзапросы.

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

он работает с Sql Server, MySql и PostgreSql.


4

Самое быстрое буквальное решение вопроса, которое я могу придумать, это следующее:

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

Конечно, это не выглядит элегантным, и я бы отослал вас к рекомендации CodeCaster по использованию ORM. Но если вы подумаете о том, что это здесь делает, вы действительно не беспокоитесь о том, чтобы «тратить» 4 символа памяти, и компьютер действительно быстро переместит указатель на 4 позиции.

Если у вас есть время научиться использовать ORM, это действительно может окупиться. Но что касается этого, если вы пытаетесь предотвратить попадание этого дополнительного условия в базу данных SQL, это сделает это за вас.


3

В зависимости от условия в запросе можно использовать логическую логику. Что-то вроде этого :

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

3

Мне нравится свободный интерфейс конструктора строк, поэтому я сделал несколько 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

ИМХО, я считаю, что ваш подход неверен:

Запрос базы данных путем объединения строк НИКОГДА не является хорошей идеей (риск внедрения SQL и код может легко сломаться, если вы внесете какие-то изменения в другом месте).

Вы можете использовать ORM (я использую NHibernate ) или, по крайней мере, использоватьSqlCommand.Parameters

Если вы абсолютно хотите использовать конкатенацию строк, я бы использовал StringBuilder(это правильный объект для конкатенации строк):

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;

Как последняя мысль, Where 1=1это действительно некрасиво, но SQL Server все равно оптимизирует его.


SELECT * FROM Table1 WHERE AND Col1=0не кажется правильным, и в этом весь смысл WHERE 1=1.
— Mormegil

2

Dapper SqlBuilder - довольно хороший вариант. Он даже используется в производстве на StackOverflow.

Прочтите об этом в блоге Сэма .

Насколько мне известно, он не является частью какого-либо пакета Nuget, поэтому вам нужно скопировать и вставить его код в свой проект или загрузить исходный код Dapper и собрать проект SqlBuilder. В любом случае вам также потребуется ссылаться на Dapper для DynamicParametersкласса.


1
Я не думаю, что SqlBuilder от Dapper включен в этот пакет.
— Ронни Оверби

1

Я вижу, что это все время используется в Oracle при создании динамического SQL в хранимых процедурах. . Я использую его в запросах при изучении проблем с данными, чтобы ускорить переключение между различными фильтрами данных ... Просто закомментируйте условие или легко добавьте его обратно.

Я считаю, что это довольно распространено и достаточно просто для понимания тем, кто просматривает ваш код.


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

Реализация методами расширения.

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

ПРОТИВ ЗЕРНА!
— Ронни Оверби

Извините? Что ты имеешь в виду?
— Максим Жуков

0

Используя stringфункцию, вы также можете сделать это следующим образом:

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  

Мне лично кажется, что в конце условный элемент (элементы) легко удалить, поскольку его положение легко предсказать.


0

Я подумал о решении, которое, возможно, более читабельно:

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

Я просто не уверен, будет ли интерпретатор SQL также оптимизировать Col1 = Col1условие (печатается, когда condition1ложно).


0

Вот более элегантный способ:

    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

Как уже было сказано, создание SQL путем конкатенации никогда не было хорошей идеей . Не только из-за SQL-инъекции. В основном потому, что он уродлив, сложен в обслуживании и совершенно не нужен . Вы должны запустить свою программу с трассировкой или отладкой, чтобы увидеть, какой SQL она генерирует. Если вы используете QueryFirst (отказ от ответственности: я написал), неприятный соблазн исчезнет , и вы сможете сразу приступить к выполнению этого в SQL.

На этой странице подробно описаны параметры TSQL для динамического добавления предикатов поиска. Следующая опция удобна в ситуациях, когда вы хотите оставить выбор комбинаций предикатов поиска своему пользователю.

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 дает вам C # null в db NULL, поэтому вы просто вызываете метод Execute () с нулевыми значениями, когда это необходимо, и все это просто работает. <Мнение> Почему разработчики C # так неохотно делают что-то на SQL, даже когда это проще? Уму непостижимо. </opinion>


0

Многие говорят, что для более длительных этапов фильтрации лучше всего подходит StringBuilder.

в вашем случае я бы пошел с:

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

Лаконично, элегантно и мило, как показано на изображении ниже.

введите описание изображения здесь

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.