Подготовлено заявление в пункте альтернативы?
каковы лучшие обходные пути для использования SQL IN
предложение с экземплярами java.sql.PreparedStatement
, который не поддерживается для нескольких значений из-за проблем безопасности атаки SQL injection: One ?
местозаполнитель представляет одно значение, а не список значений.
рассмотрим следующую инструкцию SQL:
SELECT my_column FROM my_table where search_column IN (?)
используя preparedStatement.setString( 1, "'A', 'B', 'C'" );
по существу неработающая попытка обойти причины использования ?
в первую очередь.
что обходные пути существуют?
27 ответов:
анализ различных доступных вариантов, а также плюсы и минусы каждого из них доступны здесь.
предлагаемые опции:
- готовить
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 значениями. Чтобы найти в списке 6 различных значений, заполните запрос size-10 так, чтобы он выглядел как
SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6)
. Любой приличный сервер оптимизирует повторяющиеся значения перед запуском запроса.ни один из этих вариантов не супер большой,.
дубликаты вопросов были даны ответы в этих местах с одинаково разумными альтернативами, но ни один из них не был супер великим:
- PreparedStatement со списком параметров в предложении IN
- Как установить список параметров на подготовленный заявление?
правильный ответ, если вы используете JDBC4 и сервер, который поддерживает
x = ANY(y)
использоватьPreparedStatement.setArray
как описано здесь:кажется, нет никакого способа сделать
setArray
работа со списками, хотя.
решение для 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(); }
не простой способ AFAIK. Если цель состоит в том, чтобы поддерживать высокий коэффициент кэша операторов (т. е. не создавать оператор на каждый счетчик параметров), вы можете сделать следующее:
создать отчет с несколькими (например, 10) параметры:
... ГДЕ А В (?,?,?,?,?,?,?,?,?,?) ...
привязать все фактические параметры
setString(1, " foo"); setString(2, "bar");
привязать остальные как 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++; }
неприятный обходной путь, но, безусловно, возможно использовать вложенный запрос. Создайте временную таблицу MYVALUES со столбцом в ней. Вставьте список значений в таблицу MYVALUES. Затем выполнить
select my_column from my_table where search_column in ( SELECT value FROM MYVALUES )
уродливая, но жизнеспособная альтернатива, если ваш список значений очень велик.
этот метод имеет дополнительное преимущество потенциально лучших планов запросов от оптимизатора (проверьте страницу для нескольких значений, tablescan только один раз вместо одного значения и т. д) может сэкономить на накладных расходах, если ваша база данных не кэширует подготовленные инструкции. Ваши "вставки" должны быть выполнены в пакетном режиме, и таблицу MYVALUES, возможно, потребуется настроить, чтобы иметь минимальную блокировку или другие высокозатратные защиты.
ограничения оператора in () - это корень всего зла.
Он работает для тривиальных случаев, и вы можете расширить его с помощью "автоматической генерации подготовленного утверждения", однако он всегда имеет свои пределы.
- если вы создаете оператор с переменным числом параметров, это сделает накладные расходы на синтаксический анализ sql при каждом вызове
- на многих платформах количество параметров оператора in() ограничено
- на всех платформах, общий размер текста SQL ограничен, что делает невозможным отправку 2000 заполнителей для In params
- отправка вниз привязать переменные 1000-10k не представляется возможным, так как драйвер JDBC имеет свои ограничения
подход in() может быть достаточно хорош для некоторых случаев, но не для ракетного доказательства:)
ракетостойкое решение состоит в том, чтобы передать произвольное количество параметров в отдельном вызове( например, передав clob params), а затем иметь представление (или любым другим способом), чтобы представить их в 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 для select стабилен
- нет сложность конвейерных функций-это всего лишь один запрос
- SQL использует простое соединение, а не оператор IN, что довольно быстро
- в конце концов, это хорошее правило для не попадание в базу данных с любым простым выбором или DML, так как это Oracle, который предлагает lightyears больше, чем MySQL или аналогичные простые движки баз данных. PL / SQL позволяет скрыть модель хранения из модели домена приложения в эффективной путь.
фокус вот в чем:
- нам нужен вызов, который принимает длинную строку, и хранить где-то, где сеанс БД может получить к нему доступ (например, простая переменная пакета или 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 со всеми параметрами в строке и преобразование его в представление является наиболее эффективным решением.
Я никогда не пробовал, но хотел .setArray() делать то, что вы ищете?
обновление: очевидно, нет. setArray только кажется, что работает с java.язык SQL.Массив, который поступает из столбца массива, полученного из предыдущего запроса, или подзапроса со столбцом массива.
мое решение:
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(?)))"
С уважением,
Хавьер Ибаньес
вот как я решил это в моем собственном приложении. В идеале, вы должны использовать 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 выше вместо конкретных чисел очень помогает, если вы решите изменить запрос позже.
Я полагаю, что вы могли бы (используя базовую манипуляцию строками) генерировать строку запроса в
PreparedStatement
ряд?
соответствует количеству элементов в вашем списке.конечно, если вы делаете это, вы всего в шаге от создания гигантской цепи
OR
в вашем запросе, но без правильного числа?
в строке запроса, я не вижу, как еще вы можете работать вокруг этого.
вы можете использовать метод 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();
попробуйте использовать функцию instr?
select my_column from my_table where instr(?, ','||search_column||',') > 0
затем
ps.setString(1, ",A,B,C,");
по общему признанию, это немного грязный хак, но он уменьшает возможности для инъекции sql. Все равно работает в oracle.
вместо
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 будут скомпилированы и сохранены на сервере баз данных
Я столкнулся с рядом ограничений, связанных с подготовленным заявлением:
- подготовленные операторы кэшируются только внутри одного сеанса (Postgres), поэтому он действительно будет работать только с пулом соединений
- множество различных подготовленных операторов, предложенных @BalusC, может привести к переполнению кэша, и ранее кэшированные операторы будут удалены
- запрос должен быть оптимизирован и показатели использования. Звучит очевидно, однако например ЛЮБОЙ МАССИВ...) заявление, предложенное @Boris в одном из лучших ответов, не может использовать индексы, и запрос будет медленным, несмотря на кэширование
- подготовленный оператор также кэширует план запроса, и фактические значения любых параметров, указанных в операторе, недоступны.
среди предложенных решений я бы выбрал то, которое не снижает производительность запросов и делает меньше запросов. Это будет #4 (пакетирование нескольких запросов) из @Don ссылка или указание нулевых значений для ненужных '?'знаки, предложенные @Vladimir Dyuzhev
вот полное решение на 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; } }
весной позволяет передача java.утиль.Списки для NamedParameterJdbcTemplate , который автоматизирует поколение (?, ?, ?, ..., ?), в зависимости от количества аргументов.
Для Oracle, этот блог обсуждается использование oracle.язык SQL.Массив (соединение.createArrayOf не работает с Oracle). Для этого вам нужно изменить свой оператор SQL:
SELECT my_column FROM my_table where search_column IN (select COLUMN_VALUE from table(?))
The функция таблицы oracle преобразует массив в таблица Как значение, используемое в
IN
заявление.
просто для полноты: пока набор значений не слишком велик, вы может также просто строка-построить оператор, как
... WHERE tab.col = ? OR tab.col = ? OR tab.col = ?
который затем можно передать в prepare (), а затем использовать setXXX () в цикле для установки всех значений. Это выглядит отвратительно, но многие "большие" коммерческие системы обычно делают такие вещи, пока они не достигают определенных для БД ограничений, таких как 32 КБ (я думаю, что это так) для операторов в Oracle.
конечно вам нужно обеспечить что набор никогда не будет неоправданно большим, или сделать ошибку захвата в том случае, если это так.
следуя идее Адама. Сделайте ваш подготовленный оператор сортировки выберите my_column из таблицы my_table, где search_column в (#) Создайте строку x и заполните ее числом "?,?,?"в зависимости от вашего списка значений Затем просто измените # в запросе для вашей новой строки x заполнить
создать строку запроса в PreparedStatement, чтобы иметь число ?'S соответствует количеству элементов в вашем списке. Вот пример:
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; }
существуют различные альтернативные подходы, которые мы можем использовать для предложения IN в PreparedStatement.
- использование одиночных запросов-самая низкая производительность и ресурсоемкость
- использование StoredProcedure-самый быстрый, но специфичный для базы данных
- создание динамического запроса для PreparedStatement-хорошая производительность, но не получает преимущества кэширования и PreparedStatement перекомпилируется каждый раз.
использовать NULL в запросах PreparedStatement - Оптимальная производительность, отлично работает, когда вы знаете предел в аргументах предложения. Если нет ограничений, то вы можете выполнять запросы в пакетном режиме. Пример фрагмента кода;
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); }
вы можете проверить более подробную информацию об этих альтернативных подходах здесь.
в некоторых ситуациях регулярное выражение может помочь. Вот пример, который я проверил на Oracle, и он работает.
select * from my_table where REGEXP_LIKE (search_column, 'value1|value2')
но есть ряд недостатков это:
- любой столбец, который он применил, должен быть преобразован в varchar / char, по крайней мере неявно.
- нужно быть осторожным со специальными символами.
- это может замедлить производительность - в моем случае в версии используется сканирование индекса и диапазона, а версия 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" в вашем запросе в любом месте. Вы можете использовать комбинацию специальных символов и алфавитов вместо слова "PARAM", чтобы убедиться, что нет возможности такого слова в запросе. Надеюсь, вы получили решение.
Примечание: хотя это не подготовленный запрос, он выполняет работу, которую я хотел, чтобы мой код сделал.
просто для полноты и потому, что я не видел, чтобы кто-то еще предлагал это:
перед реализацией любого из сложных предложений выше рассмотрите, действительно ли SQL-инъекция является проблемой в вашем сценарии.
во многих случаях значение, предоставленное IN (...)- это список идентификаторов, которые были сгенерированы таким образом, что вы можете быть уверены, что инъекция невозможна... (например, результаты предыдущего выбора some_id из some_table, где some_condition.)
Если в этом случае вы можете просто объединить это значение и не использовать службы или подготовленный оператор для него или использовать их для других параметров этого запроса.
query="select f1,f2 from t1 where f3=? and f2 in (" + sListOfIds + ");";
PreparedStatement не предоставляет никакого хорошего способа справиться с SQL в предложении. За 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 */ }
мой обходной путь (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
- это массив, который содержит входной/ключи/поля и т. д.