Чи є кращий спосіб динамічно побудувати пункт 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 не здається елегантним. Якщо я не використовував його, я повинен був би пам’ятати та перевіряти кожен раз, чи ключове слово «де» було додане чи ні до запиту.

Чи є приємніше рішення?


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, які це забезпечує.
астері

12
@Jeff Якщо ви не жорстко кодуєте значення в пункті, де ви можете просто мати 2-й список з SqlParameters. Вам просто потрібно заповнити цей список одночасно зі списком умов та викликати AddRange (parametri.ToArray ()) наприкінці.
Скотт Чемберлен

5
@ScottChamberlain Так, ви також можете просто уникнути вхідних рядків, перш ніж помістити їх у список. Я здебільшого просто застеріг від можливого нападу, використовуючи вибагливий гумор.
астері

4
@Jeff він вразливий до ін'єкції SQL, лише якщо умови включають введення користувача (оригінальний приклад не робить)
D Стенлі,

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

Вибачте, я шукав, щоб порівняти продуктивність dapper з іншими ORM, і я потрапив сюди google, тому я подумав, що згенерований PrintResults(query)запит потім використовуватиметься в dapper як запит !!
vaheeds

@vaheeds гаразд, але нерозуміння відповіді не є підставою для протистояння. Якби це ви, це випадково сталося одночасно з вашим коментарем.
CodeCaster

ваше право, це було непорозуміння. Я потерпаю від посилань на зв’язки з особами, які погано працюють за складними запитами. Я компенсував голосування, що був
уніфікований,

Це не відповідь на питання
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);

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


Я не бачу, як це робить його більш елегантним. Звичайно, не зрозуміло, що тут відбувається. Я бачу використання цієї функції утиліти, але вона не є більш елегантною.
usr

дав вам один голос за просвітлення нас про важливість мікросекунди
user1451111

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 спеціально існує, щоб цього уникнути.
Гай

Так перейти до String query = "SELECT * FROM Table1";і string jointer = " WHERE ";?
Брендан Лонг

@BrendanLong Тоді WHEREрозміщення ANDs між умовами?
PenguinCoder

@PenguinCoder Важко показати повний код у коментарі. Я мав на увазі замінити string joinerрядок на string joiner = " WHERE ";і залишити joiner = " AND ";лінію в спокої.
Брендан Лонг

@Gaius Я припустив, що котирування є не порожніми, але, якщо поставити WHERE в столяр, слід зробити трюк. Дякуємо за зауваження!
Даріуш

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 та IMHO , вона досить чиста. Remove()Просто видаляє останнійAND ;

Він працює як, якщо не було встановлено жодних умов, якщо вони були встановлені, або якщо було встановлено кілька.


1
Я не впевнений (не використовуйте сам C #), але я б сказав, що conditions != nullце завжди true, як ви ініціалізуєте це ""(якщо тільки в C # "" == null). Ймовірно, має бути чек, якщо conditionsвін не порожній… ;-)
siegi

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 І "або" ДЕ 1 = 1 ". Це може бути так, якщо умова містить підзапит або намагається перевірити, чи є якісь наприклад, цей стовпець містить цей текст. Можливо, це не проблема у вашому випадку, але вам слід пам’ятати про це ... "

Для того, щоб позбутися цього питання, нам потрібно виділити «головне», де БЕЗ 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хак. Але це вдумливий внесок.
котяча їжа

3
Там була помилка. Виправили це. Мій запит був би бомбардований, якби жодна з умов не була присутня :-П. Але я мушу сказати, що Ахмед чи Кодекстер для мене - найкращі рішення. Я лише представив альтернативу для вас, хлопці!
Аншуман

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

Познач, це працює. Я пробував це в багатьох проектах. Спробуйте, і ви зрозумієте, що це так!
Аншуман

8
некрасиво, як пекло :) плюс це може створити до 7 рядків, якщо я правильно порахував
Пьотр Перак

5

Чому б не використовувати існуючий Builder Query? Щось на зразок 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, 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 db, це зробить це за вас.


4

Якщо це 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

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

Мені подобається вільний інтерфейс stringbuilder, тому я зробив кілька 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 () з нулями, коли це доречно, і все це просто працює. <opinion> Чому C # devs настільки неохоче займається вмістом у 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.