Правильный способ использования StringBuilder в SQL

88

Я только что нашел в своем проекте такую ​​сборку sql-запроса:

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

Достигает ли это StringBuilderсвоей цели, т.е. уменьшения использования памяти?

Я сомневаюсь в этом, потому что в конструкторе используется '+' (оператор конкатенации строк). Будет ли это занимать такой же объем памяти, как при использовании String, как в приведенном ниже коде? s Я понял, при использовании отличается StringBuilder.append().

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

Оба оператора одинаковы в использовании памяти или нет? Просьба уточнить.

Заранее спасибо!

Редактировать:

Кстати, это не мой код . Нашел в старом проекте. Кроме того, запрос не такой маленький, как в моем примере. :)

Ваанду
источник
1
БЕЗОПАСНОСТЬ SQL: всегда используйте PreparedStatementили что-то подобное: docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Кристоф Русси
Помимо использования памяти, почему бы вместо этого не использовать библиотеку построителя SQL: stackoverflow.com/q/370818/521799
Лукас Эдер

Ответы:

182

Цель использования StringBuilder, т.е. уменьшение памяти. Это достигнуто?

Нет, совсем нет. Этот код используется StringBuilderнеправильно. (Я думаю, вы неверно процитировали это; наверняка вокруг нет цитат id2и table?)

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

Будет ли это занимать память, равную использованию String, как показано ниже?

Нет, это вызовет больший отток памяти, чем просто указанное вами прямое соединение. (До тех пор, пока оптимизатор 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;

... что довольно уродливо.

Однако важно помнить, что во всех случаях, за исключением очень немногих, это не имеет значения, и предпочтение отдается удобочитаемости (что повышает ремонтопригодность), за исключением конкретных проблем с производительностью.

TJ Crowder
источник
Хорошо, так лучше. Использование конструктора без параметров немного неудачно, но вряд ли будет значительным. Я бы все равно использовал одно x + y + zвыражение, StringBuilderесли бы у меня не было веских оснований подозревать, что это будет серьезной проблемой.
Джон Скит,
@Crowder, есть еще одно сомнение. StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");.... Подобные sql.appendстроки составляют около 60 строк. Это нормально?
Vaandu 04
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);
   }
}

Когда вы используете 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 представляет собой неизменяемую последовательность символов, поэтому при добавлении двух строк виртуальная машина создает новое значение 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
Нет, не надо. Он менее читабелен, чем using +, который в любом случае будет преобразован в тот же код. StringBuilderполезно, когда вы не можете выполнить всю конкатенацию в одном выражении, но не в этом случае.
Джон Скит,
1
Я понимаю, что строка в вопросе размещена в качестве примера. Не имело бы смысла создавать такую ​​«фиксированную» строку ни с помощью StringBuilder, ни с добавлением различных фрагментов, поскольку вы могли бы просто определить ее в одной константе «выберите id1, id2 из таблицы»
Томас Наррос
Но даже если бы у переменных были непостоянные значения, он все равно использовал бы одно, StringBuilderесли бы вы использовали return "select id1, " + foo + "something else" + bar;- так почему бы этого не сделать? Этот вопрос не указывает на то, что нужно что-то обходить StringBuilderстороной.
Джон Скит