Есть ли лучший способ динамически построить предложение 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);
}
CodeCaster
источник
@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;

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

jgauffin
источник
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 ", "");
Milesma
источник
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
mckeejm
источник
Скрытие столбцов внутри выражения может предотвратить использование индексов, и по этой причине здесь не рекомендуется использовать этот метод .
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, это сделает это за вас.

Trevorgrayson
источник
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  

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

NeverHopeless
источник
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ложно).

CodeCaster
источник
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;
    }
WonderWorker
источник
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>

bbsimonbb
источник
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 "
HGMamaci
источник
0

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

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

user1451111
источник