Existe-t-il un meilleur moyen de créer dynamiquement une clause SQL WHERE qu'en utilisant 1 = 1 au début?

110

Je construis une requête SQL en C #. Il différera 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 souvenir et vérifier à chaque fois si le mot-clé «où» était déjà ajouté ou non à la requête.

Y a-t-il une meilleure solution?

RRM
la source
118
Pour être honnête - je le ferais aussi comme ça, mais j'utiliserais 42 = 42;-)
fero
5
En fait, j'écris toujours mes requêtes comme ça.
Facilite le
4
@catfood Le premier projet sur lequel j'ai participé en tant que stagiaire consistait à écrire des outils pour aider à analyser les requêtes de performances par rapport à nos serveurs Sybase. Une découverte amusante a été les centaines de milliers de Select 42requêtes que nous recevions. (pas amusant d'essayer de retrouver la source)
Mr.Mindor
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- C'est pourquoi vous utilisez 1 = 1. Le moteur de base de données l'optimise de toute façon, donc bien que cela puisse paraître moche, c'est de loin le moyen le plus simple de résoudre le problème.
Robert Harvey
4
Bien que les réponses données soient très belles, je pense que votre code d'origine est le plus facile à lire.
Uooo

Réponses:

157

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());
Ahmed KRAIEM
la source
24
Bonne solution, mais ce ToArray()n'est pas nécessaire avec .NET 4 car il y a une surcharge qui en accepte IEnumerable<string>.
fero
101
Je suis ravi de toutes les opportunités d'injection SQL que cela offre.
asteri
12
@Jeff Si vous ne codez pas en dur les valeurs de la clause where, vous pouvez également avoir une deuxième liste avec SqlParameters. Il vous suffit de remplir cette liste en même temps que la liste de conditions et d'appeler AddRange (parameters.ToArray ()) à la fin.
Scott Chamberlain
5
@ScottChamberlain Ouais, vous pouvez aussi simplement échapper les chaînes d'entrée avant de les mettre dans la liste. Je mettais surtout en garde contre une éventuelle attaque utilisant un humour facétieux.
asteri
4
@Jeff il n'est vulnérable à l'injection SQL que si les conditions incluent une entrée utilisateur (l'exemple d'origine ne le fait pas)
D Stanley
85

Une solution consiste simplement à ne pas écrire de requêtes manuellement en ajoutant des chaînes. Vous pouvez utiliser un ORM, comme Entity Framework , et avec LINQ to Entities, utiliser les fonctionnalités que le langage et le framework vous offrent:

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
la source
@vaheeds Je ne comprends pas cette question. Les deux sont des ORM différents.
CodeCaster
Désolé, je cherchais à comparer les performances de dapper à d'autres ORM, et je suis arrivé ici par google, donc j'ai pensé que la PrintResults(query)requête générée sera ensuite utilisée dans dapper comme requête !!
vaheeds
@vaheeds bien, mais ne pas comprendre une réponse ne justifie pas un vote défavorable. Si c'était vous, ce qui s'est produit par hasard en même temps que votre commentaire.
CodeCaster
votre droit, c'était un malentendu. Je souffre de linq aux entités de mauvaises performances sur des requêtes compliquées. J'ai compensé le vote à la baisse par vos autres réponses à la hausse;)
vaheeds
Ce n'est pas une réponse à la question
HGMamaci
17

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 enregistrez le serveur sql une micro-seconde de traitement de la clause where indésirable lorsqu'il analyse l'instruction sql.

Alan Barber
la source
Je ne vois pas en quoi cela le rend plus élégant. Ce qui se passe ici n'est certainement pas plus clair. Je peux voir l'utilisation de cette fonction utilitaire, mais ce n'est pas plus élégant.
usr
vous a donné un vote pour nous éclairer sur l'importance d'une micro-seconde
user1451111
15

Il existe une autre solution, qui peut également 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:

  • liste de conditions vide, le résultat sera simplement SELECT * FROM Table1,
  • une seule condition ce sera SELECT * FROM Table1 WHERE cond1
  • chaque condition suivante générera des AND condN
Dariusz
la source
6
Cela laisse un pendant WHEREs'il n'y a pas de prédicats; le 1 = 1 existe spécifiquement pour éviter cela.
Gaius
Alors passez à String query = "SELECT * FROM Table1";et string jointer = " WHERE ";?
Brendan Long
@BrendanLong Alors WHEREles ANDs doivent-ils être placés entre les conditions?
PenguinCoder
@PenguinCoder Il est difficile d'afficher le code complet dans un commentaire. Je voulais dire remplacer la string joinerligne par string joiner = " WHERE ";et laisser la joiner = " AND ";ligne seule.
Brendan Long du
@Gaius J'ai supposé que les coditions n'étaient pas vides, mais mettre WHERE dans Joiner devrait faire l'affaire. Merci pour la remarque!
Dariusz
11

Faites 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 l' injection SQL sûre et à mon humble avis , c'est assez propre. Le Remove()supprime simplement le dernier AND;

Cela fonctionne à la fois si aucune condition n'a été définie, si une seule a été définie ou si plusieurs ont été définies.

Jgauffin
la source
1
Je ne suis pas sûr (n'utilisez pas C # moi-même) mais je dirais que conditions != nullc'est toujours le cas true, car vous l'initialisez avec ""(sauf en C # "" == null). Cela devrait probablement être un chèque, si ce conditionsn'est pas vide… ;-)
siegi
9

Ajoutez simplement 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 d'avoir signalé un défaut de cette solution:

"Cela peut interrompre la requête si, pour une raison quelconque, l'une des conditions contient le texte" 1 = 1 AND "ou" WHERE 1 = 1 ". Cela peut être le cas si la condition contient une sous-requête ou tente de vérifier si la colonne contient ce texte, par exemple. Ce n'est peut-être pas un problème dans votre cas, mais vous devriez le garder à l'esprit… "

Afin de se débarrasser de ce problème, nous devons distinguer le "principal" WHERE 1 = 1 et ceux de la sous-requête, ce qui est facile:

Rendez simplement le WHERE "principal" 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 ";

Ensuite, ajoutez toujours deux lignes:

Query.Replace("WHERE$ 1=1 AND ", "WHERE ");
Query.Replace(" WHERE$ 1=1 ", "");
milesma
la source
1
Cela pourrait interrompre la requête si, pour une raison quelconque, l'une des conditions contient le texte "1=1 AND "ou " WHERE 1=1 ". Cela peut être le cas si la condition contient une sous-requête ou tente de vérifier si une colonne contient ce texte, par exemple. Peut-être que ce n'est pas un problème dans votre cas, mais vous devriez le garder à l'esprit…
siegi
8

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());
Anshuman
la source
Cette réponse fonctionnera, et il n'y a rien de vraiment mal à cela, mais je ne pense pas que ce soit plus clair et simple que la question initiale. La recherche de chaînes QuerySubn'est à mon avis ni meilleure ni pire que l'utilisation du where 1=1hack. Mais c'est une contribution réfléchie.
catfood
3
Il y avait une erreur. Corrigé. Ma requête aurait bombardé si aucune des conditions n'était présente :-P Je dois quand même dire qu'Ahmed ou CodeCaster sont pour moi les meilleures solutions. Je n'ai présenté qu'une alternative pour vous les gars!
Anshuman
C'est toujours faux, en général. Supposons que c'était le cas ... FROM SOMETABLE WHERE ; alors le TrimEndréduirait réellement ceci à ... FROM SOMETABL. Si c'était en fait un StringBuilder(ce qu'il devrait être si vous avez à propos de cette manipulation de chaînes ou plus), vous pouvez simplement Query.Length -= "WHERE ".Length;.
Mark Hurd
Mark, ça marche. J'ai essayé cela dans de nombreux projets. Essayez-le et vous constaterez que c'est le cas!
Anshuman
8
moche comme l'enfer :) en plus il peut créer jusqu'à 7 cordes si j'ai compté correctement
Piotr Perak
5

Pourquoi ne pas utiliser un générateur de requêtes existant? Quelque chose comme Sql Kata .

Il prend en charge les conditions complexes, les jointures et les 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"))

il fonctionne avec Sql Server, MySql et PostgreSql.

amd
la source
4

La solution littérale la plus rapide à ce que vous demandez et à laquelle je puisse penser 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, auquel 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 «gaspiller» 4 caractères de mémoire, et il est très rapide pour un ordinateur de déplacer un pointeur de 4 places.

Si vous avez le temps d'apprendre à utiliser un ORM, cela pourrait vraiment être rentable pour vous. Mais à ce propos, si vous essayez d'empêcher cette condition supplémentaire d'atteindre la base de données SQL, cela le fera pour vous.

trevorgrayson
la source
4

S'il s'agit de 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 pense 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 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
mckeejm
la source
Cacher vos colonnes dans une expression peut empêcher l'utilisation d'index, et cette technique est déconseillée pour cette raison ici .
bbsimonbb
c'est une découverte intéressante. Merci pour cette information. mettra à jour
mckeejm
3

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) ";
Rémi
la source
3

J'aime l'interface fluide de stringbuilder, j'ai donc créé des 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;
}
Jürgen Steinblock
la source
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 d' injection SQL et le code peut facilement être cassé si vous apportez des modifications ailleurs).

Vous pouvez utiliser un ORM (j'utilise NHibernate ) ou au moins utiliserSqlCommand.Parameters

Si vous voulez absolument utiliser la concaténation de chaînes, j'utiliserais a 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;

En dernière analyse, Where 1=1c'est vraiment moche mais SQL Server l'optimisera quand même.

giammin
la source
SELECT * FROM Table1 WHERE AND Col1=0ne semble pas correct, c'est tout l'intérêt de WHERE 1=1.
Mormegil
2

Le Dapper SqlBuilder est une très bonne option. Il est même utilisé en production sur StackOverflow.

Lisez l'article de blog de Sam à ce sujet .

Pour autant que je sache, il ne fait partie d'aucun package Nuget, vous devrez donc copier-coller son code dans votre projet ou télécharger la source Dapper et créer le projet SqlBuilder. Dans tous les cas, vous devrez également référencer Dapper pour la DynamicParametersclasse.

Ronnie Overby
la source
1
Je ne pense pas que SqlBuilder de Dapper soit inclus dans ce package.
Ronnie Overby
1

Je vois cela utilisé tout le temps dans Oracle lors de la création de SQL dynamique dans des procédures stockées . Je l'utilise dans les requêtes tout en explorant les problèmes de données, simplement pour passer plus rapidement entre différents filtres de données ... Il suffit de commenter une condition ou de la rajouter facilement.

Je trouve que c'est assez courant et assez facile à comprendre pour quelqu'un qui examine votre code.

Don Boling
la source
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 ");
    }
}

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);
    }
Maxim Joukov
la source
CONTRE LE GRAIN!
Ronnie Overpar
Excusez-moi? Qu'est-ce que ça veut dire?
Maxim Zhukov
0

En utilisant la stringfonction, vous pouvez également 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, je me sens facile de supprimer le ou les éléments conditionnels à la fin, car sa position est facile à prévoir.

NeverHopeless
la source
0

J'ai pensé à une solution qui, eh bien, est peut-être 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 tout simplement pas sûr que l'interpréteur SQL optimisera également la Col1 = Col1condition (imprimée lorsque condition1est false).

CodeCaster
la source
0

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;
    }
WonderWorker
la source
0

Comme cela a été dit, créer du SQL par concaténation n'est jamais une bonne idée . Pas seulement à cause de l'injection SQL. Surtout parce que c'est juste moche, difficile à entretenir et totalement inutile . Vous devez exécuter votre programme avec trace ou débogage pour voir quel SQL il génère. Si vous utilisez QueryFirst (clause de non-responsabilité: ce que j'ai écrit), la tentation malheureuse est supprimée et vous pouvez directement le faire en SQL.

Cette page a une couverture complète des options TSQL pour ajouter dynamiquement des prédicats de recherche. L'option suivante est pratique dans les situations où vous souhaitez laisser le choix des 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, vous appelez donc simplement la méthode Execute () avec des valeurs nulles le cas échéant, et tout fonctionne simplement. <opinion> Pourquoi les développeurs C # sont-ils si réticents à faire des choses en SQL, même si c'est plus simple? Mind boggles. </opinion>

bbsimonbb
la source
0

Pour les étapes de filtrage plus longues, StringBuilder est la meilleure approche, comme beaucoup le disent.

sur votre 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 "
HGMamaci
la source
0

Concis, élégant et doux, comme le montre l'image ci-dessous.

entrez la description de l'image ici

user1451111
la source