Альтернативы предложения PreparedStatement IN?

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

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

SELECT my_column FROM my_table where search_column IN (?)

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

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

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

Chris Mazzola 09.07.2009 01:51

Одним из преимуществ подготовленных операторов является то, что sohuld может быть скомпилирован один раз для повышения эффективности. Делая предложение in динамическим, это эффективно отменяет подготовленный оператор.

user246585 08.01.2010 20:29

Фактически, это работает для MySQL (используя setObject для установки массива String в качестве значения параметра). Какую БД вы используете?

Frans 17.07.2012 17:40

Вот Конкретный ответ Oracle

Peter Hart 09.03.2013 00:32

Вот связанный с этим вопрос: stackoverflow.com/q/6956025/521799

Lukas Eder 11.11.2013 19:37

@Frans у меня не работает. Он выполняет запрос, но всегда безрезультатно.

Jayen 22.09.2016 09:55

Конкретный вопрос по MSSQL от основателей SO: stackoverflow.com/questions/337704/…

Vadzim 30.04.2019 01:29
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
359
7
335 047
29
Перейти к ответу Данный вопрос помечен как решенный

Ответы 29

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

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

Не работает со всеми базами данных, но это «правильный» подход.

skaffman 07.10.2008 17:48

Вы имеете в виду всех драйверов. Некоторые драйверы имеют проприетарные эквиваленты стандарта этого года (прошлого века?). Другой способ - вставить пакет значений во временную таблицу, но не все базы данных поддерживают это ...

Tom Hawtin - tackline 07.10.2008 18:06
java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… According to Sun, Array content [typically] remains on the server side and is pulled as needed. PreparedStatement.setArray() can send back an Array from a previous ResultSet, not create a new Array on the client side.
Chris Mazzola 09.10.2008 20:21

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

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

На самом деле это не решение для меня, так как я хочу отправить другое количество? каждый раз, когда я вызываю пс. Но не думайте, что я не подумал об этом. :П

Chris Mazzola 07.10.2008 19:28

Еще один прием: вы можете использовать большое количество заполнителей параметров - столько, сколько будет самый длинный список значений, который у вас будет - и если ваш список значений короче, вы можете повторять значения: ... WHERE searchfield IN (? ,?,?,?,?,?,?,?), а затем укажите значения: A, B, C, D, A, B, C, D

Bill Karwin 07.10.2008 20:08

Но в целом я предпочитаю решение Адама: динамически генерировать SQL и объединять? заполнители, соответствующие количеству передаваемых значений.

Bill Karwin 07.10.2008 20:12

Билл, это решение работает, если я не хочу повторно использовать PreparedStatement. Другое решение - выполнить вызов одного параметра несколько раз и накапливать результаты на стороне клиента. Вероятно, было бы более эффективно создать / выполнить новый оператор с произвольным числом? хотя каждый раз.

Chris Mazzola 09.10.2008 20:29

попробовать использовать функцию instr?

select my_column from my_table where  instr(?, ','||search_column||',') > 0

потом

ps.setString(1, ",A,B,C,"); 

По общему признанию, это немного грязный прием, но он снижает возможности внедрения sql. В любом случае работает в Oracle.

О, и я знаю, что он не будет использовать индексы

stjohnroe 07.10.2008 20:07

это не сработает для некоторых строк, например, если строка содержит ','.

David Portabella 04.06.2012 15:34

Просто для полноты: пока набор значений не слишком велик, вы также мог просто строите строку для такого оператора, как

... WHERE tab.col = ? OR tab.col = ? OR tab.col = ?

который затем можно передать в prepare (), а затем использовать setXXX () в цикле для установки всех значений. Это выглядит неприятно, но многие «большие» коммерческие системы обычно делают такие вещи, пока не достигают специфичных для БД ограничений, таких как 32 КБ (я думаю, что это так) для операторов в Oracle.

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

Да, ты прав. В этом случае моей целью было повторно использовать PreparedStatement с разным количеством элементов каждый раз.

Chris Mazzola 09.10.2008 20:24

Использование «ИЛИ» запутает намерение. Придерживайтесь «IN», так как его легче читать и смысл более ясен. Единственная причина для переключения - если планы запроса были другими.

James Schek 10.10.2008 01:41

Следуя идее Адама. Сделайте свой подготовленный оператор вроде select my_column из my_table, где search_column в (#) Создайте строку x и заполните ее числом "?,?,?" в зависимости от вашего списка ценностей Затем просто измените # в запросе для вашей новой строки x и заполните

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

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

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

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

Какие преимущества это будет по сравнению с запросом my_table по одному значению за раз?

Paul Tomblin 09.10.2008 21:43

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

James Schek 10.10.2008 01:34

выглядит неплохо, но могут быть проблемы с параллелизмом. содержит ли спецификация jdbc способ создания временной анонимной таблицы в памяти? или что-то в этом роде, если возможно, не конкретное jdbc-vendor?

David Portabella 04.06.2012 15:31

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

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

    ... ГДЕ А В (?,?,?,?,?,?,?,?,?,?) ...

  2. Привязать все актуальные параметры

    setString (1, «фу»); setString (2, «бар»);

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

    setNull (3, Типы.VARCHAR) ... setNull (10, Типы.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++;
}

«NULL никогда ничего не соответствует» - Соответствует ли NULL в запросе значению NULL в базе данных?

Craig McQueen 14.10.2013 06:11

@CraigMcQueen Нет, это не так. Null даже не соответствует нулю, согласно стандарту ANSI.

Dawood ibn Kareem 12.02.2014 01:00

Вы можете сопоставить NULL с помощью ключевого слова IS NULL. Хороший способ обнаружить строки, которых нет в объединенной таблице, - использовать LEFT JOIN вместе с IS NULL. 'SELECT a.URL, b.URL FROM TABLE_A a LEFT JOIN TABLE_B b ON a_A.URL = b_B.URL WHERE b.URL IS NULL' Это покажет все строки в таблице A, которые не соответствуют в таблице B.

Jens Tandstad 10.11.2014 19:25

Но будьте осторожны с этим. NOT IN и IN не обрабатывают значения NULL одинаково. Запустите это и посмотрите, что произойдет: select 'Matched' as did_it_match where 1 not in (5, null); Затем удалите null и наблюдайте за волшебством.

Brandon 10.06.2016 00:19

Или вы можете установить для всех дополнительных параметров значение любого предыдущего параметра. Любой достойный движок БД отфильтрует их. Итак, a IN (1,2,3,3,3,3,3) - это то же самое, что a IN (1,2,3). Он также работает с NOT IN, в отличие от a NOT IN (1,2,3,null,null,null,null) (который всегда не возвращает строк, поскольку any_value != NULL всегда ложен).

Ruslan Stelmachenko 07.08.2018 19:19
Ответ принят как подходящий

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

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

  • Подготовьте SELECT my_column FROM my_table WHERE search_column = ?, выполните его для каждого значения и СОЕДИНИТЕ результаты на стороне клиента. Требуется только один подготовленный оператор. Медленно и болезненно.
  • Подготовьте SELECT my_column FROM my_table WHERE search_column IN (?,?,?) и выполните его. Требуется одна подготовленная инструкция для каждого размера IN-list. Быстро и очевидно.
  • Подготовьте SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ... и выполните его. [Или используйте UNION ALL вместо точек с запятой. --ed] Требуется один подготовленный оператор для каждого списка размера IN. Глупо медленно, строго хуже, чем WHERE search_column IN (?,?,?), поэтому я не знаю, почему блогер даже предложил это.
  • Используйте хранимую процедуру для создания набора результатов.
  • Подготовить N запросов разного размера IN-list; скажем, со значениями 2, 10 и 50. Чтобы найти список IN с 6 различными значениями, заполните запрос размера 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(
        String.join(", ", Collections.nCopies(possibleValue.size(), "?")));

    // 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.

Создайте строку запроса в PreparedStatement, чтобы число? Соответствовало количеству элементов в вашем списке. Вот пример:

public void myQuery(List<String> items, int other) {
  ...
  String q4in = generateQsForIn(items.size());
  String sql = "select * from stuff where foo in ( " + q4in + " ) and bar = ?";
  PreparedStatement ps = connection.prepareStatement(sql);
  int i = 1;
  for (String item : items) {
    ps.setString(i++, item);
  }
  ps.setInt(i++, other);
  ResultSet rs = ps.executeQuery();
  ...
}

private String generateQsForIn(int numQs) {
    String items = "";
    for (int i = 0; i < numQs; i++) {
        if (i != 0) items += ", ";
        items += "?";
    }
    return items;
}

Больше нет необходимости использовать StringBuilder. Компилятор все равно преобразует знаки + в StringBuilder.append (), поэтому производительность не снижается. Попробуйте сами :)

neu242 18.12.2009 14:03

@ neu242: Да, компилятор использует StringBuilder. Но не так, как вы думаете. Декомпилируя generateQsForIn, вы можете увидеть, что для каждой итерации цикла выделяется два нового StringBuilder и для каждого вызывается toString. Оптимизация StringBuilder улавливает только такие вещи, как "x" + i+ "y" + j, но не выходит за рамки одного выражения.

A.H. 29.11.2012 19:45

@ neu242 Разве нельзя использовать ps.setObject(1,items) вместо перебора списка и последующей установки paramteres?

Neha Choudhary 16.07.2013 21:02

Мое решение:

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(?)))"

С уважением,

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

Это PL / SQL, да. В других базах данных работать не будет. Обратите внимание, что эта реализация имеет ограничение входных параметров (общая длина ограничена 32 КБ символов), а также ограничение производительности, поскольку вызов конвейерной функции выполняет переключение контекста между PL / SQL и SQL-механизмами Oracle.

Gee Bee 06.02.2018 22:40

Сормула поддерживает оператор SQL IN, позволяя вам указать объект java.util.Collection в качестве параметра. Он создает подготовленный оператор с? для каждого из элементов коллекции. См. Пример 4 (SQL в примере - это комментарий, поясняющий, что создается, но не используется Sormula).

Решение для 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));

try (ResultSet rs = statement.executeQuery()) {
    while(rs.next()) {
        // do some...
    }
}

или же

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

try (ResultSet rs = statement.executeQuery()) {
    while(rs.next()) {
        // do some...
    }
}

выглядит хорошо. какую часть этого кода указывает PostgreSQL? "где столбец поиска = ЛЮБОЙ (?)"? или connection.createArrayOf? или что-то другое?

David Portabella 04.06.2012 13:53

Я думаю, что он более специфичен для JDBC4, чем для PostgreSQL, из-за части .createArrayOf(), но я не уверен, что строгая семантика для пользовательских Array определяется спецификацией JDBC.

lvella 19.07.2012 18:01

Если .createArrayOf не работает, вы можете вручную создать литерал массива, например String arrayLiteral = "{A,\"B \", C,D}"(обратите внимание, что в "B" есть пробел, а в C нет), а затем statement.setString(1,arrayLiteral), где подготовленным оператором является ... IN (SELECT UNNEST(?::VARCHAR[])) или ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: я не думаю, что ANY работает с SELECT.)

ADTC 01.08.2013 07:17

Отличное решение! Действительно спас мне день. Для целочисленного массива я использовал "int" в первом параметре createArrayOf (), и это выглядит хорошо. Однако, если судить по документации, этот первый параметр специфичен для БД.

Emmanuel Touzery 26.08.2013 11:11

Это кажется самым чистым решением. Если кто-то ищет специфический синтаксис HSQLDB: мне удалось заставить его работать с IN (UNNEST (?))

aureianimus 04.03.2014 11:20

Жаль, что функция JDBC не имеет широкой поддержки в драйверах БД. Ни один из драйверов для MSSQL, которые я пробовал, не реализовал. Тем не менее, мне нравится, что если я когда-нибудь напишу специфичный для PGSQL код, это будет выглядеть аккуратно.

Vlasec 30.01.2015 12:09

Когда я реализую это, я получаю: ОШИБКА: аргумент IN не должен возвращать набор

artis3n 09.11.2015 06:28

@Ivella Если я понимаю решение, то, что специфично для PostgreSQL, является возможность использования параметра массива в качестве списка значений. Обычно параметры массива используются для типов массивов sql, как показано здесь docs.oracle.com/javase/tutorial/jdbc/basics/array.html

cquezel 20.01.2016 20:23

Я использовал jdbcTemplate с этим ЛЮБЫМ форматом. Единственное, что мне пришлось настроить abit, чтобы заставить его работать, - это преобразовать список / массив значений, входящих в качестве аргумента, в строку с отдельными значениями, разделенными запятыми. Мне также нужно было поставить {перед строкой и закончить ее}. Затем в ЛЮБОЙ (?) Я помещаю ЛЮБОЙ (: theCommaSeparatedValuesString :: long []). Может быть, был бы даже более простой и изящный способ сделать это, но я остановился на нем, так как он работал. Я создал метод удаления, используя это.

ghoulfolk 30.04.2019 09:33

вместо использования

SELECT my_column FROM my_table where search_column IN (?)

используйте оператор Sql как

select id, name from users where id in (?, ?, ?)

и

preparedStatement.setString( 1, 'A');
preparedStatement.setString( 2,'B');
preparedStatement.setString( 3, 'C');

или используйте хранимую процедуру, это было бы лучшим решением, поскольку операторы sql будут скомпилированы и сохранены на сервере базы данных

Я столкнулся с рядом ограничений, связанных с подготовленным оператором:

  1. Подготовленные операторы кэшируются только внутри одного сеанса (Postgres), поэтому он действительно будет работать только с пулом соединений.
  2. Множество различных подготовленных операторов, предложенных @BalusC, могут привести к переполнению кеша, и ранее кэшированные операторы будут отброшены
  3. Запрос должен быть оптимизирован и использовать индексы. Звучит очевидно, однако, например, оператор ANY (ARRAY ...), предложенный @Boris в одном из лучших ответов, не может использовать индексы, и запрос будет медленным, несмотря на кеширование
  4. Подготовленный оператор также кэширует план запроса, и фактические значения любых параметров, указанных в операторе, недоступны.

Среди предложенных решений я бы выбрал то, которое не снижает производительность запросов и делает меньше запросов. Это будет номер 4 (пакетирование нескольких запросов) из ссылки @Don или указание значений NULL для ненужных символов '?' отметки по предложению @Vladimir Dyuzhev

Существуют разные альтернативные подходы, которые мы можем использовать для предложения IN в PreparedStatement.

  1. Использование одиночных запросов - самая низкая производительность и ресурсоемкость
  2. Использование StoredProcedure - самое быстрое, но зависит от базы данных
  3. Создание динамического запроса для PreparedStatement - хорошая производительность, но без кеширования, и PreparedStatement каждый раз перекомпилируется.
  4. Используйте NULL в запросах PreparedStatement - Оптимальная производительность, отлично работает, когда вы знаете предел аргументов предложения IN. Если ограничения нет, то вы можете выполнять запросы в пакетном режиме. Фрагмент кода примера:

        int i = 1;
        for(; i <=ids.length; i++){
            ps.setInt(i, ids[i-1]);
        }
    
        //set null for remaining ones
        for(; i<=PARAM_SIZE;i++){
            ps.setNull(i, java.sql.Types.INTEGER);
        }
    

Вы можете проверить более подробную информацию об этих альтернативных подходах здесь.

«Создание динамического запроса для PreparedStatement - хорошая производительность, но без кеширования, и PreparedStatement перекомпилируется каждый раз». кэширование и предотвращение перекомпиляции - вот что делает подготовленный оператор хорошо работающим. Поэтому я не согласен с вашим утверждением. Однако это предотвратит внедрение SQL, поскольку вы ограничиваете конкатенированный / динамический ввод запятой.

Brandon 11.07.2014 03:36

Я согласен с вами, однако «Хорошая производительность» здесь предназначена для этого конкретного сценария. Он лучше, чем подход 1, однако подход 2 является самым быстрым.

Pankaj 11.07.2014 11:12

В некоторых ситуациях может помочь регулярное выражение. Вот пример, который я проверял на Oracle, и он работает.

select * from my_table where REGEXP_LIKE (search_column, 'value1|value2')

Но у него есть ряд недостатков:

  1. Любой применяемый столбец должен быть преобразован в varchar / char, по крайней мере, неявно.
  2. Нужно быть осторожным со спецсимволами.
  3. Это может снизить производительность - в моем случае версия IN использует сканирование индекса и диапазона, а версия REGEXP выполняет полное сканирование.

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

Пример: Предположим, у вас есть несколько параметров для передачи в предложении IN. Просто поместите фиктивную строку внутри предложения 'IN', скажем, «PARAM» действительно обозначает список параметров, которые будут приходить на место этой фиктивной строки.

    select * from TABLE_A where ATTR IN (PARAM);

Вы можете собрать все параметры в одну строковую переменную в вашем Java-коде. Это можно сделать следующим образом:

    String param1 = "X";
    String param2 = "Y";
    String param1 = param1.append(",").append(param2);

Вы можете добавить все свои параметры, разделенные запятыми, в одну строковую переменную, 'param1', в нашем случае.

После объединения всех параметров в одну строку вы можете просто заменить фиктивный текст в своем запросе, то есть "PARAM" в данном случае, параметром String, то есть param1. Вот что вам нужно сделать:

    String query = query.replaceFirst("PARAM",param1); where we have the value of query as 

    query = "select * from TABLE_A where ATTR IN (PARAM)";

Теперь вы можете выполнить свой запрос, используя метод executeQuery (). Просто убедитесь, что в вашем запросе нет слова «PARAM». Вы можете использовать комбинацию специальных символов и алфавитов вместо слова «ПАРАМЕТР», чтобы исключить возможность появления такого слова в запросе. Надеюсь, у вас есть решение.

Примечание. Хотя это не подготовленный запрос, он выполняет ту работу, которую я хотел, чтобы мой код выполнял.

Просто для полноты и потому, что я не видел, чтобы кто-то еще предлагал это:

Перед реализацией любого из приведенных выше сложных предложений подумайте, действительно ли SQL-инъекция является проблемой в вашем сценарии.

Во многих случаях значение, предоставленное IN (...), представляет собой список идентификаторов, которые были сгенерированы таким образом, чтобы вы могли быть уверены, что никакая инъекция невозможна ... (например, результаты предыдущего выбора some_id из some_table, где некоторое_условие.)

В этом случае вы можете просто объединить это значение и не использовать для него службы или подготовленный оператор или использовать их для других параметров этого запроса.

query = "select f1,f2 from t1 where f3=? and f2 in (" + sListOfIds + ");";

Вот полное решение на Java для создания подготовленного оператора за вас:

/*usage:

Util u = new Util(500); //500 items per bracket. 
String sqlBefore  = "select * from myTable where (";
List<Integer> values = new ArrayList<Integer>(Arrays.asList(1,2,4,5)); 
string sqlAfter = ") and foo = 'bar'"; 

PreparedStatement ps = u.prepareStatements(sqlBefore, values, sqlAfter, connection, "someId");
*/



import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class Util {

    private int numValuesInClause;

    public Util(int numValuesInClause) {
        super();
        this.numValuesInClause = numValuesInClause;
    }

    public int getNumValuesInClause() {
        return numValuesInClause;
    }

    public void setNumValuesInClause(int numValuesInClause) {
        this.numValuesInClause = numValuesInClause;
    }

    /** Split a given list into a list of lists for the given size of numValuesInClause*/
    public List<List<Integer>> splitList(
            List<Integer> values) {


        List<List<Integer>> newList = new ArrayList<List<Integer>>(); 
        while (values.size() > numValuesInClause) {
            List<Integer> sublist = values.subList(0,numValuesInClause);
            List<Integer> values2 = values.subList(numValuesInClause, values.size());   
            values = values2; 

            newList.add( sublist);
        }
        newList.add(values);

        return newList;
    }

    /**
     * Generates a series of split out in clause statements. 
     * @param sqlBefore ""select * from dual where ("
     * @param values [1,2,3,4,5,6,7,8,9,10]
     * @param "sqlAfter ) and id = 5"
     * @return "select * from dual where (id in (1,2,3) or id in (4,5,6) or id in (7,8,9) or id in (10)"
     */
    public String genInClauseSql(String sqlBefore, List<Integer> values,
            String sqlAfter, String identifier) 
    {
        List<List<Integer>> newLists = splitList(values);
        String stmt = sqlBefore;

        /* now generate the in clause for each list */
        int j = 0; /* keep track of list:newLists index */
        for (List<Integer> list : newLists) {
            stmt = stmt + identifier +" in (";
            StringBuilder innerBuilder = new StringBuilder();

            for (int i = 0; i < list.size(); i++) {
                innerBuilder.append("?,");
            }



            String inClause = innerBuilder.deleteCharAt(
                    innerBuilder.length() - 1).toString();

            stmt = stmt + inClause;
            stmt = stmt + ")";


            if (++j < newLists.size()) {
                stmt = stmt + " OR ";
            }

        }

        stmt = stmt + sqlAfter;
        return stmt;
    }

    /**
     * Method to convert your SQL and a list of ID into a safe prepared
     * statements
     * 
     * @throws SQLException
     */
    public PreparedStatement prepareStatements(String sqlBefore,
            ArrayList<Integer> values, String sqlAfter, Connection c, String identifier)
            throws SQLException {

        /* First split our potentially big list into lots of lists */
        String stmt = genInClauseSql(sqlBefore, values, sqlAfter, identifier);
        PreparedStatement ps = c.prepareStatement(stmt);

        int i = 1;
        for (int val : values)
        {

            ps.setInt(i++, val);

        }
        return ps;

    }

}

Spring позволяет использовать передача java.util.Lists в NamedParameterJdbcTemplate, который автоматизирует создание (?,?,?, ...,?) В зависимости от количества аргументов.

Для Oracle это сообщение в блоге обсуждает использование oracle.sql.ARRAY (Connection.createArrayOf не работает с Oracle). Для этого вам нужно изменить свой оператор SQL:

SELECT my_column FROM my_table where search_column IN (select COLUMN_VALUE from table(?))

функция таблицы оракула преобразует переданный массив в табличное значение, используемое в операторе IN.

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

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

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

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

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

Вариант грубой силы здесь 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, что довольно быстро
  • в конце концов, это хорошее практическое правило, когда нет попадает в базу данных с помощью любого простого select или 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 со всеми параметрами в строке и преобразование его в представление является наиболее эффективным решением.

Вот как я решил это в собственном приложении. В идеале вы должны использовать 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 вместо конкретных чисел очень помогает, если вы решите изменить запрос позже.

Вы можете использовать метод 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();

это поддерживается не всеми драйверами, если функция не поддерживается, вы получите SQLFeatureNotSupportedException

unnamed 25.10.2017 19:04

К сожалению, мой драйвер не поддерживает это

EdXX 13.01.2018 18:38

Мой способ обхода (JavaScript)

    var s1 = " SELECT "

 + "FROM   table t "

 + "  where t.field in ";

  var s3 = '(';

  for(var i =0;i<searchTerms.length;i++)
  {
    if (i+1 == searchTerms.length)
    {
     s3  = s3+'?)';
    }
    else
    {
        s3  = s3+'?, ' ;
    }
   }
    var query = s1+s3;

    var pstmt = connection.prepareStatement(query);

     for(var i =0;i<searchTerms.length;i++)
    {
        pstmt.setString(i+1, searchTerms[i]);
    }

SearchTerms - это массив, который содержит ваши входные данные / ключи / поля и т. д.

PreparedStatement не предоставляет никакого хорошего способа справиться с предложением SQL IN. Per http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "Вы не можете заменять вещи, которые должны стать частью инструкции SQL. Это необходимо, потому что, если сам SQL может измениться, драйвер не может предварительно скомпилировать инструкцию. Это также имеет приятный побочный эффект предотвращения SQL инъекционные атаки ". В итоге я использовал следующий подход:

String query = "SELECT my_column FROM my_table where search_column IN ($searchColumns)";
query = query.replace("$searchColumns", "'A', 'B', 'C'");
Statement stmt = connection.createStatement();
boolean hasResults = stmt.execute(query);
do {
    if (hasResults)
        return stmt.getResultSet();

    hasResults = stmt.getMoreResults();

} while (hasResults || stmt.getUpdateCount() != -1);

SetArray - лучшее решение, но оно недоступно для многих старых драйверов. В java8 можно использовать следующий обходной путь

String baseQuery  = "SELECT my_column FROM my_table where search_column IN (%s)"

String markersString = inputArray.stream().map(e -> "?").collect(joining(","));
String sqlQuery = String.format(baseSQL, markersString);

//Now create Prepared Statement and use loop to Set entries
int index=1;

for (String input : inputArray) {
     preparedStatement.setString(index++, input);
}

Это решение лучше, чем другие уродливые решения цикла while, в которых строка запроса создается вручную.

Вы можете использовать Collections.nCopies для создания коллекции заполнителей и присоединить их с помощью String.join:

List<String> params = getParams();
String placeHolders = String.join(",", Collections.nCopies(params.size(), "?"));
String sql = "select * from your_table where some_column in (" + placeHolders + ")";
try (   Connection connection = getConnection();
        PreparedStatement ps = connection.prepareStatement(sql)) {
    int i = 1;
    for (String param : params) {
        ps.setString(i++, param);
    }
    /*
     * Execute query/do stuff
     */
}

Кажется, пока что это лучшее решение при использовании Oracle JDBC ...

jansohn 06.03.2020 16:39

Я только что разработал для этого вариант, специфичный для PostgreSQL. Это что-то вроде взлома и имеет свои плюсы, минусы и ограничения, но, похоже, он работает и не ограничивается конкретным языком разработки, платформой или драйвером PG.

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

Поэтому, если вы хотите искать «foo», «blah» и «abc», вы можете объединить их в одну строку как: 'foo, blah, abc'. Вот простой SQL:

select column from table
where search_column = any (string_to_array('foo,blah,abc', ',')::text[]);

Вы, очевидно, измените явное приведение на то, что вы хотите, чтобы ваш результирующий массив значений был - int, text, uuid и т.д. И поскольку функция принимает одно строковое значение (или два, я полагаю, если вы хотите настроить разделитель также), вы можете передать его как параметр в подготовленном операторе:

select column from table
where search_column = any (string_to_array(, ',')::text[]);

Это даже достаточно гибко, чтобы поддерживать такие вещи, как сравнения LIKE:

select column from table
where search_column like any (string_to_array('foo%,blah%,abc%', ',')::text[]);

Опять же, нет никаких сомнений в том, что это взлом, но он работает и позволяет вам по-прежнему использовать предварительно скомпилированные подготовленные операторы, которые принимают дискретные параметры * кхм *, с соответствующими преимуществами безопасности и (возможно) производительности. Является ли это целесообразным и действительно эффективным? Естественно, это зависит от того, у вас есть синтаксический анализ строк и, возможно, приведение типов, прежде чем ваш запрос даже будет запущен. Если вы ожидаете отправить три, пять, несколько десятков значений, конечно, это нормально. Несколько тысяч? Да, может быть, не так уж и много. YMMV, применяются ограничения и исключения, никаких гарантий, явных или подразумеваемых.

Но это работает.

Это сработало для меня (псевдокод):

public class SqlHelper
{
    public static final ArrayList<String>platformList = new ArrayList<>(Arrays.asList("iOS","Android","Windows","Mac"));

    public static final String testQuery = "select * from devices where platform_nm in (:PLATFORM_NAME)";
}

указать привязку:

public class Test extends NamedParameterJdbcDaoSupport
public List<SampleModelClass> runQuery()
{
    //define rowMapper to insert in object of SampleClass
    final Map<String,Object> map = new HashMap<>();
    map.put("PLATFORM_LIST",DeviceDataSyncQueryConstants.platformList);
    return getNamedParameterJdbcTemplate().query(SqlHelper.testQuery, map, rowMapper)
}

Другие вопросы по теме