Cara yang benar untuk menggunakan StringBuilder di SQL

88

Saya baru saja menemukan beberapa sql query build seperti ini di proyek saya:

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

Apakah ini StringBuildermencapai tujuannya, yaitu mengurangi penggunaan memori?

Saya meragukan itu, karena dalam konstruktor '+' (String concat operator) digunakan. Akankah itu mengambil jumlah memori yang sama dengan menggunakan String seperti kode di bawah ini? s Saya mengerti, itu berbeda saat menggunakan StringBuilder.append().

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

Apakah kedua pernyataan sama dalam penggunaan memori atau tidak? Mohon klarifikasi.

Terima kasih sebelumnya!

Edit:

BTW, itu bukan kode saya . Menemukannya di proyek lama. Juga, kueri tidak sekecil yang ada di contoh saya. :)

Vaandu
sumber
1
KEAMANAN SQL: selalu gunakan PreparedStatementatau yang serupa: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe
Terlepas dari masalah penggunaan memori, mengapa tidak menggunakan pustaka pembuat SQL sebagai gantinya: stackoverflow.com/q/370818/521799
Lukas Eder

Jawaban:

182

Tujuan menggunakan StringBuilder, yaitu mengurangi memori. Apakah itu tercapai?

Tidak, tidak sama sekali. Kode itu tidak digunakan StringBuilderdengan benar. (Saya pikir Anda salah mengutipnya; pasti tidak ada kutipan di sekitar id2dan table?)

Perhatikan bahwa tujuannya (biasanya) adalah untuk mengurangi churn memori daripada total memori yang digunakan, untuk membuat hidup sedikit lebih mudah pada pengumpul sampah.

Akankah itu mengambil memori yang sama dengan menggunakan String seperti di bawah ini?

Tidak, ini akan menyebabkan lebih banyak churn memori daripada hanya concat lurus yang Anda kutip. (Hingga / kecuali pengoptimal JVM melihat bahwa eksplisit StringBuilderdalam kode tidak diperlukan dan mengoptimalkannya, jika bisa.)

Jika penulis kode itu ingin menggunakan StringBuilder(ada argumen untuk, tetapi juga menentang; lihat catatan di akhir jawaban ini), lebih baik melakukannya dengan benar (di sini saya berasumsi sebenarnya tidak ada tanda kutip id2dan table):

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

Perhatikan bahwa saya telah mendaftar some_appropriate_sizedi StringBuilderkonstruktor, sehingga itu dimulai dengan kapasitas yang cukup untuk konten lengkap yang akan kita tambahkan. Ukuran default yang digunakan jika Anda tidak menentukannya adalah 16 karakter , yang biasanya terlalu kecil dan mengakibatkan StringBuilderharus melakukan realokasi untuk membuatnya lebih besar (IIRC, di Sun / Oracle JDK, ia menggandakan dirinya sendiri [atau lebih, jika ia tahu bahwa ia membutuhkan lebih banyak untuk memenuhi append] tertentu setiap kali kehabisan ruangan).

Anda mungkin pernah mendengar bahwa penggabungan string akan menggunakan a di StringBuilderbawah sampul jika dikompilasi dengan kompiler Sun / Oracle. Ini benar, ini akan menggunakan satu StringBuilderuntuk ekspresi keseluruhan. Tapi itu akan menggunakan konstruktor default, yang berarti di sebagian besar kasus, itu harus melakukan realokasi. Lebih mudah dibaca. Perhatikan bahwa ini tidak berlaku untuk rangkaian penggabungan. Jadi misalnya, ini menggunakan satu StringBuilder:

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

Secara kasar diterjemahkan menjadi:

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

Jadi tidak apa-apa, meskipun konstruktor default dan realokasi berikutnya tidak ideal, kemungkinannya cukup bagus - dan penggabungan jauh lebih mudah dibaca.

Tapi itu hanya untuk satu ekspresi. Beberapa StringBuilders digunakan untuk ini:

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

Itu akhirnya menjadi seperti ini:

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;

... yang sangat jelek.

Namun, penting untuk diingat bahwa dalam semua kecuali beberapa kasus tidak masalah dan menggunakan keterbacaan (yang meningkatkan kemudahan pemeliharaan) lebih disukai kecuali masalah kinerja tertentu.

TJ Crowder
sumber
Benar, itu lebih baik. Penggunaan konstruktor tanpa parameter sedikit disayangkan, tetapi tidak mungkin signifikan. Saya masih akan menggunakan satu x + y + zekspresi daripada StringBuilderkecuali saya memiliki alasan kuat untuk mencurigai bahwa itu akan menjadi masalah yang signifikan.
Jon Skeet
@Cder punya satu keraguan lagi. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... sql.appendGaris serupa ada sekitar 60 baris. Apa ini bagus?
Vaandu
1
@Vanathi: ("Pertanyaan", bukan "keraguan" - ini adalah kesalahan penerjemahan yang umum.) Tidak masalah tetapi mungkin akan menghasilkan beberapa realokasi, karena StringBuilderawalnya akan dialokasikan ruang yang cukup untuk string yang Anda berikan kepada konstruktor ditambah 16 karakter. Jadi, jika Anda menambahkan lebih dari 16 karakter (saya berani bilang begitu, jika ada 60 karakter tambahan!), Maka StringBuilderharus mengalokasikan ulang setidaknya sekali dan mungkin berkali-kali. Jika Anda memiliki gagasan yang masuk akal seberapa besar hasil akhirnya (katakanlah, 400 karakter), yang terbaik adalah melakukan sql = new StringBuilder(400);(atau apa pun) lalu lakukan appends.
TJ Crowder
@Vanathi: Senang bisa membantu. Ya, jika itu akan menjadi 6.000 karakter, memberitahu StringBuilderbahwa sebelumnya akan menghemat sekitar delapan realokasi memori (dengan asumsi string awal sekitar 10 karakter, SB akan menjadi 26 untuk memulai, kemudian digandakan menjadi 52, lalu 104, 208, 416, 832, 1664, 3328, dan terakhir 6656). Hanya penting jika ini adalah hotspot, tetapi tetap saja, jika Anda tahu sebelumnya ... :-)
TJ Crowder
@TJ Crowder maksud Anda saya tidak boleh menggunakan operator "+" untuk kinerja yang lebih baik. Baik? lalu mengapa Oracal menambahkan operator "+" dalam bahasa mereka, bisakah Anda jelaskan? dengan cara apa pun saya mendukung jawaban Anda.
Smit Patel
38

Ketika Anda sudah memiliki semua "bidak" yang ingin Anda tambahkan, tidak ada gunanya menggunakan StringBuildersama sekali. Penggunaan StringBuilder dan penggabungan string dalam panggilan yang sama sesuai kode sampel Anda bahkan lebih buruk.

Ini akan lebih baik:

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

Dalam kasus ini, penggabungan string sebenarnya terjadi pada waktu kompilasi , jadi ini setara dengan yang lebih sederhana:

return "select id1, id2 from table";

Penggunaan new StringBuilder().append("select id1, ").append(" id2 ")....toString()sebenarnya akan menghambat kinerja dalam kasus ini, karena memaksa penggabungan dilakukan pada waktu eksekusi , bukan pada waktu kompilasi . Ups.

Jika kode sebenarnya membangun kueri SQL dengan memasukkan nilai dalam kueri, maka itu adalah pemisah lainnya masalah , yaitu Anda harus menggunakan kueri berparameter, menentukan nilai dalam parameter, bukan di SQL.

Saya memiliki artikel tentang String/StringBuffer yang saya tulis beberapa waktu lalu - sebelum StringBuilderdatang. Prinsip-prinsipnya berlaku dengan StringBuildercara yang sama.

Jon Skeet
sumber
10

[[Ada beberapa jawaban yang bagus di sini tetapi saya menemukan bahwa jawaban-jawaban itu masih kurang sedikit informasi. ]]

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

Jadi seperti yang Anda tunjukkan, contoh yang Anda berikan adalah sederhana, tetapi mari kita analisis. Apa yang terjadi di sini adalah kompiler sebenarnya melakukan +pekerjaan di sini karena "select id1, " + " id2 " + " from " + " table"semuanya adalah konstanta. Jadi ini berubah menjadi:

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

Dalam hal ini, jelas tidak ada gunanya menggunakan StringBuilder. Anda mungkin juga melakukan:

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

Namun, bahkan jika Anda menambahkan bidang atau non-konstanta apa pun, kompilator akan menggunakan internal StringBuilder - Anda tidak perlu mendefinisikannya:

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

Di bawah sampulnya, ini berubah menjadi kode yang kira-kira setara dengan:

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

Sungguh satu-satunya saat Anda perlu menggunakan StringBuilder secara langsung adalah ketika Anda memiliki kode bersyarat. Misalnya, kode yang terlihat seperti berikut ini sangat membutuhkan StringBuilder:

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

Di +baris pertama menggunakan satu StringBuildercontoh. Kemudian +=menggunakan StringBuildercontoh lain . Lebih efisien untuk melakukan:

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

Waktu lain yang saya gunakan StringBuilderadalah ketika saya membuat string dari sejumlah panggilan metode. Kemudian saya bisa membuat metode yang membutuhkan StringBuilderargumen:

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

Saat Anda menggunakan StringBuilder, Anda harus memperhatikan setiap penggunaan +pada saat yang sama:

sb.append("select " + fieldName);

Itu +akan menyebabkan internal lain StringBuilderdibuat. Ini tentu saja harus:

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

Terakhir, seperti yang ditunjukkan @TJrowder, Anda harus selalu menebak ukuran file StringBuilder. Ini akan menghemat jumlah char[]objek yang dibuat saat memperbesar ukuran buffer internal.

Abu-abu
sumber
4

Anda benar dalam menebak bahwa tujuan penggunaan pembuat string tidak tercapai, setidaknya tidak sepenuhnya.

Namun, ketika compiler melihat ekspresi "select id1, " + " id2 " + " from " + " table"itu memancarkan kode yang sebenarnya membuat di StringBuilderbelakang layar dan menambahkannya, jadi hasil akhirnya tidak terlalu buruk.

Tapi tentu saja siapa pun yang melihat kode itu pasti akan berpikir bahwa itu agak terbelakang.

Mike Nakis
sumber
2

Dalam kode yang Anda posting tidak akan ada keuntungan, karena Anda menyalahgunakan StringBuilder. Anda membangun String yang sama di kedua kasus. Menggunakan StringBuilder Anda dapat menghindari +operasi pada Strings menggunakan appendmetode ini. Anda harus menggunakannya dengan cara ini:

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

Di Java, tipe String adalah urutan karakter yang tidak dapat diubah, jadi ketika Anda menambahkan dua String, VM membuat nilai String baru dengan kedua operan digabungkan.

StringBuilder menyediakan urutan karakter yang bisa berubah, yang dapat Anda gunakan untuk menggabungkan nilai atau variabel yang berbeda tanpa membuat objek String baru, dan karenanya terkadang bisa lebih efisien daripada bekerja dengan string

Ini menyediakan beberapa fitur yang berguna, seperti mengubah konten urutan karakter yang diteruskan sebagai parameter di dalam metode lain, yang tidak dapat Anda lakukan dengan 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);
}

Info lebih lanjut di http://docs.oracle.com/javase/tutorial/java/data/buffers.html

Tomas Narros
sumber
1
Tidak, sebaiknya tidak. Ini kurang terbaca daripada menggunakan +, yang akan diubah menjadi kode yang sama. StringBuilderberguna saat Anda tidak dapat melakukan semua penggabungan dalam satu ekspresi, tetapi tidak dalam kasus ini.
Jon Skeet
1
Saya memahami bahwa string dalam pertanyaan diposting sebagai contoh. Tidak masuk akal untuk membangun string "tetap" seperti ini baik dengan StringBuilder maupun menambahkan fragmen yang berbeda, karena Anda dapat mendefinisikannya dalam satu konstanta "pilih id1, id2 dari tabel"
Tomas Narros
Tetapi bahkan jika ada nilai non-konstan dari variabel, itu masih akan menggunakan nilai tunggal StringBuilderjika Anda menggunakannya return "select id1, " + foo + "something else" + bar;- jadi mengapa tidak melakukannya? Pertanyaan tersebut tidak memberikan indikasi bahwa ada sesuatu yang perlu diedarkan StringBuilder.
Jon Skeet
1

Anda juga bisa menggunakan MessageFormat terlalu

JGFMK
sumber