Bonne façon d'utiliser StringBuilder dans SQL

88

Je viens de trouver une construction de requête SQL comme celle-ci dans mon projet:

return (new StringBuilder("select id1, " + " id2 " + " from " + " table")).toString();

Cela StringBuilderatteint-il son objectif, à savoir réduire l'utilisation de la mémoire?

J'en doute, car dans le constructeur, le «+» (opérateur de concat) est utilisé. Cela prendra-t-il la même quantité de mémoire que l'utilisation de String comme le code ci-dessous? s J'ai compris, cela diffère lors de l'utilisation StringBuilder.append().

return "select id1, " + " id2 " + " from " + " table";

Les deux instructions sont-elles égales dans l'utilisation de la mémoire ou non? Précisez s'il vous plaît.

Merci d'avance!

Éditer:

BTW, ce n'est pas mon code . Je l'ai trouvé dans un ancien projet. De plus, la requête n'est pas aussi petite que celle de mon exemple. :)

Vaandu
la source
1
SÉCURITÉ SQL: utilisez toujours PreparedStatementou quelque chose de similaire: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe Roussy
Outre l'utilisation de la mémoire, pourquoi ne pas utiliser une bibliothèque SQL Builder à la place: stackoverflow.com/q/370818/521799
Lukas Eder

Réponses:

182

Le but de l'utilisation de StringBuilder, c'est-à-dire réduire la mémoire. Est-ce atteint?

Non pas du tout. Ce code n'utilise pas StringBuildercorrectement. (Je pense que vous l'avez mal cité, cependant; il n'y a sûrement pas de citations autour id2et table?)

Notez que le but (généralement) est de réduire le taux de désabonnement de la mémoire plutôt que la mémoire totale utilisée, afin de faciliter un peu la vie du ramasse-miettes.

Cela prendra-t-il une mémoire égale à l'utilisation de String comme ci-dessous?

Non, cela causera plus de désabonnement de mémoire que le simple concat que vous avez cité. (Jusqu'à / à moins que l'optimiseur JVM ne voit que l'explicite StringBuilderdans le code n'est pas nécessaire et l'optimise, s'il le peut.)

Si l'auteur de ce code veut utiliser StringBuilder(il y a des arguments pour, mais aussi contre; voir la note à la fin de cette réponse), mieux vaut le faire correctement (ici je suppose qu'il n'y a pas réellement de guillemets autour id2et table):

StringBuilder sb = new StringBuilder(some_appropriate_size);
sb.append("select id1, ");
sb.append(id2);
sb.append(" from ");
sb.append(table);
return sb.toString();

Notez que j'ai répertorié some_appropriate_sizedans le StringBuilderconstructeur, de sorte qu'il commence avec une capacité suffisante pour le contenu complet que nous allons ajouter. La taille par défaut utilisée si vous n'en spécifiez pas un est de 16 caractères , ce qui est généralement trop petit et StringBuilderoblige à effectuer des réallocations pour devenir plus grand (IIRC, dans le JDK Sun / Oracle, il se double [ou plus, si il sait qu'il a besoin de plus pour satisfaire un particulier append] chaque fois qu'il manque de place).

Vous avez peut - être entendu dire que la concaténation de chaîne va utiliser un StringBuildersous les couvertures si compilé avec le compilateur Sun / Oracle. C'est vrai, il en utilisera un StringBuilderpour l'expression globale. Mais il utilisera le constructeur par défaut, ce qui signifie que dans la majorité des cas, il devra faire une réallocation. C'est plus facile à lire, cependant. Notez que ce n'est pas le cas pour une série de concaténations. Ainsi, par exemple, cela en utilise un StringBuilder:

return "prefix " + variable1 + " middle " + variable2 + " end";

Cela se traduit en gros par:

StringBuilder tmp = new StringBuilder(); // Using default 16 character size
tmp.append("prefix ");
tmp.append(variable1);
tmp.append(" middle ");
tmp.append(variable2);
tmp.append(" end");
return tmp.toString();

Donc ce n'est pas grave, bien que le constructeur par défaut et les réallocations ultérieures ne soient pas idéales, les chances sont que c'est assez bon - et la concaténation est beaucoup plus lisible.

Mais ce n'est que pour une seule expression. Plusieurs StringBuilders sont utilisés pour cela:

String s;
s = "prefix ";
s += variable1;
s += " middle ";
s += variable2;
s += " end";
return s;

Cela finit par devenir quelque chose comme ça:

String s;
StringBuilder tmp;
s = "prefix ";
tmp = new StringBuilder();
tmp.append(s);
tmp.append(variable1);
s = tmp.toString();
tmp = new StringBuilder();
tmp.append(s);
tmp.append(" middle ");
s = tmp.toString();
tmp = new StringBuilder();
tmp.append(s);
tmp.append(variable2);
s = tmp.toString();
tmp = new StringBuilder();
tmp.append(s);
tmp.append(" end");
s = tmp.toString();
return s;

... ce qui est assez moche.

Il est important de se rappeler, cependant, que dans tous les cas, sauf dans de très rares cas, cela n'a pas d'importance et que la lisibilité (qui améliore la maintenabilité) est préférable, sauf pour un problème de performance spécifique.

TJ Crowder
la source
Oui, c'est mieux. L'utilisation du constructeur sans paramètre est un peu malheureuse, mais peu susceptible d'être significative. J'utiliserais toujours une seule x + y + zexpression plutôt que StringBuildersi j'avais de bonnes raisons de soupçonner que ce serait un problème important.
Jon Skeet
@Crowder a encore un doute. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... Des sql.appendlignes similaires sont d'environ 60 lignes. Est-ce bien?
Vaandu le
1
@Vanathi: ("Question", pas "doute" - c'est une erreur de traduction courante.) C'est bien, mais cela entraînera probablement plusieurs réallocations, car le StringBuildersera initialement alloué assez de place pour la chaîne que vous avez passée au constructeur plus 16 caractères. Donc, si vous ajoutez plus de 16 caractères (j'ose dire que vous l'êtes, s'il y en a 60!), Le StringBuilderdevra réallouer au moins une fois et peut-être plusieurs fois. Si vous avez une idée raisonnable de la taille du résultat final (disons, 400 caractères), il est préférable de faire sql = new StringBuilder(400);(ou quoi que ce soit) puis de faire le appends.
TJ Crowder
@Vanathi: Heureux que cela ait aidé. Ouais, s'il doit y avoir 6000 caractères, dire StringBuilderqu'à l'avance économisera environ huit réallocations de mémoire (en supposant que la chaîne initiale était d'environ 10 caractères, le SB aurait été de 26 au départ, puis doublé à 52, puis 104, 208, 416, 832, 1664, 3328 et enfin 6656). Seulement significatif s'il s'agit d'un hotspot, mais quand même, si vous le savez à l'avance ... :-)
TJ Crowder
@TJ Crowder vous voulez dire que je ne dois pas utiliser l'opérateur "+" pour de meilleures performances. droite? alors pourquoi Oracal a ajouté l'opérateur "+" dans leur langue pouvez-vous s'il vous plaît élaborer? de toute façon mon vote pour votre réponse.
Smit Patel
38

Lorsque vous avez déjà tous les "morceaux" que vous souhaitez ajouter, il est inutile d'utiliser StringBuilderdu tout. L'utilisation StringBuilder et la concaténation de chaînes dans le même appel que votre exemple de code est encore pire.

Ce serait mieux:

return "select id1, " + " id2 " + " from " + " table";

Dans ce cas, la concaténation de chaînes se produit de toute façon au moment de la compilation , c'est donc l'équivalent du plus simple:

return "select id1, id2 from table";

L'utilisation new StringBuilder().append("select id1, ").append(" id2 ")....toString()réduira en fait les performances dans ce cas, car elle force la concaténation à être effectuée au moment de l'exécution , plutôt qu'au moment de la compilation . Oups.

Si le code réel construit une requête SQL en incluant des valeurs dans la requête, il s'agit d'un autre problème distinct , à savoir que vous devez utiliser des requêtes paramétrées, en spécifiant les valeurs dans les paramètres plutôt que dans le SQL.

J'ai un article sur String/ surStringBuffer lequel j'ai écrit il y a quelque temps - avant d' StringBuilderarriver. Les principes s'appliquent cependant StringBuilderde la même manière.

Jon Skeet
la source
10

[[Il y a de bonnes réponses ici, mais je trouve qu'elles manquent encore d'un peu d'informations. ]]

return (new StringBuilder("select id1, " + " id2 " + " from " + " table"))
     .toString();

Donc, comme vous le faites remarquer, l'exemple que vous donnez est simpliste mais analysons-le quand même. Ce qui se passe ici, c'est que le compilateur fait réellement le +travail ici car ce "select id1, " + " id2 " + " from " + " table"sont toutes des constantes. Donc, cela se transforme en:

return new StringBuilder("select id1,  id2  from  table").toString();

Dans ce cas, évidemment, il ne sert à rien d'utiliser StringBuilder. Vous pourriez aussi bien faire:

// the compiler combines these constant strings
return "select id1, " + " id2 " + " from " + " table";

Cependant, même si vous ajoutiez des champs ou d'autres non-constantes, le compilateur utiliserait un interne StringBuilder - vous n'avez pas besoin d'en définir un:

// an internal StringBuilder is used here
return "select id1, " + fieldName + " from " + tableName;

Sous les couvertures, cela se transforme en code qui équivaut à peu près à:

StringBuilder sb = new StringBuilder("select id1, ");
sb.append(fieldName).append(" from ").append(tableName);
return sb.toString();

Vraiment, le seul moment où vous devez utiliser StringBuilder directement est lorsque vous avez du code conditionnel. Par exemple, un code qui ressemble à ce qui suit est désespéré pour un StringBuilder:

// 1 StringBuilder used in this line
String query = "select id1, " + fieldName + " from " + tableName;
if (where != null) {
   // another StringBuilder used here
   query += ' ' + where;
}

Le +dans la première ligne utilise une StringBuilderinstance. Ensuite, +=utilise une autre StringBuilderinstance. Il est plus efficace de faire:

// choose a good starting size to lower chances of reallocation
StringBuilder sb = new StringBuilder(64);
sb.append("select id1, ").append(fieldName).append(" from ").append(tableName);
// conditional code
if (where != null) {
   sb.append(' ').append(where);
}
return sb.toString();

Une autre fois que j'utilise a, StringBuilderc'est lorsque je construis une chaîne à partir d'un certain nombre d'appels de méthode. Ensuite, je peux créer des méthodes qui prennent un StringBuilderargument:

private void addWhere(StringBuilder sb) {
   if (where != null) {
      sb.append(' ').append(where);
   }
}

Lorsque vous utilisez a StringBuilder, vous devez surveiller toute utilisation de +en même temps:

sb.append("select " + fieldName);

Cela +entraînera la StringBuildercréation d' un autre interne . Cela devrait bien sûr être:

sb.append("select ").append(fieldName);

Enfin, comme le souligne @TJrowder, vous devez toujours deviner la taille du fichier StringBuilder. Cela permettra d'économiser sur le nombre d' char[]objets créés lors de l'augmentation de la taille du tampon interne.

gris
la source
4

Vous avez raison de supposer que l'objectif de l'utilisation du générateur de chaînes n'est pas atteint, du moins pas dans toute sa mesure.

Cependant, lorsque le compilateur voit l'expression, "select id1, " + " id2 " + " from " + " table"il émet du code qui crée en fait un StringBuilderderrière la scène et y ajoute, de sorte que le résultat final n'est pas si mal après tout.

Mais bien sûr, quiconque regarde ce code est tenu de penser qu'il est un peu retardé.

Mike Nakis
la source
2

Dans le code que vous avez publié, il n'y aurait aucun avantage, car vous utilisez à mauvais escient StringBuilder. Vous créez la même chaîne dans les deux cas. En utilisant StringBuilder, vous pouvez éviter l' +opération sur des chaînes en utilisant la appendméthode. Vous devriez l'utiliser de cette façon:

return new StringBuilder("select id1, ").append(" id2 ").append(" from ").append(" table").toString();

En Java, le type String est une séquence non modifiable de caractères, donc lorsque vous ajoutez deux chaînes, la machine virtuelle crée une nouvelle valeur String avec les deux opérandes concaténés.

StringBuilder fournit une séquence de caractères modifiables, que vous pouvez utiliser pour concater différentes valeurs ou variables sans créer de nouveaux objets String, ce qui peut parfois être plus efficace que de travailler avec des chaînes

Cela fournit des fonctionnalités utiles, comme la modification du contenu d'une séquence char passée en paramètre dans une autre méthode, ce que vous ne pouvez pas faire avec Strings.

private void addWhereClause(StringBuilder sql, String column, String value) {
   //WARNING: only as an example, never append directly a value to a SQL String, or you'll be exposed to SQL Injection
   sql.append(" where ").append(column).append(" = ").append(value);
}

Plus d'informations sur http://docs.oracle.com/javase/tutorial/java/data/buffers.html

Tomas Narros
la source
1
Non, tu ne devrais pas. C'est moins lisible que using +, qui sera de toute façon converti dans le même code. StringBuilderest utile lorsque vous ne pouvez pas effectuer toute la concaténation dans une seule expression, mais pas dans ce cas.
Jon Skeet le
1
Je comprends que la chaîne de la question est publiée à titre d'exemple. Cela n'aurait aucun sens de construire une chaîne "fixe" comme celle-ci ni avec StringBuilder ni en ajoutant différents fragments, car vous pourriez simplement le définir dans une seule constante "select id1, id2 from table"
Tomas Narros
Mais même s'il y avait des valeurs non constantes à partir de variables, il en utiliserait toujours une seule StringBuildersi vous deviez en utiliser return "select id1, " + foo + "something else" + bar;- alors pourquoi ne pas le faire? La question ne donne aucune indication que quoi que ce soit doit passer StringBuilder.
Jon Skeet
1

Vous pouvez également utiliser MessageFormat aussi

JGFMK
la source