Правильний спосіб використання StringBuilder в SQL


88

Я щойно знайшов у своєму проекті таку збірку запитів sql:

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

Чи StringBuilderдосягається це своєю метою, тобто зменшенням використання пам'яті?

Я сумніваюся в цьому, оскільки в конструкторі використовується '+' (оператор concat String). Це займе таку ж пам’ять, як використання рядка, як код нижче? s Я зрозумів, це відрізняється при використанні StringBuilder.append().

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

Чи рівні обидва твердження за використанням пам'яті чи ні? Будь ласка, поясніть.

Спасибі заздалегідь!

Редагувати:

До речі, це не мій код . Знайшов у старому проекті. Крім того, запит не такий маленький, як у моєму прикладі. :)


1
БЕЗПЕКА SQL: завжди використовувати PreparedStatementабо щось подібне: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Крістоф Руссі

Окрім того, що стосується використання пам'яті, чому б не використовувати замість цього бібліотеку конструктора
Лукас Едер,

Відповіді:


182

Мета використання StringBuilder, тобто зменшення пам'яті. Чи досягнуто це?

Ні, зовсім ні. Цей код використовується StringBuilderнеправильно. (Мені здається, ви це неправильно вказали; звичайно там немає цитат навколо id2таtable ?)

Зверніть увагу, що метою (як правило) є зменшення відтоку пам’яті, а не загальної використаної пам’яті, щоб полегшити життя збирача сміття.

Чи буде пам'ять рівною використанню рядка, як показано нижче?

Ні, це спричинить більший обсяг пам’яті, ніж просто прямий конкат, який ви цитували. (Поки / якщо 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для загального виразу. Але він буде використовувати конструктор за замовчуванням, що означає, що в більшості випадків йому доведеться виконати перерозподіл. Однак читати його легше. Зверніть увагу, що це не відповідає серії об'єднань. Так, наприклад, для цього використовується один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();

Отже, це нормально, хоча конструктор за замовчуванням та подальший перерозподіл (и) не є ідеальними, шанси на те, що він досить хороший - і конкатенація багато читабельніша.

Але це лише для одного виразу. StringBuilderДля цього використовуються декілька s:

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якщо не мав поважних причин підозрювати, що це буде важливою проблемою.
Джон Скіт,

@Crowder має ще один сумнів. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... Подібні sql.appendрядки складають близько 60 рядків. Це добре?
Ваанду,

1
@Vanathi: ("Питання", а не "сумнів" - це типовий переклад.) Це нормально, але, ймовірно, призведе до кількох перерозподілів, оскільки StringBuilderспочатку буде виділено достатньо місця для рядка, який ви передали конструктору, плюс 16 символів. Отже, якщо ви додаєте більше 16 символів (я смію сказати, ви є, якщо їх додається 60!), Вам StringBuilderдоведеться перерозподілити принаймні один раз, а можливо, і багато разів. Якщо ви маєте розумне уявлення, наскільки великим буде кінцевий результат (скажімо, 400 символів), найкраще зробити sql = new StringBuilder(400);(або що завгодно), тоді зробіть appends.
TJ Crowder,

@Vanathi: Радий, що допомогло. Так, якщо це буде 6000 символів, кажучи, StringBuilderщо заздалегідь буде збережено близько восьми перерозподілів пам'яті (якщо припустити, що початковий рядок складав близько 10 символів, SB для початку мав би 26, потім подвоївся до 52, потім 104, 208, 416, 832, 1664, 3328 і, нарешті, 6656). Важливо лише якщо це гаряча точка, але все ж, якщо ви знаєте заздалегідь ... :-)
TJ Crowder

@TJ Crowder, ти хочеш сказати, що я не повинен використовувати оператор "+" для кращої роботи. так? то чому Oracal додав оператор "+" на їхній мові, будь ласка, уточніть? Будь-яким чином я підтримую вашу відповідь.
Сміт Патель

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 безпосередньо, це коли у вас є умовний код. Наприклад, код, який виглядає так, відчайдушно потребує 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, ви можете уникнути +операцій над рядками, використовуючи appendметод. Вам слід використовувати це таким чином:

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

У Java тип String - це незмінна послідовність символів, тому при додаванні двох рядків VM створює нове значення String із з’єднаними обома операндами.

StringBuilder забезпечує змінну послідовність символів, яку ви можете використовувати для зв’язку різних значень або змінних, не створюючи нових об’єктів String, і тому іноді це може бути ефективніше, ніж робота зі рядками

Це надає деякі корисні функції, як зміна вмісту послідовності символів, переданої як параметр всередині іншого методу, чого не можна зробити за допомогою рядків.

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корисно, коли ви не можете виконати всі конкатенації в одному виразі, але не в цьому випадку.
Джон Скіт,

1
Я розумію, що рядок у питанні розміщений як приклад. Немає сенсу будувати такий "фіксований" рядок ні за допомогою StringBuilder, ні додаючи різні фрагменти, оскільки ви можете просто визначити його в одній константі "select id1, id2 from table"
Tomas Narros

Але навіть якби існували непостійні значення змінних, він все одно використовував би одиницю, StringBuilderякщо б ви використовували return "select id1, " + foo + "something else" + bar;- так чому б цього не зробити? Питання не вказує на те, що щось потрібно пройти StringBuilderнавколо.
Джон Скіт,

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.