Modo corretto di utilizzare StringBuilder in SQL


88

Ho appena trovato alcune build di query sql come questa nel mio progetto:

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

Questo StringBuilderraggiunge il suo obiettivo, ovvero ridurre l'utilizzo della memoria?

Ne dubito, perché nel costruttore viene utilizzato il "+" (operatore di concatenazione di stringhe). Ciò richiederà la stessa quantità di memoria dell'utilizzo di String come il codice seguente? s Ho capito, è diverso quando si usa StringBuilder.append().

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

Entrambe le affermazioni sono uguali nell'utilizzo della memoria o no? Si prega di precisare.

Grazie in anticipo!

Modificare:

BTW, non è il mio codice . Trovato in un vecchio progetto. Inoltre, la query non è così piccola come quella nel mio esempio. :)


1
SICUREZZA SQL: usa sempre PreparedStatemento qualcosa di simile: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe Roussy

A parte l'utilizzo della memoria, perché non utilizzare invece una libreria di SQL Builder: stackoverflow.com/q/370818/521799
Lukas Eder

Risposte:


182

Lo scopo di utilizzare StringBuilder, ovvero ridurre la memoria. È stato raggiunto?

No, per niente. Quel codice non viene utilizzato StringBuildercorrettamente. (Penso che tu l'abbia citato erroneamente, però; sicuramente non ci sono citazioni in giro id2e table?)

Si noti che l'obiettivo (di solito) è ridurre il tasso di abbandono della memoria piuttosto che la memoria totale utilizzata, per rendere la vita un po 'più facile al garbage collector.

Richiederà memoria uguale all'uso di String come sotto?

No, causerà più turbolenze della memoria rispetto al semplice concatenato che hai citato. (Finché / a meno che l'ottimizzatore JVM non veda che l'esplicito StringBuildernel codice non è necessario e lo ottimizzi, se possibile.)

Se l'autore di quel codice vuole usare StringBuilder(ci sono argomenti a favore, ma anche contro; vedi nota alla fine di questa risposta), meglio farlo correttamente (qui presumo che non ci siano effettivamente virgolette intorno 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();

Nota che ho elencato some_appropriate_sizenel StringBuildercostruttore, in modo che inizi con una capacità sufficiente per l'intero contenuto che aggiungeremo. La dimensione predefinita utilizzata se non ne specifichi uno è di 16 caratteri , che di solito è troppo piccola e comporta la StringBuildernecessità di riallocazioni per ingrandirsi (IIRC, nel JDK Sun / Oracle, si raddoppia [o più, se sa che ha bisogno di più per soddisfare uno specifico append] ogni volta che esaurisce lo spazio).

Potreste aver sentito che la concatenazione di stringhe sarà utilizzare un StringBuildersotto le coperte, se compilato con il compilatore Sun / Oracle. Questo è vero, ne userà uno StringBuilderper l'espressione generale. Ma utilizzerà il costruttore predefinito, il che significa che nella maggior parte dei casi dovrà eseguire una riallocazione. Tuttavia è più facile da leggere. Notare che questo non è vero per una serie di concatenazioni. Quindi, ad esempio, questo ne usa uno StringBuilder:

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

Si traduce approssimativamente in:

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

Quindi va bene, anche se il costruttore predefinito e le successive riallocazioni non sono l'ideale, le probabilità sono che siano abbastanza buone e la concatenazione è molto più leggibile.

Ma questo è solo per una singola espressione. Molteplici StringBuilders sono utilizzati per questo:

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

Finisce per diventare qualcosa del genere:

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;

... che è piuttosto brutto.

È importante ricordare, tuttavia, che in tutti i casi tranne in pochissimi non ha importanza e si preferisce la leggibilità (che migliora la manutenibilità) salvo un problema di prestazioni specifico.


Bene, va meglio. L'uso del costruttore senza parametri è leggermente sfortunato, ma è improbabile che sia significativo. Userei comunque una singola x + y + zespressione piuttosto che a StringBuildermeno che non avessi buone ragioni per sospettare che sarebbe stato un problema significativo.
Jon Skeet

@ Crowder ha ancora un dubbio. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... Le sql.appendlinee simili sono circa 60 linee. Va bene?
Vaandu

1
@ Vanathi: ("Domanda", non "dubbio" - è un errore di traduzione comune.) Va bene ma probabilmente si tradurrà in più riallocazioni, perché StringBuilderinizialmente verrà assegnato spazio sufficiente per la stringa che hai passato al costruttore più 16 caratteri. Quindi se aggiungi più di 16 caratteri (oserei dire, se ci sono 60 aggiunte!), StringBuilderDovrà riallocare almeno una volta e possibilmente molte volte. Se hai un'idea ragionevole di quanto sarà grande il risultato finale (diciamo 400 caratteri), è meglio fare sql = new StringBuilder(400);(o qualsiasi altra cosa) e poi fare la appends.
TJ Crowder

@ Vanathi: Sono contento che abbia aiutato. Sì, se saranno 6.000 caratteri, dire StringBuilderche in anticipo salverà circa otto riallocazioni di memoria (supponendo che la stringa iniziale fosse di circa 10 caratteri, SB sarebbe stato 26 per iniziare, quindi raddoppiato a 52, quindi 104, 208, 416, 832, 1664, 3328 e infine 6656). Significativo solo se questo è un hotspot, ma comunque, se lo sai in anticipo ... :-)
TJ Crowder

@TJ Crowder intendi dire che non devo usare l'operatore "+" per ottenere prestazioni migliori. giusto? allora perché Oracal ha aggiunto l'operatore "+" nella loro lingua puoi per favore elaborare? in qualche modo il mio voto positivo per la tua risposta.
Smit Patel

38

Quando hai già tutti i "pezzi" che desideri aggiungere, non ha alcun senso utilizzarli StringBuilder. Uso StringBuilder e concatenazione di stringhe nella stessa chiamata secondo il vostro codice di esempio è ancora peggio.

Questo sarebbe meglio:

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

In questo caso, la concatenazione di stringhe avviene comunque in fase di compilazione , quindi è equivalente a quella ancora più semplice:

return "select id1, id2 from table";

L'utilizzo ostacolerànew StringBuilder().append("select id1, ").append(" id2 ")....toString() effettivamente le prestazioni in questo caso, perché impone l' esecuzione della concatenazione in fase di esecuzione , invece che in fase di compilazione . Ops.

Se il codice reale sta creando una query SQL includendo valori nella query, allora questo è un altro problema separato , ovvero che dovresti usare query parametrizzate, specificando i valori nei parametri piuttosto che nell'SQL.

Ho un articolo su String/StringBuffer che ho scritto qualche tempo fa - prima che StringBuilderarrivasse. I principi si applicano allo StringBuilderstesso modo però.


10

[[Ci sono alcune buone risposte qui, ma trovo che manchino ancora un po 'di informazioni. ]]

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

Quindi, come fai notare, l'esempio che fornisci è semplicistico ma analizziamolo comunque. Quello che succede qui è che il compilatore fa effettivamente il +lavoro qui perché "select id1, " + " id2 " + " from " + " table"sono tutte costanti. Quindi questo si trasforma in:

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

In questo caso, ovviamente, non ha senso usare StringBuilder. Potresti anche fare:

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

Tuttavia, anche se si aggiungessero campi o altre non costanti, il compilatore utilizzerebbe un interno StringBuilder : non è necessario definirne uno:

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

Sotto le coperte, questo si trasforma in codice che è approssimativamente equivalente a:

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

In realtà l'unica volta che devi usare StringBuilder direttamente è quando hai codice condizionale. Ad esempio, un codice simile al seguente è alla disperata ricerca di un StringBuilder:

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

La +nella prima riga utilizza un StringBuilderesempio. Quindi +=utilizza un'altra StringBuilderistanza. È più efficiente fare:

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

Un'altra volta che uso a StringBuilderè quando creo una stringa da un numero di chiamate di metodo. Quindi posso creare metodi che accettano un StringBuilderargomento:

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

Quando stai usando a StringBuilder, dovresti guardare per qualsiasi utilizzo +allo stesso tempo:

sb.append("select " + fieldName);

Ciò +causerà la StringBuildercreazione di un altro interno . Questo dovrebbe ovviamente essere:

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

Infine, come sottolinea @TJrowder, dovresti sempre fare un'ipotesi sulla dimensione del file StringBuilder. Ciò consentirà di risparmiare sul numero di char[]oggetti creati aumentando la dimensione del buffer interno.


4

Hai ragione nell'indovinare che l'obiettivo di utilizzare String Builder non è stato raggiunto, almeno non nella sua piena estensione.

Tuttavia, quando il compilatore vede l'espressione "select id1, " + " id2 " + " from " + " table", emette codice che in realtà crea un StringBuilderdietro le quinte e lo aggiunge, quindi il risultato finale non è poi così male.

Ma ovviamente chiunque guardi quel codice è destinato a pensare che sia un po 'ritardato.


2

Nel codice che hai pubblicato non ci sarebbero vantaggi, poiché stai abusando di StringBuilder. Costruisci la stessa stringa in entrambi i casi. Utilizzando StringBuilder è possibile evitare l' +operazione su String utilizzando il appendmetodo. Dovresti usarlo in questo modo:

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

In Java, il tipo String è una sequenza di caratteri immutabile, quindi quando si aggiungono due stringhe la VM crea un nuovo valore String con entrambi gli operandi concatenati.

StringBuilder fornisce una sequenza mutabile di caratteri, che puoi usare per concatenare diversi valori o variabili senza creare nuovi oggetti String, e quindi a volte può essere più efficiente che lavorare con le stringhe

Ciò fornisce alcune funzionalità utili, come la modifica del contenuto di una sequenza di caratteri passata come parametro all'interno di un altro metodo, cosa che non puoi fare con le stringhe.

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

Maggiori informazioni su http://docs.oracle.com/javase/tutorial/java/data/buffers.html


1
No, non dovresti. È meno leggibile rispetto all'utilizzo +, che verrà comunque convertito nello stesso codice. StringBuilderè utile quando non è possibile eseguire tutta la concatenazione in una singola espressione, ma non in questo caso.
Jon Skeet

1
Capisco che la stringa nella domanda è pubblicata come esempio. Non avrebbe senso costruire una stringa "fissa" come questa né con StringBuilder né aggiungendo frammenti diversi, poiché potresti semplicemente definirla in una singola costante "seleziona id1, id2 dalla tabella"
Tomas Narros

Ma anche se ci fossero valori non costanti dalle variabili, StringBuilderse dovessi usarne uno ne userebbe comunque uno singolo return "select id1, " + foo + "something else" + bar;, quindi perché non farlo? La domanda non fornisce alcuna indicazione che qualcosa debba passare StringBuilder.
Jon Skeet

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.