Maneira correta de usar StringBuilder em SQL

88

Acabei de encontrar alguma construção de consulta sql como esta no meu projeto:

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

Isso StringBuilderatinge seu objetivo, ou seja, reduzir o uso de memória?

Duvido, porque no construtor é usado o '+' (operador String concat). Isso consumirá a mesma quantidade de memória que usar String como o código abaixo? s eu entendi, ele difere durante o uso StringBuilder.append().

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

Ambas as instruções são iguais no uso de memória ou não? Por favor, esclareça.

Desde já, obrigado!

Editar:

BTW, não é meu código . Encontrei em um projeto antigo. Além disso, a consulta não é tão pequena quanto a do meu exemplo. :)

Vaandu
fonte
1
SEGURANÇA SQL: use sempre PreparedStatementou algo semelhante: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe Roussy
Além do uso de memória, por que não usar uma biblioteca SQL builder: stackoverflow.com/q/370818/521799
Lukas Eder

Respostas:

182

O objetivo de usar StringBuilder, ou seja, reduzir a memória. Isso foi alcançado?

Não, de forma alguma. Esse código não está sendo usado StringBuildercorretamente. (Eu acho que você citou errado, entretanto; certamente não há aspas id2e table?)

Observe que o objetivo (geralmente) é reduzir a rotatividade de memória em vez da memória total usada, para tornar a vida um pouco mais fácil para o coletor de lixo.

Isso levará memória igual ao uso de String como abaixo?

Não, isso causará mais rotatividade de memória do que apenas o concat direto que você citou. (Até / a menos que o otimizador JVM veja que o explícito StringBuilderno código é desnecessário e o otimize, se puder.)

Se o autor desse código quiser usar StringBuilder(há argumentos a favor, mas também contra; consulte a nota no final desta resposta), melhor fazer isso corretamente (aqui estou assumindo que não há aspas id2e table):

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

Observe que listei some_appropriate_sizeno StringBuilderconstrutor, de forma que ele comece com capacidade suficiente para todo o conteúdo que iremos anexar. O tamanho padrão usado se você não especificar um é de 16 caracteres , que geralmente é muito pequeno e resulta na StringBuildernecessidade de fazer realocações para se tornar maior (IIRC, no Sun / Oracle JDK, ele se duplica [ou mais, se sabe que precisa de mais para satisfazer um determinado append] cada vez que fica sem espaço).

Você pode ter ouvido que concatenação irá usar um StringBuilderdebaixo das cobertas se compilado com o compilador Sun / Oracle. Isso é verdade, ele usará um StringBuilderpara a expressão geral. Mas ele usará o construtor padrão, o que significa que na maioria dos casos terá que fazer uma realocação. É mais fácil de ler, no entanto. Observe que isso não é verdade para uma série de concatenações. Então, por exemplo, ele usa um StringBuilder:

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

Isso se traduz aproximadamente em:

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

Então está tudo bem, embora o construtor padrão e as realocações subsequentes não sejam ideais, as chances são boas o suficiente - e a concatenação é muito mais legível.

Mas isso é apenas para uma única expressão. Vários StringBuilders são usados ​​para isso:

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

Isso acaba se tornando algo assim:

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;

... o que é muito feio.

É importante lembrar, porém, que em todos os casos , exceto em alguns casos , isso não importa e ir com a legibilidade (o que aumenta a capacidade de manutenção) é preferível, exceto um problema de desempenho específico.

TJ Crowder
fonte
Certo, está melhor. O uso do construtor sem parâmetros é um pouco infeliz, mas improvável que seja significativo. Eu ainda usaria uma única x + y + zexpressão em vez de, a StringBuildermenos que eu tivesse um bom motivo para suspeitar que seria um problema significativo.
Jon Skeet
@Crowder tem mais uma dúvida. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... sql.appendLinhas semelhantes têm cerca de 60 linhas. Esta bem?
Vaandu
1
@Vanathi: ("Pergunta", não "dúvida" - é um erro de tradução comum.) Tudo bem, mas provavelmente resultará em várias realocações, porque StringBuilderinicialmente será alocado espaço suficiente para a string que você passou ao construtor mais 16 caracteres. Portanto, se você anexar mais de 16 caracteres (ouso dizer que sim, se houver 60 anexos!), O StringBuilderterá que realocar pelo menos uma vez e possivelmente várias vezes. Se você tiver uma ideia razoável de quão grande será o resultado final (digamos, 400 caracteres), é melhor fazer sql = new StringBuilder(400);(ou o que quer que seja) do que fazer o appends.
TJ Crowder
@Vanathi: Que bom que ajudou. Sim, se for 6.000 caracteres, dizer StringBuilderisso com antecedência economizará cerca de oito realocações de memória (assumindo que a string inicial tinha cerca de 10 caracteres, o SB teria sido de 26 para começar, depois dobrado para 52, depois 104, 208, 416, 832, 1664, 3328 e finalmente 6656). Apenas significativo se for um ponto de acesso, mas ainda assim, se você souber com antecedência ... :-)
TJ Crowder
@TJ Crowder, você quer dizer que não devo usar o operador "+" para um melhor desempenho. certo? então por que Oracal adicionou o operador "+" em seu idioma, você pode explicar? De qualquer forma, meu voto positivo para sua resposta.
Smit Patel
38

Quando você já tem todas as "peças" que deseja acrescentar, não há sentido em usar StringBuilder. Usando StringBuilder e concatenação na mesma chamada de acordo com o seu código de amostra é ainda pior.

Isso seria melhor:

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

Nesse caso, a concatenação de string está realmente acontecendo em tempo de compilação de qualquer maneira, então é equivalente ao ainda mais simples:

return "select id1, id2 from table";

O uso new StringBuilder().append("select id1, ").append(" id2 ")....toString()realmente prejudicará o desempenho neste caso, porque força a concatenação a ser realizada em tempo de execução , em vez de em tempo de compilação . Opa.

Se o código real está construindo uma consulta SQL incluindo valores na consulta, então esse é outro problema separado , que é que você deve usar consultas parametrizadas, especificando os valores nos parâmetros em vez de no SQL.

Eu tenho um artigo sobre String/StringBuffer que escrevi há um tempo - antes de StringBuilderaparecer. Os princípios se aplicam StringBuilderda mesma forma.

Jon Skeet
fonte
10

[[Existem algumas respostas boas aqui, mas acho que ainda faltam um pouco de informação. ]]

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

Como você indicou, o exemplo que você deu é simplista, mas vamos analisá-lo de qualquer maneira. O que acontece aqui é que o compilador realmente faz o +trabalho aqui porque "select id1, " + " id2 " + " from " + " table"são todos constantes. Então, isso se transforma em:

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

Nesse caso, obviamente, não adianta usar StringBuilder. Você também pode fazer:

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

No entanto, mesmo se você estivesse anexando quaisquer campos ou outros não constantes, o compilador usaria um interno StringBuilder - não há necessidade de você definir um:

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

Nos bastidores, isso se transforma em código que é aproximadamente equivalente a:

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

Realmente, a única vez que você precisa usar StringBuilder diretamente é quando você tem código condicional. Por exemplo, o código que se parece com o seguinte está desesperado por um StringBuilder:

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

O +na primeira linha usa uma StringBuilderinstância. Então o +=usa outroStringBuilder instância. É mais eficiente fazer:

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

Outra vez que uso um StringBuilderé quando estou construindo uma string a partir de várias chamadas de método. Então, posso criar métodos que recebam um StringBuilderargumento:

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

Quando estiver usando um StringBuilder, você deve observar se há uso de +ao mesmo tempo:

sb.append("select " + fieldName);

Isso +vai causar outro internoStringBuilder seja criado. Claro que deveria ser:

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

Por fim, como @TJrowder aponta, você sempre deve tentar adivinhar o tamanho do StringBuilder. Isso economizará no número de char[]objetos criados ao aumentar o tamanho do buffer interno.

cinzento
fonte
4

Você está correto ao supor que o objetivo de usar o construtor de cordas não foi alcançado, pelo menos não em toda a sua extensão.

No entanto, quando o compilador vê a expressão, "select id1, " + " id2 " + " from " + " table"ele emite um código que, na verdade, cria um StringBuildersegundo plano e o anexa, de modo que o resultado final não é tão ruim assim.

Mas é claro que qualquer pessoa que olhar para esse código vai pensar que ele é meio retardado.

Mike Nakis
fonte
2

No código que você postou, não haveria vantagens, pois você está usando incorretamente o StringBuilder. Você constrói a mesma String em ambos os casos. Usando StringBuilder, você pode evitar a +operação em Strings usando o appendmétodo. Você deve usá-lo desta forma:

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

Em Java, o tipo String é uma sequência inmutável de caracteres, portanto, quando você adiciona duas Strings, a VM cria um novo valor String com ambos os operandos concatenados.

StringBuilder fornece uma sequência mutável de caracteres, que você pode usar para conciliar diferentes valores ou variáveis ​​sem criar novos objetos String e, portanto, às vezes pode ser mais eficiente do que trabalhar com strings

Isso fornece alguns recursos úteis, como alterar o conteúdo de uma sequência de caracteres passada como parâmetro dentro de outro método, o que você não pode fazer com 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);
}

Mais informações em http://docs.oracle.com/javase/tutorial/java/data/buffers.html

Tomas Narros
fonte
1
Não, você não deveria. É menos legível do que usar +, que será convertido no mesmo código de qualquer maneira. StringBuilderé útil quando você não pode realizar toda a concatenação em uma única expressão, mas não neste caso.
Jon Skeet
1
Eu entendo que a string em questão é postada como exemplo. Não faria sentido construir uma string "fixa" como esta nem com StringBuilder nem adicionando fragmentos diferentes, já que você poderia apenas defini-la em uma única constante "select id1, id2 from table"
Tomas Narros
Mas mesmo que houvesse valores não constantes nas variáveis, ainda usaria um único StringBuilderse você fosse usar return "select id1, " + foo + "something else" + bar;- então por que não fazer isso? A pergunta não fornece nenhuma indicação de que algo precisa ser divulgado StringBuilder.
Jon Skeet
1

Você também pode usar MessageFormat demais

JGFMK
fonte