วิธีที่ถูกต้องในการใช้ StringBuilder ใน SQL


88

ฉันเพิ่งพบแบบสอบถาม sql บางตัวสร้างแบบนี้ในโครงการของฉัน:

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

นี้ไม่StringBuilderบรรลุเป้าหมายของมันคือการลดการใช้หน่วยความจำ?

ฉันสงสัยว่าเพราะในคอนสตรัคเตอร์จะใช้ '+' (ตัวดำเนินการ String concat) จะใช้หน่วยความจำเท่ากันกับการใช้ String เหมือนโค้ดด้านล่างหรือไม่? s StringBuilder.append()ผมเข้าใจมันแตกต่างเมื่อใช้

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

ทั้งสองงบเท่ากันในการใช้หน่วยความจำหรือไม่? กรุณาชี้แจง.

ขอบคุณล่วงหน้า!

แก้ไข:

BTW, มันไม่ได้เป็นรหัสของฉัน พบในโครงการเก่า นอกจากนี้แบบสอบถามยังไม่เล็กเท่าในตัวอย่างของฉัน :)


1
ความปลอดภัยของ SQL: ใช้เสมอPreparedStatementหรือสิ่งที่คล้ายกัน: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe Roussy

นอกเหนือจากสิ่งที่ใช้หน่วยความจำทำไมไม่ใช้ไลบรารีตัวสร้าง SQL แทน: stackoverflow.com/q/370818/521799
Lukas Eder

คำตอบ:


182

จุดมุ่งหมายของการใช้ StringBuilder คือการลดหน่วยความจำ บรรลุหรือไม่?

ไม่เลย. รหัสนั้นใช้ไม่StringBuilderถูกต้อง (ฉันคิดว่าคุณอ้างผิด แต่แน่นอนว่าไม่มีคำพูดรอบ ๆid2และtable?)

โปรดทราบว่าจุดมุ่งหมาย (โดยปกติ) คือการลดการปั่นหน่วยความจำแทนที่จะใช้หน่วยความจำทั้งหมดเพื่อให้ชีวิตง่ายขึ้นในตัวเก็บขยะ

จะใช้หน่วยความจำเท่ากับการใช้ String เหมือนด้านล่างหรือไม่?

ไม่มีก็จะก่อให้เกิดมากขึ้นปั่นหน่วยความจำมากกว่าเพียงแค่ concat ตรงที่คุณยกมา (จนกว่า / เว้นแต่ว่าเครื่องมือเพิ่มประสิทธิภาพ JVM จะเห็นว่าสิ่งที่ชัดเจนStringBuilderในโค้ดนั้นไม่จำเป็นและปรับให้เหมาะสมหากทำได้)

หากผู้เขียนรหัสนั้นต้องการใช้StringBuilder(มีข้อโต้แย้ง แต่ยังต่อต้านดูหมายเหตุท้ายคำตอบนี้) ควรทำอย่างถูกต้องดีกว่า (ที่นี่ฉันสมมติว่าไม่มีเครื่องหมายคำพูดid2และtable):

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

โปรดทราบว่าฉันได้ระบุไว้some_appropriate_sizeในตัวStringBuilderสร้างเพื่อให้เริ่มต้นด้วยความจุเพียงพอสำหรับเนื้อหาทั้งหมดที่เราจะต่อท้าย ขนาดเริ่มต้นที่ใช้หากคุณไม่ได้ระบุไว้คือ16 อักขระซึ่งโดยปกติจะเล็กเกินไปและส่งผลให้StringBuilderต้องทำการจัดสรรใหม่เพื่อทำให้ตัวมันใหญ่ขึ้น (IIRC ใน Sun / Oracle JDK จะเพิ่มเป็นสองเท่า [หรือมากกว่านั้นถ้า มันรู้ว่ามันต้องการมากกว่านั้นเพื่อตอบสนองความเฉพาะเจาะจงappend] ทุกครั้งที่ห้องนั้นหมด)

คุณอาจเคยได้ยินว่าการต่อสายอักขระจะใช้StringBuilderภายใต้การครอบคลุมหากคอมไพล์ด้วยคอมไพเลอร์ Sun / Oracle นี่เป็นความจริงมันจะใช้หนึ่งStringBuilderสำหรับนิพจน์โดยรวม แต่จะใช้ตัวสร้างเริ่มต้นซึ่งหมายความว่าในกรณีส่วนใหญ่จะต้องทำการจัดสรรใหม่ แม้ว่าจะอ่านง่ายกว่า โปรดทราบว่านี้ไม่ได้ที่แท้จริงของชุดของ concatenations ตัวอย่างเช่นสิ่งนี้ใช้StringBuilder:

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

แปลได้คร่าวๆว่า:

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

ดังนั้นก็ไม่เป็นไรแม้จะสร้างการเริ่มต้นและการจัดสรรที่ตามมา (s) ไม่เหมาะราคาถูกก็พอที่ดี - และเรียงต่อกันเป็นจำนวนมากอ่านได้มากขึ้น

แต่นั่นเป็นเพียงนิพจน์เดียว ใช้หลายรายการStringBuilderสำหรับสิ่งนี้:

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

ที่กลายเป็นสิ่งนี้:

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;

... ซึ่งค่อนข้างน่าเกลียด

อย่างไรก็ตามสิ่งสำคัญคือต้องจำไว้ว่าในทุกกรณี แต่มีไม่กี่กรณีที่ไม่สำคัญและการดำเนินการต่อด้วยความสามารถในการอ่าน (ซึ่งช่วยเพิ่มความสามารถในการบำรุงรักษา) เป็นที่ต้องการยกเว้นปัญหาด้านประสิทธิภาพที่เฉพาะเจาะจง


ถูกต้องดีกว่า การใช้ตัวสร้างแบบไม่ใช้พารามิเตอร์นั้นโชคร้ายเล็กน้อยแต่ไม่น่าจะมีนัยสำคัญ ฉันจะยังคงใช้x + y + zนิพจน์เดียวมากกว่าStringBuilderเว้นแต่ฉันจะมีเหตุผลที่ดีที่จะสงสัยว่ามันจะเป็นปัญหาสำคัญ
Jon Skeet

@Crowder มีอีกหนึ่งข้อสงสัย StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... sql.appendเส้นที่คล้ายกันคือประมาณ 60 เส้น นี่สบายดีไหม
Vaandu

1
@ วันฑี: ("คำถาม" ไม่ใช่ "ข้อสงสัย" - เป็นการแปลผิดทั่วไป) ใช้ได้ดี แต่อาจส่งผลให้มีการจัดสรรซ้ำหลายครั้งเนื่องจากในStringBuilderตอนแรกจะจัดสรรพื้นที่เพียงพอสำหรับสตริงที่คุณส่งผ่านตัวสร้างบวก 16 อักขระ ดังนั้นหากคุณต่อท้ายอักขระมากกว่า 16 ตัว (ฉันกล้าบอกว่าคุณเป็นถ้ามี 60 ต่อท้าย!) StringBuilderจะต้องจัดสรรใหม่อย่างน้อยหนึ่งครั้งและอาจหลายครั้ง หากคุณมีความคิดที่สมเหตุสมผลว่าผลลัพธ์สุดท้ายจะใหญ่แค่ไหน (พูด 400 อักขระ) ที่ดีที่สุดคือทำsql = new StringBuilder(400);(หรืออะไรก็ได้) จากนั้นทำappends
TJ Crowder

@ วันฑี: ดีใจที่ช่วย. ใช่ถ้ามันจะเป็น 6,000 ตัวอักษรการบอกStringBuilderว่าล่วงหน้าจะบันทึกการจัดสรรหน่วยความจำใหม่ได้ประมาณแปดตัว (สมมติว่าสตริงเริ่มต้นมีประมาณ 10 อักขระ SB จะเป็น 26 ในการเริ่มต้นจากนั้นเพิ่มสองเท่าเป็น 52 จากนั้น 104, 208 416, 832, 1664, 3328 และสุดท้ายคือ 6656) สำคัญเฉพาะในกรณีที่นี่เป็นฮอตสปอต แต่ถ้าคุณรู้ล่วงหน้า ... :-)
TJ Crowder

@TJ Crowder คุณหมายถึงว่าฉันต้องไม่ใช้ตัวดำเนินการ "+" เพื่อประสิทธิภาพที่ดีขึ้น ขวา? แล้วทำไม Oracal จึงเพิ่มโอเปอเรเตอร์ "+" ในภาษาของพวกเขาคุณช่วยอธิบายอย่างละเอียดได้อย่างไรฉันเพิ่มคะแนนให้กับคำตอบของคุณอย่างไร
Smit Patel

38

เมื่อคุณมี "ชิ้นส่วน" ทั้งหมดที่คุณต้องการต่อท้ายแล้วคุณจะไม่สามารถใช้StringBuilderงานได้เลย การใช้StringBuilder และการต่อสายอักขระในการเรียกเดียวกันตามรหัสตัวอย่างของคุณนั้นแย่ยิ่งกว่า

สิ่งนี้จะดีกว่า:

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

ในกรณีนี้การต่อสายอักขระจะเกิดขึ้นจริงในเวลาคอมไพล์อยู่ดีดังนั้นจึงเทียบเท่ากับสิ่งที่ง่ายกว่า:

return "select id1, id2 from table";

การใช้new StringBuilder().append("select id1, ").append(" id2 ")....toString()จะขัดขวางประสิทธิภาพในกรณีนี้จริง ๆเพราะมันบังคับให้ทำการเชื่อมต่อกันในเวลาดำเนินการแทนที่จะเป็นเวลาคอมไพล์ อ๊ะ.

หากรหัสจริงกำลังสร้างแบบสอบถาม SQL โดยรวมค่าในแบบสอบถามนั่นคืออีกปัญหาหนึ่งที่แยกต่างหากซึ่งก็คือคุณควรใช้แบบสอบถามที่กำหนดพารามิเตอร์โดยระบุค่าในพารามิเตอร์แทนที่จะเป็นใน SQL

ฉันมีบทความเกี่ยวกับString/StringBufferที่ฉันเขียนเมื่อสักครู่ก่อนหน้าStringBuilderนี้มาก่อน หลักการใช้StringBuilderในลักษณะเดียวกันแม้ว่า


10

[[มีคำตอบดีๆอยู่ที่นี่ แต่ฉันพบว่าพวกเขายังขาดข้อมูลอยู่เล็กน้อย ]]

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

ดังที่คุณชี้ให้เห็นตัวอย่างที่คุณยกมาเป็นเรื่องง่าย แต่ลองวิเคราะห์กันดู สิ่งที่เกิดขึ้นที่นี่คือคอมไพเลอร์+ทำงานที่นี่จริง ๆเพราะ"select id1, " + " id2 " + " from " + " table"เป็นค่าคงที่ทั้งหมด สิ่งนี้จึงกลายเป็น:

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

StringBuilderในกรณีนี้เห็นได้ชัดว่ามีจุดในการใช้ไม่มี คุณอาจทำ:

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

อย่างไรก็ตามแม้ว่าคุณจะต่อท้ายฟิลด์ใด ๆ หรือค่าคงที่อื่น ๆ คอมไพเลอร์ก็จะใช้ภายใน StringBuilder - คุณไม่จำเป็นต้องกำหนดฟิลด์ใดฟิลด์หนึ่ง:

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

ภายใต้ฝาปิดสิ่งนี้จะกลายเป็นรหัสที่เทียบเท่ากับ:

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

ครั้งเดียวที่คุณต้องใช้StringBuilder โดยตรงคือเมื่อคุณมีรหัสเงื่อนไข ตัวอย่างเช่นโค้ดที่มีลักษณะดังต่อไปนี้หมดหวังสำหรับ a StringBuilder:

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

+ในบรรทัดแรกจะใช้อย่างใดอย่างหนึ่งStringBuilderเช่น จากนั้น+=ใช้StringBuilderอินสแตนซ์อื่น มีประสิทธิภาพมากกว่าที่จะทำ:

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

อีกครั้งหนึ่งที่ฉันใช้ a StringBuilderคือเมื่อฉันสร้างสตริงจากการเรียกเมธอดจำนวนมาก จากนั้นฉันสามารถสร้างเมธอดที่ใช้StringBuilderโต้แย้ง:

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

เมื่อคุณใช้ a StringBuilderคุณควรดูการใช้งานใด ๆ+ในเวลาเดียวกัน:

sb.append("select " + fieldName);

นั่น+จะทำให้เกิดการStringBuilderสร้างภายในขึ้นมาใหม่ สิ่งนี้ควรเป็น:

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

สุดท้ายดังที่ @TJrowder ชี้ให้เห็นคุณควรคาดเดาขนาดของไฟล์StringBuilder. ซึ่งจะช่วยประหยัดจำนวนchar[]วัตถุที่สร้างขึ้นเมื่อมีการขยายขนาดของบัฟเฟอร์ภายใน


4

คุณเดาถูกว่าไม่บรรลุจุดมุ่งหมายของการใช้ตัวสร้างสตริงอย่างน้อยก็ไม่ถึงขอบเขตทั้งหมด

อย่างไรก็ตามเมื่อคอมไพเลอร์เห็นนิพจน์"select id1, " + " id2 " + " from " + " table"มันจะปล่อยโค้ดซึ่งสร้างStringBuilderเบื้องหลังและผนวกเข้าด้วยกันดังนั้นผลลัพธ์สุดท้ายก็ไม่ได้แย่ขนาดนั้น

แต่แน่นอนว่าใครก็ตามที่มองรหัสนั้นจะต้องคิดว่ามันเป็นเรื่องปัญญาอ่อน


2

ในโค้ดที่คุณโพสต์จะไม่มีข้อดีเนื่องจากคุณใช้ StringBuilder ในทางที่ผิด คุณสร้างสตริงเดียวกันในทั้งสองกรณี การใช้ StringBuilder คุณสามารถหลีกเลี่ยงการ+ดำเนินการกับ Strings โดยใช้appendวิธีการ คุณควรใช้วิธีนี้:

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

ใน Java ประเภท String เป็นลำดับของอักขระที่ไม่สามารถเปลี่ยนได้ดังนั้นเมื่อคุณเพิ่มสตริงสองสตริง VM จะสร้างค่าสตริงใหม่โดยมีตัวถูกดำเนินการทั้งสองเชื่อมต่อกัน

StringBuilder มีลำดับของอักขระที่ไม่แน่นอนซึ่งคุณสามารถใช้เพื่อต่อค่าหรือตัวแปรต่างๆโดยไม่ต้องสร้างอ็อบเจกต์ String ใหม่ดังนั้นบางครั้งจึงมีประสิทธิภาพมากกว่าการทำงานกับสตริง

สิ่งนี้มีคุณสมบัติที่มีประโยชน์บางอย่างเนื่องจากการเปลี่ยนเนื้อหาของลำดับถ่านที่ส่งผ่านเป็นพารามิเตอร์ภายในวิธีอื่นซึ่งคุณไม่สามารถทำได้กับ 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);
}

ข้อมูลเพิ่มเติมที่http://docs.oracle.com/javase/tutorial/java/data/buffers.html


1
ไม่คุณไม่ควร อ่านได้น้อยกว่าการใช้+งานซึ่งจะถูกแปลงเป็นรหัสเดียวกันอยู่ดี StringBuilderมีประโยชน์เมื่อคุณไม่สามารถทำการเชื่อมต่อทั้งหมดในนิพจน์เดียว แต่ไม่ใช่ในกรณีนี้
Jon Skeet

1
ฉันเข้าใจว่าสตริงในคำถามถูกโพสต์ไว้เป็นตัวอย่าง มันจะไม่มีเหตุผลที่จะสร้างสตริง "คงที่" เช่นนี้ไม่ว่าจะด้วย StringBuilder หรือเพิ่มส่วนที่แตกต่างกันเนื่องจากคุณสามารถกำหนดเป็นค่าคงที่เดียว "เลือก id1, id2 จากตาราง"
Tomas Narros

แต่แม้ว่าจะมีค่าที่ไม่ใช่ค่าคงที่จากตัวแปร แต่ก็ยังคงใช้ค่าเดียวStringBuilderถ้าคุณจะใช้return "select id1, " + foo + "something else" + bar;- ทำไมไม่ทำเช่นนั้น? คำถามไม่ได้ระบุว่ามีสิ่งใดที่ต้องผ่านไปStringBuilderรอบ ๆ
Jon Skeet

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.