PreparedStatement В предложении альтернативы? - PullRequest
320 голосов
/ 07 октября 2008

Каковы лучшие обходные пути для использования предложения SQL IN с экземплярами java.sql.PreparedStatement, которое не поддерживается для нескольких значений из-за проблем безопасности атаки SQL-инъекцией: один ? заполнитель представляет одно значение, а не список значений.

Рассмотрим следующий оператор SQL:

SELECT my_column FROM my_table where search_column IN (?)

Использование preparedStatement.setString( 1, "'A', 'B', 'C'" );, по сути, является нерабочей попыткой обойти причины, по которым сначала стоит использовать ?.

Какие обходные пути доступны?

Ответы [ 28 ]

178 голосов
/ 10 октября 2008

Анализ различных доступных вариантов, а также плюсы и минусы каждого доступны здесь .

Предлагаемые варианты:

  • Подготовьте SELECT my_column FROM my_table WHERE search_column = ?, выполните его для каждого значения и объедините результаты на стороне клиента. Требуется только одно подготовленное заявление. Медленно и больно.
  • Подготовьте SELECT my_column FROM my_table WHERE search_column IN (?,?,?) и выполните его. Требуется одно подготовленное утверждение на размер списка. Быстро и очевидно.
  • Подготовьте SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ... и выполните его. [Или используйте UNION ALL вместо этих точек с запятой. --ed] Требуется одно подготовленное утверждение на размер списка. Тупо медленно, строго хуже, чем WHERE search_column IN (?,?,?), поэтому я не знаю, почему блоггер даже предложил это.
  • Использование хранимой процедуры для построения набора результатов.
  • Подготовить N запросов разного размера в списке; скажем, с 2, 10 и 50 значениями. Для поиска IN-списка с 6 различными значениями заполните запрос size-10 так, чтобы он выглядел как SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Любой порядочный сервер оптимизирует дублирующиеся значения перед выполнением запроса.

Ни один из этих вариантов не является супер отличным.

В этих местах были даны ответы на дубликаты вопросов с одинаково разумными альтернативами, но ни один из них не был супер великим:

Правильный ответ, если вы используете JDBC4 и сервер, который поддерживает x = ANY(y), должен использовать PreparedStatement.setArray, как описано здесь:

Похоже, что setArray не может работать с IN-списками.


Иногда операторы SQL загружаются во время выполнения (например, из файла свойств), но требуют переменного количества параметров. В таких случаях сначала определите запрос:

query=SELECT * FROM table t WHERE t.column IN (?)

Затем загрузите запрос. Затем определите количество параметров до его запуска. Когда число параметров известно, выполните:

sql = any( sql, count );

Например:

/**
 * Converts a SQL statement containing exactly one IN clause to an IN clause
 * using multiple comma-delimited parameters.
 *
 * @param sql The SQL statement string with one IN clause.
 * @param params The number of parameters the SQL statement requires.
 * @return The SQL statement with (?) replaced with multiple parameter
 * placeholders.
 */
public static String any(String sql, final int params) {
    // Create a comma-delimited list based on the number of parameters.
    final StringBuilder sb = new StringBuilder(
            new String(new char[params]).replace("\0", "?,")
    );

    // Remove trailing comma.
    sb.setLength(Math.max(sb.length() - 1, 0));

    // For more than 1 parameter, replace the single parameter with
    // multiple parameter placeholders.
    if (sb.length() > 1) {
        sql = sql.replace("(?)", "(" + sb + ")");
    }

    // Return the modified comma-delimited list of parameters.
    return sql;
}

Для некоторых баз данных, в которых передача массива через спецификацию JDBC 4 не поддерживается, этот метод может облегчить преобразование медленного = ? в более быстрое условие предложения IN (?), которое затем можно расширить, вызвав метод any.

112 голосов
/ 20 апреля 2012

Решение для PostgreSQL:

final PreparedStatement statement = connection.prepareStatement(
        "SELECT my_column FROM my_table where search_column = ANY (?)"
);
final String[] values = getValues();
statement.setArray(1, connection.createArrayOf("text", values));
final ResultSet rs = statement.executeQuery();
try {
    while(rs.next()) {
        // do some...
    }
} finally {
    rs.close();
}

или

final PreparedStatement statement = connection.prepareStatement(
        "SELECT my_column FROM my_table " + 
        "where search_column IN (SELECT * FROM unnest(?))"
);
final String[] values = getValues();
statement.setArray(1, connection.createArrayOf("text", values));
final ResultSet rs = statement.executeQuery();
try {
    while(rs.next()) {
        // do some...
    }
} finally {
    rs.close();
}
18 голосов
/ 10 октября 2008

Нет простого способа AFAIK. Если цель состоит в том, чтобы поддерживать высокий коэффициент кэширования операторов (т.е. не создавать оператор для каждого счетчика параметров), вы можете сделать следующее:

  1. создать оператор с несколькими (например, 10) параметрами:

    ... ГДЕ ВХОД (?,?,?,?,?,?,?,?,?,?) ...

  2. Свяжите все действующие параметры

    SetString (1, "Foo"); SetString (2, "бар");

  3. Свяжите остальные как NULL

    SetNull (3, Types.VARCHAR) ... SetNull (10, Types.VARCHAR)

NULL никогда не совпадает с чем-либо, поэтому он оптимизируется разработчиком плана SQL.

Эту логику легко автоматизировать, если передать список в функцию DAO:

while( i < param.size() ) {
  ps.setString(i+1,param.get(i));
  i++;
}

while( i < MAX_PARAMS ) {
  ps.setNull(i+1,Types.VARCHAR);
  i++;
}
10 голосов
/ 08 октября 2008

Неприятный обходной путь, но, безусловно, выполнимый - использовать вложенный запрос. Создайте временную таблицу MYVALUES со столбцом в ней. Вставьте ваш список значений в таблицу MYVALUES. Затем выполните

select my_column from my_table where search_column in ( SELECT value FROM MYVALUES )

Уродливая, но жизнеспособная альтернатива, если ваш список значений очень большой.

Этот метод имеет дополнительное преимущество, заключающееся в том, что оптимизатор может оптимизировать планы запросов (проверять страницу на наличие нескольких значений, сканирование таблицы только один раз, а не один раз для каждого значения и т. Д.), Что может сэкономить накладные расходы, если ваша база данных не кэширует подготовленные операторы. Ваши «ВСТАВКИ» должны быть выполнены в пакетном режиме, а таблицу MYVALUES, возможно, потребуется настроить, чтобы иметь минимальную блокировку или другие средства защиты от высоких накладных расходов.

8 голосов
/ 17 февраля 2016

Ограничения оператора in () - корень всего зла.

Это работает для тривиальных случаев, и вы можете расширить его с помощью «автоматической генерации подготовленного оператора», однако оно всегда имеет свои пределы.

  • если вы создаете оператор с переменным числом параметров, это приведет к дополнительным затратам на анализ SQL при каждом вызове
  • на многих платформах количество параметров оператора in () ограничено
  • на всех платформах, общий размер текста SQL ограничен, что делает невозможным отправку 2000 заполнителей для параметров in
  • отправка переменных связывания 1000-10k невозможна, поскольку драйвер JDBC имеет свои ограничения

Подход in () может быть достаточно хорош для некоторых случаев, но не является надежным:)

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

Вариант грубой силы здесь http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Однако, если вы можете использовать PL / SQL, этот беспорядок может стать довольно аккуратным.

function getCustomers(in_customerIdList clob) return sys_refcursor is 
begin
    aux_in_list.parse(in_customerIdList);
    open res for
        select * 
        from   customer c,
               in_list v
        where  c.customer_id=v.token;
    return res;
end;

Затем в параметре можно передать произвольное количество идентификаторов клиентов, разделенных запятыми, и:

  • не получит задержки разбора, поскольку SQL для выбора стабилен
  • нет сложности с конвейерными функциями - это всего лишь один запрос
  • SQL использует простое соединение вместо оператора IN, что довольно быстро
  • В конце концов, это хорошее эмпирическое правило: , а не , воздействовать на базу данных любым простым выбором или DML, поскольку это Oracle, который предлагает световые годы больше, чем MySQL или аналогичные простые движки баз данных. PL / SQL позволяет эффективно скрывать модель хранения от модели предметной области вашего приложения.

Хитрость здесь в следующем:

  • нам нужен вызов, который принимает длинную строку и сохраняет где-то, где сессия db может получить к ней доступ (например, простая переменная пакета или dbms_session.set_context)
  • тогда нам нужно представление, которое может разобрать это по строкам
  • и затем у вас есть представление, содержащее идентификаторы, которые вы запрашиваете, поэтому все, что вам нужно, - это простое соединение с запрашиваемой таблицей.

Вид выглядит так:

create or replace view in_list
as
select
    trim( substr (txt,
          instr (txt, ',', 1, level  ) + 1,
          instr (txt, ',', 1, level+1)
             - instr (txt, ',', 1, level) -1 ) ) as token
    from (select ','||aux_in_list.getpayload||',' txt from dual)
connect by level <= length(aux_in_list.getpayload)-length(replace(aux_in_list.getpayload,',',''))+1

где aux_in_list.getpayload ссылается на исходную строку ввода.


Возможным подходом было бы передать массивы pl / sql (поддерживаемые только Oracle), однако вы не можете использовать их в чистом SQL, поэтому всегда необходим шаг преобразования. Преобразование не может быть выполнено в SQL, поэтому, в конце концов, передача clob со всеми параметрами в строке и преобразование его в представление является наиболее эффективным решением.

5 голосов
/ 24 февраля 2011

Мой обходной путь:

create or replace type split_tbl as table of varchar(32767);
/

create or replace function split
(
  p_list varchar2,
  p_del varchar2 := ','
) return split_tbl pipelined
is
  l_idx    pls_integer;
  l_list    varchar2(32767) := p_list;
  l_value    varchar2(32767);
begin
  loop
    l_idx := instr(l_list,p_del);
    if l_idx > 0 then
      pipe row(substr(l_list,1,l_idx-1));
      l_list := substr(l_list,l_idx+length(p_del));
    else
      pipe row(l_list);
      exit;
    end if;
  end loop;
  return;
end split;
/

Теперь вы можете использовать одну переменную для получения некоторых значений в таблице:

select * from table(split('one,two,three'))
  one
  two
  three

select * from TABLE1 where COL1 in (select * from table(split('value1,value2')))
  value1 AAA
  value2 BBB

Итак, подготовленное заявление может быть:

  "select * from TABLE where COL in (select * from table(split(?)))"

С уважением,

Хавьер Ибанез

5 голосов
/ 08 апреля 2016

Вот как я решил это в своем приложении. В идеале вы должны использовать StringBuilder вместо + для строк.

    String inParenthesis = "(?";
    for(int i = 1;i < myList.size();i++) {
      inParenthesis += ", ?";
    }
    inParenthesis += ")";

    try(PreparedStatement statement = SQLite.connection.prepareStatement(
        String.format("UPDATE table SET value='WINNER' WHERE startTime=? AND name=? AND traderIdx=? AND someValue IN %s", inParenthesis))) {
      int x = 1;
      statement.setLong(x++, race.startTime);
      statement.setString(x++, race.name);
      statement.setInt(x++, traderIdx);

      for(String str : race.betFair.winners) {
        statement.setString(x++, str);
      }

      int effected = statement.executeUpdate();
    }

Использование переменной типа x вместо конкретных чисел очень помогает, если вы решите изменить запрос позже.

5 голосов
/ 07 октября 2008

Я никогда не пробовал, но .setArray () будет делать то, что вы ищете?

Обновление : Очевидно, нет. setArray, кажется, работает только с java.sql.Array, который поступает из столбца ARRAY, который вы получили из предыдущего запроса, или из подзапроса со столбцом ARRAY.

3 голосов
/ 07 октября 2008

Полагаю, вы могли бы (используя базовые операции со строками) сгенерировать строку запроса в PreparedStatement, чтобы количество ? соответствовало количеству элементов в вашем списке.

Конечно, если вы делаете это, вы в шаге от генерации гигантской цепочки OR в своем запросе, но без правильного числа ? в строке запроса, я не вижу как еще можно обойти это.

2 голосов
/ 09 июня 2016

Вы можете использовать метод setArray, как указано в этом javadoc :

PreparedStatement statement = connection.prepareStatement("Select * from emp where field in (?)");
Array array = statement.getConnection().createArrayOf("VARCHAR", new Object[]{"E1", "E2","E3"});
statement.setArray(1, array);
ResultSet rs = statement.executeQuery();
...