Prawidłowy sposób używania StringBuilder w SQL

88

Właśnie znalazłem w moim projekcie taką kompilację kwerend sql:

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

Czy to StringBuilderosiąga swój cel, tj. Zmniejsza zużycie pamięci?

Wątpię, ponieważ w konstruktorze zastosowano „+” (operator konkatowania ciągów). Czy zajmie to taką samą ilość pamięci, jak użycie String, jak w kodzie poniżej? Zrozumiałem, różni się przy używaniu StringBuilder.append().

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

Czy obie instrukcje są równe pod względem użycia pamięci, czy nie? Proszę o wyjaśnienie.

Z góry dziękuję!

Edytować:

BTW, to nie jest mój kod . Znalazłem to w starym projekcie. Ponadto zapytanie nie jest tak małe, jak w moim przykładzie. :)

Vaandu
źródło
1
ZABEZPIECZENIA SQL: zawsze używaj PreparedStatementlub czegoś podobnego: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe Roussy
Oprócz kwestii wykorzystania pamięci, dlaczego nie użyć zamiast tego biblioteki SQL Builder: stackoverflow.com/q/370818/521799
Lukas Eder

Odpowiedzi:

182

Cel wykorzystania StringBuilder, czyli redukcja pamięci. Czy to się udało?

Nie, wcale. Ten kod nie jest używany StringBuilderpoprawnie. (Myślę jednak, że źle to zacytowałeś; na pewno nie ma wokół cudzysłowu id2i table?)

Zauważ, że celem (zwykle) jest zmniejszenie utraty pamięci, a nie całkowitej ilości używanej pamięci, aby nieco ułatwić życie odśmiecaczowi pamięci.

Czy zajmie to pamięć równą użyciu String, jak poniżej?

Nie, spowoduje to więcej zmian w pamięci niż tylko proste konkatowanie, które zacytowałeś. (Dopóki / chyba że optymalizator JVM zobaczy, że jawny plikStringBuilder w kodzie są niepotrzebne i zoptymalizuje je, jeśli to możliwe).

Jeśli autor tego kodu chce użyć StringBuilder(są argumenty za, ale także przeciw; patrz uwaga na końcu tej odpowiedzi), lepiej zrobić to poprawnie (tutaj zakładam, że w rzeczywistości nie ma cudzysłowów id2i table):

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

Zauważ, że wymieniłem some_appropriate_sizew StringBuilderkonstruktorze, więc zaczyna się z wystarczającą pojemnością dla pełnej zawartości, którą zamierzamy dołączyć. Domyślny rozmiar używany, jeśli nie podasz żadnego, to 16 znaków , co jest zwykle zbyt małe i powoduje StringBuilderkonieczność dokonywania ponownych przydziałów, aby się powiększyć (IIRC, w Sun / Oracle JDK, podwaja się [lub więcej, jeśli wie, że potrzebuje więcej, aby zaspokoić określone potrzeby append] za każdym razem, gdy zabraknie mu miejsca).

Być może słyszeliście, że ciąg konkatenacji będzie używać StringBuilderpod kołdrą jeśli skompilowany z kompilatora Sun / Oracle. To prawda, użyje jednego StringBuilderdla ogólnego wyrażenia. Ale użyje domyślnego konstruktora, co oznacza, że ​​w większości przypadków będzie musiał dokonać ponownej alokacji. Ale jest łatwiejszy do odczytania. Zauważ, że nie dotyczy to serii konkatenacji. Na przykład ten używa jednego StringBuilder:

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

To z grubsza przekłada się na:

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

Więc to jest w porządku, chociaż domyślny konstruktor i późniejsza realokacja nie są idealne, szanse są wystarczająco dobre - a konkatenacja jest o wiele bardziej czytelna.

Ale to tylko dla jednego wyrażenia. W StringBuildertym celu używa się wielu s:

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

To kończy się czymś takim:

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;

... co jest dość brzydkie.

Należy jednak pamiętać, że we wszystkich, z wyjątkiem nielicznych przypadków , nie ma to znaczenia i preferowana jest czytelność (która zwiększa łatwość konserwacji), poza konkretnym problemem z wydajnością.

TJ Crowder
źródło
Tak, tak lepiej. Użycie konstruktora bez parametrów jest nieco niefortunne, ale prawdopodobnie nie będzie miało znaczenia. Nadal używałbym jednego x + y + zwyrażenia, StringBuilderchyba że miałbym dobry powód, by podejrzewać, że będzie to poważny problem.
Jon Skeet
@Crowder ma jeszcze jedną wątpliwość. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... Podobne sql.appendlinie mają około 60 linii. Czy to w porządku?
Vaandu
1
@Vanathi: ("Pytanie", nie "wątpienie" - to częsty błąd w tłumaczeniu.) Jest w porządku, ale prawdopodobnie spowoduje wielokrotne ponowne przydzielenie, ponieważ StringBuilderpoczątkowo zostanie przydzielone wystarczająco dużo miejsca na napis, który przekazałeś konstruktorowi, plus 16 znaków. Więc jeśli dodasz więcej niż 16 znaków (śmiem twierdzić, że tak jest, jeśli jest 60 dopisków!), StringBuilderBędziesz musiał ponownie przydzielić co najmniej raz, a być może wiele razy. Jeśli masz rozsądny pomysł, jak duży będzie wynik końcowy (powiedzmy 400 znaków), najlepiej zrób sql = new StringBuilder(400);(lub cokolwiek), a następnie wykonaj czynności append.
TJ Crowder
@Vanathi: Cieszę się, że pomogło. Tak, jeśli ma mieć 6000 znaków, informując, StringBuilderże z góry zaoszczędzi około ośmiu realokacji pamięci (zakładając, że początkowy ciąg miał około 10 znaków, SB na początku miałby 26, a następnie podwoił się do 52, a następnie 104, 208, 416, 832, 1664, 3328 i wreszcie 6656). Istotne tylko, jeśli jest to hotspot, ale nadal, jeśli wiesz z góry ... :-)
TJ Crowder
@TJ Crowder chcesz powiedzieć, że nie wolno mi używać operatora „+” dla lepszej wydajności. dobrze? to dlaczego Oracal dodał operator „+” w swoim języku, czy możesz to rozwinąć? w jakikolwiek sposób moje głosy za Twoją odpowiedzią.
Smit Patel
38

Kiedy masz już wszystkie „elementy”, które chcesz dołączyć, nie ma sensu ich używać StringBuilder. Używanie StringBuilder i konkatenacja ciągów znaków w tym samym wywołaniu, co w przykładowym kodzie, jest jeszcze gorsze.

Tak byłoby lepiej:

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

W tym przypadku konkatenacja ciągów i tak ma miejsce w czasie kompilacji , więc jest to odpowiednik jeszcze prostszego:

return "select id1, id2 from table";

Użycie w new StringBuilder().append("select id1, ").append(" id2 ")....toString()rzeczywistości utrudni wydajność w tym przypadku, ponieważ wymusza konkatenację w czasie wykonywania , a nie w czasie kompilacji . Ups.

Jeśli rzeczywisty kod tworzy zapytanie SQL poprzez uwzględnienie wartości w zapytaniu, to jest to kolejna osobna kwestia kwestia, która polega na tym, że należy używać zapytań sparametryzowanych, określając wartości w parametrach, a nie w języku SQL.

Mam artykuł na String/StringBuffer który napisałem jakiś czas temu - zanim się StringBuilderpojawił. Zasady mają jednak zastosowanie StringBuilderw ten sam sposób.

Jon Skeet
źródło
10

[[Jest tu kilka dobrych odpowiedzi, ale wydaje mi się, że nadal brakuje im trochę informacji. ]]

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

Jak więc zauważyłeś, podany przykład jest uproszczony, ale i tak go przeanalizujmy. To, co dzieje się tutaj, to fakt, że kompilator faktycznie +działa tutaj, ponieważ "select id1, " + " id2 " + " from " + " table"wszystkie są stałymi. Więc to zamienia się w:

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

W tym przypadku oczywiście nie ma sensu używać StringBuilder. Równie dobrze możesz:

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

Jednak nawet jeśli dołączałeś jakiekolwiek pola lub inne zmienne nie będące stałymi, kompilator użyłby wewnętrznego StringBuilder - nie ma potrzeby, abyś go definiował:

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

Pod okładkami zamienia się to w kod, który jest w przybliżeniu równoważny z:

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

Naprawdę jedyny czas, którego potrzebujesz do StringBuilder bezpośredniego użycia, to kod warunkowy. Na przykład kod, który wygląda jak poniższy, desperacko szuka StringBuilder:

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

W +pierwszej linii używa jednej StringBuilderinstancji. Następnie +=używa innej StringBuilderinstancji. Bardziej efektywne jest:

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

Innym razem używam a, StringBuilderkiedy buduję ciąg z wielu wywołań metod. Następnie mogę stworzyć metody, które przyjmują StringBuilderargument:

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

Kiedy używasz StringBuilder, powinieneś uważać na jakiekolwiek użycie +w tym samym czasie:

sb.append("select " + fieldName);

Że +spowoduje kolejny wewnętrzny StringBuilderma zostać utworzony. Powinno to oczywiście być:

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

Na koniec, jak wskazuje @TJrowder, zawsze powinieneś zgadywać rozmiar StringBuilder. Pozwoli to zaoszczędzić na liczbie char[]obiektów utworzonych podczas zwiększania rozmiaru bufora wewnętrznego.

Szary
źródło
4

Masz rację, domyślając się, że cel używania konstruktora stringów nie został osiągnięty, przynajmniej nie w pełnym zakresie.

Jednak gdy kompilator widzi wyrażenie "select id1, " + " id2 " + " from " + " table", emituje kod, który faktycznie tworzy StringBuilderza kulisami i dołącza do niego, więc ostateczny wynik nie jest taki zły.

Ale oczywiście każdy, kto patrzy na ten kod, z pewnością pomyśli, że jest trochę opóźniony.

Mike Nakis
źródło
2

W opublikowanym kodzie nie byłoby żadnych korzyści, ponieważ niewłaściwie używasz StringBuilder. W obu przypadkach budujesz ten sam ciąg. Używając StringBuilder, możesz uniknąć +operacji na Strings przy użyciu appendmetody. Powinieneś go używać w ten sposób:

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

W Javie typ String to niezmienna sekwencja znaków, więc po dodaniu dwóch ciągów maszyna wirtualna tworzy nową wartość String z połączonymi obydwoma operandami.

StringBuilder zapewnia zmienną sekwencję znaków, której można użyć do łączenia różnych wartości lub zmiennych bez tworzenia nowych obiektów String, dzięki czemu czasami może być bardziej wydajna niż praca z ciągami znaków

Zapewnia to kilka przydatnych funkcji, takich jak zmiana zawartości sekwencji znaków przekazanych jako parametr wewnątrz innej metody, czego nie można zrobić w przypadku 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);
}

Więcej informacji na http://docs.oracle.com/javase/tutorial/java/data/buffers.html

Tomas Narros
źródło
1
Nie, nie powinieneś. Jest mniej czytelny niż użycie +, który i tak zostanie przekonwertowany na ten sam kod. StringBuilderjest przydatne, gdy nie można wykonać całej konkatenacji w jednym wyrażeniu, ale nie w tym przypadku.
Jon Skeet
1
Rozumiem, że ciąg znaków w pytaniu jest zamieszczony jako przykład. Nie miałoby sensu budowanie „ustalonego” łańcucha, takiego jak ten, ani za pomocą StringBuilder, ani dodawania różnych fragmentów, ponieważ można by po prostu zdefiniować go w jednej stałej „select id1, id2 from table”
Tomas Narros,
Ale nawet gdyby istniały zmienne nie będące stałymi wartościami, nadal używałby pojedynczego StringBuilder, gdybyś użył return "select id1, " + foo + "something else" + bar;- więc dlaczego tego nie zrobić? Pytanie nie wskazuje na to, że cokolwiek musi zostać omówione StringBuilder.
Jon Skeet
1

Można również użyć MessageFormat zbyt

JGFMK
źródło