Я создаю SQL запрос на С#. Он будет отличаться в зависимости от некоторых условий, сохраненных в качестве переменных в коде.
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" уже было добавлено или не было в запросе.
Есть ли более приятное решение?
Сохраните условия в списке:
List<string> conditions = new List<string>();
if (condition1) conditions.Add("Col1=0");
//...
if (conditions.Any())
Query += " WHERE " + string.Join(" AND ", conditions.ToArray());
ToArray()
не требуется в .NET 4, поскольку существует перегрузка, которая принимает любой IEnumerable<string>
.
Одним из решений является просто не писать запросы вручную, добавляя строки. Вы можете использовать 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);
}
Небольшой перегиб в этом простом случае, но я использовал код, похожий на этот в прошлом.
Создать функцию
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-сервер на микросекунду обработки инструкции junk where, когда она анализирует оператор sql.
Существует еще одно решение, которое также может быть не изящным, но работает и решает проблему:
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
WHERE
если нет предикатов; 1 = 1 специально существует, чтобы избежать этого.
String query = "SELECT * FROM Table1";
и string jointer = " WHERE ";
?
Просто сделайте что-нибудь вроде этого:
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
;
Он работает и в том случае, если не было установлено никаких условий, если они были установлены или установлено несколько.
conditions != null
всегда true
, поскольку вы инициализируете его с помощью ""
(если только в C # "" == null
). Вероятно, это должна быть проверка, если conditions
не пусты… ;-)
Используйте это:
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
поиском QuerySub
, на мой взгляд, не лучше и не хуже, чем использование взлома where 1=1
. Но это вдумчивый вклад.
Самое быстрое буквальное решение того, о чем вы просите, я могу подумать:
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, это сделает это за вас.
Просто добавьте две строки назад.
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 AND "
или " WHERE 1=1 "
. Это может быть в том случае, если условие содержит подзапрос или пытается проверить, например, содержит ли какой-либо столбец этот текст. Может быть, это не проблема в вашем случае, но вы должны помнить об этом ...
В зависимости от условия в запросе может быть возможно использовать логическую логику. Что-то вроде этого:
string Query="SELECT * FROM Table1 " +
"WHERE (condition1 = @test1 AND Col1=0) "+
"AND (condition2 = @test2 AND Col2=1) "+
"AND (condition3 = @test3 AND Col3=2) ";
Если это SQL Server, вы можете сделать этот код намного чище.
Это также предполагает известное количество параметров, которые могут быть плохим предположением, когда я думаю о возможностях.
В С# вы должны использовать:
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
Мне нравится свободный интерфейс стробоструйного устройства, поэтому я сделал некоторые 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;
}
Почему бы не использовать существующий Query Builder? Что-то вроде 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.
Dapper SqlBuilder - довольно хороший вариант. Он даже используется в производстве на StackOverflow.
Прочитайте запись в блоге Sam.
Насколько я знаю, он не является частью какого-либо пакета Nuget, поэтому вам нужно скопировать его код в свой проект или загрузить источник Dapper и построить проект SqlBuilder. В любом случае вам также потребуется ссылаться на Dapper для класса DynamicParameters
.
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);
}
ИМХО, я думаю, что ваш подход неправильный:
Запросить базу данных путем конкатенации строки НИКОГДА не рекомендуется (риск 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
.
Я вижу, что это использовалось все время в Oracle при построении динамического SQL в хранимых процедурах. Я использую его в запросах при изучении проблем с данными, а просто для того, чтобы быстрее переключаться между различными фильтрами данных... Просто закомментируйте условие или добавьте его обратно легко.
Я нахожу это довольно распространенным и достаточно простым, чтобы понять кого-то, просматривающего ваш код.
Для более длительных шагов фильтрации 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 "
Как было сказано, создание SQL путем конкатенации никогда не является хорошей идеей. Не только из-за SQL-инъекции. В основном потому, что это просто уродливый, трудно поддерживаемый и совершенно ненужный. Вы должны запустить свою программу с помощью трассировки или отладки, чтобы увидеть, какой SQL он генерирует. Если вы используете QueryFirst (отказ от ответственности: что я написал), неудовлетворительное соблазнение удаляется, и вы можете получить прямо в ta doin it в 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 дает вам С# null для db NULL, поэтому вы просто вызываете метод Execute() с нулями, когда это необходимо, и все это просто работает. <opinion> Почему разработчики С# так неохотно делают что-то в SQL, даже когда это проще. Mind boggles. </opinion>
Вот более элегантный способ:
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;
}
Я подумал о решении, которое, может быть, несколько читаемо:
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
является ложным).
Используя функцию 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
Мне лично легко удалить условный элемент в конце, так как его положение легко предсказать.
42 = 42
;-)