Forma correcta de usar StringBuilder en SQL

88

Acabo de encontrar una construcción de consulta SQL como esta en mi proyecto:

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

¿Esto StringBuilderlogra su objetivo, es decir, reducir el uso de memoria?

Lo dudo, porque en el constructor se usa el '+' (operador String concat). ¿Tomará la misma cantidad de memoria que usar String como el siguiente código? s lo he entendido, difiere cuando se usa StringBuilder.append().

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

¿Ambas declaraciones son iguales en el uso de memoria o no? Por favor aclare.

¡Gracias por adelantado!

Editar:

Por cierto, no es mi código . Lo encontré en un proyecto antiguo. Además, la consulta no es tan pequeña como la de mi ejemplo. :)

Vaandu
fuente
1
SEGURIDAD SQL: use siempre PreparedStatemento algo similar: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe Roussy
Aparte del uso de memoria, ¿por qué no usar una biblioteca de constructor SQL en su lugar: stackoverflow.com/q/370818/521799
Lukas Eder

Respuestas:

182

El objetivo de utilizar StringBuilder, es decir, reducir la memoria. ¿Se logra?

No, en absoluto. Ese código no se está usando StringBuildercorrectamente. (Sin embargo, creo que lo has citado mal; seguramente no hay citas id2y table?)

Tenga en cuenta que el objetivo (generalmente) es reducir la pérdida de memoria en lugar de la memoria total utilizada, para facilitar un poco la vida del recolector de basura.

¿Eso llevará memoria igual a usar String como se muestra a continuación?

No, causará más pérdida de memoria que el simple concat que citó. (Hasta / a menos que el optimizador de JVM vea que lo explícito StringBuilderen el código es innecesario y lo optimice, si puede).

Si el autor de ese código quiere usar StringBuilder(hay argumentos a favor, pero también en contra; vea la nota al final de esta respuesta), es mejor hacerlo correctamente (aquí asumo que en realidad no hay citas id2y table):

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

Tenga en cuenta que he enumerado some_appropriate_sizeen el StringBuilderconstructor, por lo que comienza con suficiente capacidad para el contenido completo que vamos a agregar. El tamaño predeterminado que se usa si no especifica uno es de 16 caracteres , que generalmente es demasiado pequeño y da como resultado StringBuildertener que realizar reasignaciones para hacerse más grande (IIRC, en Sun / Oracle JDK, se duplica [o más, si sabe que necesita más para satisfacer una determinada append] cada vez que se queda sin espacio).

Usted puede haber oído que la concatenación de cadenas se utilice un StringBuilderbajo las sábanas si se compila con el compilador de Sun / Oracle. Esto es cierto, usará uno StringBuilderpara la expresión general. Pero usará el constructor predeterminado, lo que significa que en la mayoría de los casos tendrá que realizar una reasignación. Sin embargo, es más fácil de leer. Tenga en cuenta que esto no es cierto para una serie de concatenaciones. Entonces, por ejemplo, esto usa uno StringBuilder:

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

Se traduce aproximadamente a:

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();

Así que está bien, aunque el constructor predeterminado y las reasignaciones posteriores no son ideales, lo más probable es que sea lo suficientemente bueno, y la concatenación es mucho más legible.

Pero eso es solo para una sola expresión. Se StringBuilderutilizan varios s para esto:

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

Eso termina convirtiéndose en algo como esto:

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;

... que es bastante feo.

Sin embargo, es importante recordar que en todos los casos , excepto en unos pocos , no importa y se prefiere ir con legibilidad (que mejora la capacidad de mantenimiento) salvo un problema de rendimiento específico.

TJ Crowder
fuente
Bien, eso es mejor. El uso del constructor sin parámetros es un poco desafortunado, pero es poco probable que sea significativo. Seguiría usando una sola x + y + zexpresión en lugar de a StringBuildermenos que tuviera una buena razón para sospechar que sería un problema importante.
Jon Skeet
@Crowder tiene una duda más. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... sql.appendLíneas similares son alrededor de 60 líneas. ¿Está bien esto?
Vaandu
1
@Vanathi: ("Pregunta", no "duda" - es una mala traducción común.) Está bien, pero probablemente resultará en múltiples reasignaciones, porque StringBuilderinicialmente se le asignará suficiente espacio para la cadena que le pasó al constructor más 16 caracteres. Entonces, si agrega más de 16 caracteres (¡me atrevería a decir que sí, si hay 60 anexos!), StringBuilderTendrá que reasignar al menos una vez y posiblemente muchas veces. Si tiene una idea razonable de qué tan grande será el resultado final (digamos, 400 caracteres), es mejor hacer sql = new StringBuilder(400);(o lo que sea) y luego hacer lo appends.
TJ Crowder
@Vanathi: Me alegro de que haya ayudado. Sí, si va a tener 6.000 caracteres, decirlo StringBuilderde antemano ahorrará alrededor de ocho reasignaciones de memoria (suponiendo que la cadena inicial fuera de unos 10 caracteres, el SB habría sido 26 para empezar, luego se duplicó a 52, luego 104, 208, 416, 832, 1664, 3328 y finalmente 6656). Solo es significativo si se trata de un punto de acceso, pero aún así, si lo sabe de antemano ... :-)
TJ Crowder
@TJ Crowder quiere decir que no debo usar el operador "+" para un mejor rendimiento. ¿Correcto? Entonces, ¿por qué Oracal ha agregado el operador "+" en su idioma, por favor, puede explicarme de cualquier manera mi voto a favor por su respuesta.
Smit Patel
38

Cuando ya tiene todas las "piezas" que desea agregar, no tiene sentido usarlas StringBuilderen absoluto. Utilizando StringBuilder y la concatenación de cadenas en la misma llamada según su código de ejemplo es aún peor.

Esto sería mejor:

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

En este caso, la concatenación de cadenas está ocurriendo realmente en tiempo de compilación de todos modos, por lo que es equivalente a la incluso más simple:

return "select id1, id2 from table";

El uso en new StringBuilder().append("select id1, ").append(" id2 ")....toString()realidad obstaculizará el rendimiento en este caso, porque obliga a que la concatenación se realice en el momento de la ejecución , en lugar de en el momento de la compilación . ¡Ups!

Si el código real está construyendo una consulta SQL al incluir valores en la consulta, entonces ese es otro problema separado , que es que debería usar consultas parametrizadas, especificando los valores en los parámetros en lugar de en SQL.

Tengo un artículo sobre el String/StringBuffer la que escribí hace un tiempo - antes de que StringBuilderse presentara. Sin StringBuilderembargo, los principios se aplican de la misma manera.

Jon Skeet
fuente
10

[[Hay algunas buenas respuestas aquí, pero encuentro que todavía les falta un poco de información. ]]

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

Entonces, como señala, el ejemplo que da es simplista, pero analicémoslo de todos modos. Lo que sucede aquí es que el compilador realmente hace el +trabajo aquí porque "select id1, " + " id2 " + " from " + " table"son todas constantes. Entonces esto se convierte en:

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

En este caso, obviamente, no tiene sentido usar StringBuilder. También podrías hacer:

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

Sin embargo, incluso si estuviera agregando campos u otras no constantes, el compilador usaría un interno StringBuilder ; no es necesario que defina uno:

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

Bajo las sábanas, esto se convierte en un código que es aproximadamente equivalente a:

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

Realmente, la única vez que necesita usar StringBuilder directamente es cuando tiene código condicional. Por ejemplo, el código que se parece a lo siguiente está desesperado por un StringBuilder:

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

El +de la primera línea usa una StringBuilderinstancia. Luego, +=usa otra StringBuilderinstancia. Es más eficiente hacer:

// 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();

Otra ocasión en la que utilizo un StringBuilderes cuando estoy construyendo una cadena a partir de varias llamadas a métodos. Entonces puedo crear métodos que tengan un StringBuilderargumento:

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

Cuando esté usando a StringBuilder, debe estar atento a cualquier uso de +al mismo tiempo:

sb.append("select " + fieldName);

Eso +hará StringBuilderque se cree otro interno . Por supuesto, esto debería ser:

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

Por último, como señala @TJrowder, siempre debe adivinar el tamaño del StringBuilder. Esto ahorrará en la cantidad de char[]objetos creados al aumentar el tamaño del búfer interno.

gris
fuente
4

Tiene razón al suponer que el objetivo de usar el generador de cadenas no se logra, al menos no en toda su extensión.

Sin embargo, cuando el compilador ve la expresión "select id1, " + " id2 " + " from " + " table", emite un código que en realidad crea un StringBuilderdetrás de escena y lo agrega, por lo que el resultado final no es tan malo después de todo.

Pero, por supuesto, cualquiera que mire ese código seguramente pensará que es un poco retrasado.

Mike Nakis
fuente
2

En el código que ha publicado no habría ventajas, ya que está haciendo un mal uso de StringBuilder. Construyes la misma cadena en ambos casos. Usando StringBuilder puedes evitar la +operación en Strings usando el appendmétodo. Deberías usarlo de esta manera:

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

En Java, el tipo de cadena es una secuencia inmutable de caracteres, por lo que cuando agrega dos cadenas, la máquina virtual crea un nuevo valor de cadena con ambos operandos concatenados.

StringBuilder proporciona una secuencia mutable de caracteres, que puede usar para concanear diferentes valores o variables sin crear nuevos objetos String, por lo que a veces puede ser más eficiente que trabajar con cadenas

Esto proporciona algunas características útiles, como cambiar el contenido de una secuencia de caracteres pasada como parámetro dentro de otro método, lo que no se puede hacer con 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);
}

Más información en http://docs.oracle.com/javase/tutorial/java/data/buffers.html

Tomás Narros
fuente
1
No, no deberías. Es menos legible que usarlo +, que se convertirá en el mismo código de todos modos. StringBuilderes útil cuando no puede realizar toda la concatenación en una sola expresión, pero no en este caso.
Jon Skeet
1
Entiendo que la cadena de la pregunta se publica como ejemplo. No tendría sentido construir una cadena "fija" como esta ni con StringBuilder ni agregando diferentes fragmentos, ya que podría definirla en una sola constante "seleccionar id1, id2 de la tabla"
Tomas Narros
Pero incluso si hubiera valores no constantes de las variables, todavía usaría un solo StringBuildersi fuera a usar return "select id1, " + foo + "something else" + bar;, entonces, ¿por qué no hacerlo? La pregunta no proporciona ninguna indicación de que algo deba pasar StringBuilder.
Jon Skeet