Je construis quelques SQL requête en C #. Il diffère en fonction de certaines conditions stockées en tant que variables dans le code.
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 ";
Cela fonctionne, mais tester 1 = 1 ne semble pas élégant. Si je ne l'utilisais pas, je devrais me rappeler et vérifier chaque fois si le mot clé "où" était déjà ajouté ou non à la requête.
Y a-t-il une solution plus intéressante?
Enregistrez les conditions dans une liste:
List<string> conditions = new List<string>();
if (condition1) conditions.Add("Col1=0");
//...
if (conditions.Any())
Query += " WHERE " + string.Join(" AND ", conditions.ToArray());
Une solution consiste simplement à ne pas écrire manuellement les requêtes en ajoutant des chaînes. Vous pouvez utiliser un ORM, tel que Entity Framework , et utiliser LINQ to Entities avec les fonctionnalités proposées par le langage et la structure:
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);
}
Un peu exagéré dans ce cas simple, mais j'ai utilisé un code similaire à celui-ci dans le passé.
Créer une fonction
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);
}
Utilisez-le comme ça
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);
De cette façon, si aucune condition n'est trouvée, vous ne vous souciez même pas de charger une instruction where dans la requête et de sauvegarder le serveur SQL une micro-seconde de traitement de la clause Junk Where lorsqu'il analyse l'instruction SQL.
Il existe une autre solution, qui peut aussi ne pas être élégante, mais qui fonctionne et résout le problème:
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 "
}
Pour:
SELECT * FROM Table1
,SELECT * FROM Table1 WHERE cond1
AND condN
supplémentairesFaites juste quelque chose comme ça:
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);
}
C'est injection SQL sûr et IMHO , c'est assez propre. La Remove()
supprime simplement le dernier AND
;
Cela fonctionne à la fois si aucune condition n'a été définie, si une condition a été définie ou si plusieurs conditions ont été définies.
Utilisez ceci:
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());
Juste ajouter deux lignes à l'arrière.
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 ", "");
Par exemple.
SELECT * FROM Table1 WHERE 1=1 AND Col1=0 AND Col2=1 AND Col3=2
deviendra à
SELECT * FROM Table1 WHERE Col1=0 AND Col2=1 AND Col3=2
Tandis que
SELECT * FROM Table1 WHERE 1=1
deviendra à
SELECT * FROM Table1
=====================================
Merci de signaler une faille de cette solution:
"Cela pourrait interrompre la requête si, pour une raison quelconque, l'une des conditions contenait le texte" 1 = 1 AND "ou" WHERE 1 = 1 ". Cela pourrait être le cas si la condition contient une sous-requête ou tente de vérifier si une colonne contient ce texte, par exemple. Ce n'est peut-être pas un problème dans votre cas, mais vous devez le garder à l'esprit… "
Afin de résoudre ce problème, nous devons distinguer le "principal" WHERE 1 = 1 et ceux de la sous-requête, ce qui est simple:
Faites simplement le "principal" OÙ spécial: j'ajouterais un signe "$"
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 ";
Puis ajoutez encore deux lignes:
Query.Replace("WHERE$ 1=1 AND ", "WHERE ");
Query.Replace(" WHERE$ 1=1 ", "");
Pourquoi ne pas utiliser un générateur de requêtes existant? Quelque chose comme Sql Kata .
Il supporte complexe où conditions, jointures et sous-requêtes.
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"))
cela fonctionne avec Sql Server, MySql et PostgreSql.
Si c'est SQL Server , vous pouvez rendre ce code beaucoup plus propre.
Cela suppose également un nombre connu de paramètres, ce qui peut être une mauvaise hypothèse lorsque je songe aux possibilités.
En C #, vous utiliseriez:
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();
}
Et puis du côté 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
La solution littérale la plus rapide à ce que vous me demandez est la suivante:
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);
Cela ne semble pas élégant, bien sûr, et je vous renvoie à la recommandation de CodeCaster d'utiliser un ORM. Mais si vous pensez à ce que cela fait ici, vous ne craignez vraiment pas de "perdre" 4 caractères de mémoire, et il est très rapide pour un ordinateur de déplacer un pointeur de 4 positions.
Si vous avez le temps d'apprendre à utiliser un ORM, cela pourrait vous rapporter beaucoup. Mais en ce qui concerne cela, si vous essayez d'empêcher que cette condition supplémentaire ne frappe la base de données SQL, cela le fera pour vous.
J'aime l’interface fluide de stringbuilder, j’ai donc fait quelques 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;
}
Selon la condition, il peut être possible d'utiliser une logique booléenne dans la requête. Quelque chose comme ça :
string Query="SELECT * FROM Table1 " +
"WHERE (condition1 = @test1 AND Col1=0) "+
"AND (condition2 = @test2 AND Col2=1) "+
"AND (condition3 = @test3 AND Col3=2) ";
IMHO, je pense que votre approche est fausse:
Interroger la base de données en concaténant une chaîne n'est JAMAIS une bonne idée (risque de injection SQL et le code peut facilement être cassé si vous faire des changements ailleurs).
Vous pouvez utiliser un ORM (j'utilise NHibernate ) ou au moins utiliser SqlCommand.Parameters
Si vous voulez absolument utiliser la concaténation de chaînes, j'utiliserais un StringBuilder
(c'est le bon objet pour la concaténation de chaînes):
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;
Comme dernière pensée, Where 1=1
est vraiment moche mais SQL Server l’optimisera quand même.
Le Dapper SqlBuilder est une très bonne option. Il est même utilisé en production sur StackOverflow.
Lire l'entrée de blog de Sam à ce sujet .
Pour autant que je sache, il ne fait partie d'aucun paquet Nuget. Vous devez donc coller son code dans votre projet, télécharger le fichier source Dapper et construire le projet SqlBuilder. Dans les deux cas, vous devrez également faire référence à Dapper pour la classe DynamicParameters
.
Je vois cela utilisé tout le temps dans Oracle lors de la construction de SQL dynamique dans procédures stockées . Je l'utilise dans les requêtes tout en explorant les problèmes de données, simplement pour faciliter la commutation entre différents filtres de données ... Il suffit de commenter une condition ou de l'ajouter facilement.
Je trouve que c'est assez commun et assez facile à comprendre pour quelqu'un qui passe en revue votre code.
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 ");
}
}
Réalisation avec des méthodes d'extension.
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);
}
Voici une manière plus élégante:
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;
}
J'ai pensé à une solution qui, peut-être, est un peu plus lisible:
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"));
Je ne suis pas sûr que l'interpréteur SQL optimise également le Col1 = Col1
condition (imprimée lorsque condition1
c'est faux).
Comme il a été dit, créer du SQL par concaténation n’est jamais une bonne idée . Pas seulement à cause de l'injection SQL. Principalement parce que c'est laid, difficile à maintenir et totalement inutile . Vous devez exécuter votre programme avec trace ou déboguer pour voir quel code SQL il génère. Si vous utilisez QueryFirst (disclaimer: ce que j’ai écrit), la tentation malheureuse est supprimée et vous pouvez le faire directement en SQL.
Cette page couvre de manière exhaustive les options TSQL pour l’ajout dynamique de prédicats de recherche. L'option suivante est pratique pour les situations dans lesquelles vous souhaitez laisser le choix de combinaisons de prédicats de recherche à votre utilisateur.
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 vous donne C # null à db NULL, il vous suffit donc d'appeler la méthode Execute () avec null lorsque cela convient, et tout cela fonctionne. <opinion> Pourquoi les développeurs C # sont-ils si réticents à faire des choses en SQL, même quand c'est plus simple? Mind Boggles. </ Opinion>
Pour de plus longues étapes de filtrage, StringBuilder est la meilleure approche, comme beaucoup le disent.
sur ton cas j'irais avec:
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 "
En utilisant la fonction string
, vous pouvez aussi le faire de cette façon:
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
Personnellement, j'estime qu'il est facile de supprimer le ou les éléments conditionnels à la fin, car sa position est facile à prévoir.