Cách đúng để sử dụng StringBuilder trong SQL


88

Tôi vừa tìm thấy một số bản dựng truy vấn sql như thế này trong dự án của mình:

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

Điều này có StringBuilderđạt được mục đích của nó, tức là giảm sử dụng bộ nhớ?

Tôi nghi ngờ điều đó, bởi vì trong hàm tạo, dấu '+' (toán tử kết hợp chuỗi) được sử dụng. Điều đó sẽ chiếm cùng một lượng bộ nhớ khi sử dụng Chuỗi như đoạn mã dưới đây? s Tôi hiểu, nó khác nhau khi sử dụng StringBuilder.append().

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

Cả hai câu lệnh có bằng nhau trong việc sử dụng bộ nhớ hay không? Vui lòng làm rõ.

Cảm ơn trước!

Biên tập:

BTW, nó không phải là mã của tôi . Tìm thấy nó trong một dự án cũ. Ngoài ra, truy vấn không quá nhỏ như trong ví dụ của tôi. :)


1
SQL AN NINH: luôn sử dụng PreparedStatementhoặc một cái gì đó tương tự: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe Roussy

Ngoài các điều sử dụng bộ nhớ, tại sao không sử dụng một thư viện xây dựng SQL thay vì: stackoverflow.com/q/370818/521799
Lukas Eder

Câu trả lời:


182

Mục đích của việc sử dụng StringBuilder, tức là giảm bộ nhớ. Nó có đạt được không?

Không hoàn toàn không. Mã đó không được sử dụng StringBuilderđúng cách. (Tuy nhiên, tôi nghĩ bạn đã trích dẫn sai; chắc chắn không có trích dẫn nào xung quanh id2table?)

Lưu ý rằng mục đích (thông thường) là giảm sự xáo trộn bộ nhớ thay vì tổng bộ nhớ được sử dụng, để làm cho bộ thu gom rác dễ dàng hơn một chút.

Điều đó sẽ chiếm bộ nhớ bằng cách sử dụng Chuỗi như dưới đây?

Không, nó sẽ gây ra nhiều xáo trộn bộ nhớ hơn là chỉ một câu kết bạn đã trích dẫn. (Cho đến khi / trừ khi trình tối ưu hóa JVM thấy rằng nội dung rõ ràng StringBuildertrong mã là không cần thiết và tối ưu hóa nó, nếu có thể.)

Nếu tác giả của mã đó muốn sử dụng StringBuilder(có những lập luận cho, nhưng cũng chống lại; xem lưu ý ở cuối câu trả lời này), tốt hơn nên thực hiện đúng cách (ở đây tôi giả sử rằng không có dấu ngoặc kép thực sự xung quanh id2table):

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

Lưu ý rằng tôi đã liệt kê some_appropriate_sizetrong hàm StringBuildertạo, để nó bắt đầu với đủ dung lượng cho toàn bộ nội dung mà chúng ta sẽ thêm vào. Kích thước mặc định được sử dụng nếu bạn không chỉ định là 16 ký tự , thường quá nhỏ và dẫn StringBuilderđến việc phải phân bổ lại để làm cho nó lớn hơn (IIRC, trong Sun / Oracle JDK, nó tự tăng gấp đôi [hoặc hơn, nếu nó biết nó cần nhiều hơn để đáp ứng một append] cụ thể mỗi khi nó hết chỗ).

Bạn có thể đã nghe nói rằng nối chuỗi sẽ sử dụng dấu StringBuilderdưới nếu được biên dịch bằng trình biên dịch Sun / Oracle. Điều này đúng, nó sẽ sử dụng một StringBuildercho biểu thức tổng thể. Nhưng nó sẽ sử dụng hàm tạo mặc định, có nghĩa là trong phần lớn các trường hợp, nó sẽ phải thực hiện phân bổ lại. Tuy nhiên, nó dễ đọc hơn. Lưu ý rằng điều này không đúng với một loạt các phép nối. Vì vậy, ví dụ, điều này sử dụng một StringBuilder:

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

Nó tạm dịch là:

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

Vì vậy, không sao cả, mặc dù hàm tạo mặc định và (các) phân bổ lại tiếp theo không lý tưởng, nhưng tỷ lệ cược là nó đủ tốt - và nối dễ đọc hơn rất nhiều.

Nhưng đó chỉ cho một biểu thức duy nhất. Nhiều StringBuilders được sử dụng cho việc này:

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

Điều đó cuối cùng trở thành một cái gì đó như thế này:

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;

... khá là xấu xí.

Tuy nhiên, điều quan trọng cần nhớ là trong tất cả trừ một số rất ít trường hợp, điều đó không quan trọng và việc đi kèm với khả năng đọc (giúp tăng cường khả năng bảo trì) được ưu tiên hơn nếu không có vấn đề về hiệu suất cụ thể.


Đúng vậy, tốt hơn. Việc sử dụng hàm tạo không tham số hơi đáng tiếc, nhưng không đáng kể. Tôi vẫn sẽ sử dụng một x + y + zbiểu thức duy nhất hơn là StringBuildertrừ khi tôi có lý do chính đáng để nghi ngờ đó là một vấn đề quan trọng.
Jon Skeet

@Crowder có một nghi ngờ nữa. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... Các sql.appenddòng tương tự là khoảng 60 dòng. Điều này có ổn không?
Vaandu

1
@Vanathi: ("Câu hỏi", không phải "nghi ngờ" - đó là một cách dịch sai phổ biến.) Nó ổn nhưng có thể sẽ dẫn đến nhiều lần phân bổ lại, vì StringBuilderban đầu sẽ được cấp đủ chỗ cho chuỗi bạn đã chuyển hàm tạo cộng với 16 ký tự. Vì vậy, nếu bạn thêm nhiều hơn 16 ký tự (tôi dám chắc là bạn đang có, nếu có 60 ký tự phụ!), Bạn StringBuildersẽ phải phân bổ lại ít nhất một lần và có thể nhiều lần. Nếu bạn có ý tưởng hợp lý về kết quả cuối cùng sẽ lớn như thế nào (giả sử, 400 ký tự), thì tốt nhất bạn nên làm sql = new StringBuilder(400);(hoặc bất cứ điều gì) sau đó thực hiện theo appends.
TJ Crowder

@Vanathi: Rất vui vì điều đó đã giúp ích. Vâng, nếu nó sẽ là 6.000 ký tự, nói StringBuildertrước rằng sẽ tiết kiệm được khoảng tám lần tái phân bổ bộ nhớ (giả sử chuỗi ban đầu là khoảng 10 ký tự, SB sẽ là 26 ký tự bắt đầu, sau đó nhân đôi lên 52, rồi 104, 208, 416, 832, 1664, 3328 và cuối cùng là 6656). Chỉ có ý nghĩa nếu điều này là một điểm nóng, nhưng vẫn còn, nếu bạn biết trước ... :-)
TJ Crowder

@TJ Crowder bạn muốn nói rằng tôi không được sử dụng toán tử "+" để có hiệu suất tốt hơn. đúng? vậy tại sao Oracal lại thêm toán tử "+" vào ngôn ngữ của họ, bạn có thể vui lòng giải thích thêm không? bất kỳ cách nào tôi ủng hộ câu trả lời của bạn.
Smit Patel

38

Khi bạn đã có tất cả các "mảnh" mà bạn muốn thêm vào, thì chẳng ích gì để sử dụng StringBuildercả. Sử dụng StringBuilder nối chuỗi trong cùng một lệnh gọi theo mã mẫu của bạn thậm chí còn tệ hơn.

Điều này sẽ tốt hơn:

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

Trong trường hợp này, việc nối chuỗi vẫn thực sự xảy ra tại thời điểm biên dịch , vì vậy nó tương đương với đơn giản hơn:

return "select id1, id2 from table";

Việc sử dụng new StringBuilder().append("select id1, ").append(" id2 ")....toString()sẽ thực sự cản trở hiệu suất trong trường hợp này, bởi vì nó buộc việc ghép nối được thực hiện tại thời điểm thực thi , thay vì tại thời điểm biên dịch . Giáo sư.

Nếu mã thực đang xây dựng một truy vấn SQL bằng cách bao gồm các giá trị trong truy vấn, thì đó là một vấn đề riêng biệt khác , đó là bạn nên sử dụng truy vấn được tham số hóa, chỉ định giá trị trong các tham số thay vì trong SQL.

Tôi có một bài báo về String/StringBuffer mà tôi đã viết một thời gian trước - trước khi StringBuilderxuất hiện. Tuy nhiên, các nguyên tắc được áp dụng StringBuildertheo cùng một cách.


10

[[Có một số câu trả lời hay ở đây nhưng tôi thấy rằng họ vẫn còn thiếu một chút thông tin. ]]

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

Vì vậy, như bạn đã chỉ ra, ví dụ bạn đưa ra là đơn giản nhưng chúng ta hãy phân tích nó. Điều xảy ra ở đây là trình biên dịch thực sự thực hiện +công việc ở đây vì "select id1, " + " id2 " + " from " + " table"tất cả đều là hằng số. Vì vậy, điều này biến thành:

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

Trong trường hợp này, rõ ràng, không có ích lợi gì khi sử dụng StringBuilder. Bạn cũng có thể làm:

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

Tuy nhiên, ngay cả khi bạn đang thêm bất kỳ trường nào hoặc các hằng số khác thì trình biên dịch sẽ sử dụng nội bộ StringBuilder - bạn không cần phải xác định một:

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

Dưới các trang bìa, mã này biến thành mã gần tương đương với:

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

Thực sự lần duy nhất bạn cần sử dụng StringBuilder trực tiếp là khi bạn có mã điều kiện. Ví dụ: mã trông giống như sau đây là tuyệt vọng cho StringBuilder:

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

Trong +dòng đầu tiên sử dụng một StringBuildertrường hợp. Sau đó, +=sử dụng một StringBuildertrường hợp khác . Sẽ hiệu quả hơn khi làm:

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

Một lần khác mà tôi sử dụng a StringBuilderlà khi tôi đang xây dựng một chuỗi từ một số lệnh gọi phương thức. Sau đó, tôi có thể tạo các phương thức có StringBuilderđối số:

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

Khi bạn đang sử dụng một StringBuilder, bạn nên xem bất kỳ cách sử dụng nào của +cùng một lúc:

sb.append("select " + fieldName);

Điều đó +sẽ gây ra một nội bộ khác StringBuilderđược tạo ra. Điều này tất nhiên phải là:

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

Cuối cùng, như @TJrowder đã chỉ ra, bạn nên luôn đoán kích thước của StringBuilder. Điều này sẽ tiết kiệm số lượng char[]đối tượng được tạo khi tăng kích thước của bộ đệm bên trong.


4

Bạn đoán đúng rằng mục đích của việc sử dụng trình tạo chuỗi không đạt được, ít nhất là không đạt được mức độ đầy đủ của nó.

Tuy nhiên, khi trình biên dịch nhìn thấy biểu thức, "select id1, " + " id2 " + " from " + " table"nó sẽ phát ra mã thực sự tạo ra StringBuilderhậu trường và gắn vào nó, vì vậy kết quả cuối cùng không phải là xấu.

Nhưng tất nhiên bất cứ ai nhìn vào mã đó đều có thể nghĩ rằng nó thuộc loại chậm phát triển.


2

Trong đoạn mã bạn đã đăng sẽ không có lợi thế nào, vì bạn đang sử dụng sai StringBuilder. Bạn xây dựng cùng một chuỗi trong cả hai trường hợp. Sử dụng StringBuilder, bạn có thể tránh +thao tác trên Strings bằng appendphương pháp này. Bạn nên sử dụng nó theo cách này:

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

Trong Java, kiểu Chuỗi là một chuỗi ký tự không thể thay đổi, vì vậy khi bạn thêm hai Chuỗi, máy ảo sẽ tạo ra một giá trị Chuỗi mới với cả hai toán hạng được ghép nối.

StringBuilder cung cấp một chuỗi ký tự có thể thay đổi, bạn có thể sử dụng để nối các giá trị hoặc biến khác nhau mà không cần tạo đối tượng String mới và vì vậy, đôi khi nó có thể hiệu quả hơn so với làm việc với chuỗi

Điều này cung cấp một số tính năng hữu ích, như thay đổi nội dung của một chuỗi ký tự được truyền dưới dạng tham số bên trong một phương thức khác, điều mà bạn không thể thực hiện với Chuỗi.

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

Thông tin thêm tại http://docs.oracle.com/javase/tutorial/java/data/buffers.html


1
Không, bạn không nên. Nó ít dễ đọc hơn là sử dụng +, dù sao thì mã này cũng sẽ được chuyển đổi thành cùng một mã. StringBuilderhữu ích khi bạn không thể thực hiện tất cả các phép nối trong một biểu thức duy nhất, nhưng không phải trong trường hợp này.
Jon Skeet

1
Tôi hiểu rằng chuỗi trong câu hỏi được đăng làm ví dụ. Nó sẽ làm cho không có ý nghĩa để xây dựng một chuỗi "cố định" như thế này không phải với StringBuilder cũng không thêm mảnh khác nhau, như bạn chỉ có thể định nghĩa nó trong một hằng số đơn "chọn id1, id2 từ bảng"
Tomas Narros

Nhưng ngay cả khi có các giá trị không phải là hằng số từ các biến, nó vẫn sẽ sử dụng một giá trị duy nhất StringBuildernếu bạn sử dụng return "select id1, " + foo + "something else" + bar;- vậy tại sao không làm như vậy? Câu hỏi không cung cấp dấu hiệu cho thấy bất cứ điều gì cần phải vượt qua StringBuilderxung quanh.
Jon Skeet

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.