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


343

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

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

SELECT my_column FROM my_table where search_column IN (?)

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

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


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

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

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


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

Ответы:


194

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

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

  • Подготовьте 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 различными значениями, заполните запрос на размер 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метода.


123

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

1
выглядит хорошо. какая часть этого кода специфична для PostreSQL? "где search_column = ANY (?)"? или connection.createArrayOf? или что-то другое?
Дэвид Портабелла

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

3
Если .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

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

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

19

Нет простого способа 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++;
}

«NULL никогда ничего не соответствует» - будет ли NULLв запросе совпадать со NULLзначением в базе данных?
Крейг МакКуин

5
@CraigMcQueen Нет, не будет. Нуль даже не соответствует нулю, согласно стандарту ANSI.
Дауд ибн Карим,

Вы можете сопоставить NULL, используя ключевое слово IS NULL. Хороший способ обнаружить строки, которых нет в объединенной таблице, - использовать LEFT JOIN вместе с IS NULL. 'ВЫБЕРИТЕ a.URL, b.URL ИЗ TABLE_A a СЛЕДУЮЩЕЕ СОЕДИНЕНИЕ TABLE_B b ON a_A.URL = b_B.URL WHERE b.URL IS NULL' Это покажет все строки в таблице A, которые не совпадают в таблице B.
Йенс Тандстад

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

Или вы можете установить все дополнительные параметры в значение любого предыдущего параметра. Любой приличный движок БД отфильтрует их. Так 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всегда false).
Руслан Стельмаченко

11

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

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

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

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


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

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

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

9

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

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

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


6

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

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

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


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

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

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… Согласно Sun, содержимое массива [обычно] остается на стороне сервера и извлекается по мере необходимости. PreparedStatement.setArray () может отправлять обратно массив из предыдущего ResultSet, но не создавать новый массив на стороне клиента.
Крис Маццола

5

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

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.
Джи Би

3

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

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


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

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

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

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

3

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

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

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

3

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

2

Вот полное решение на 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;

    }

}

2

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заявлении.


1

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

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

затем

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

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


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

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

1

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


1

Вместо того, чтобы использовать

SELECT my_column FROM my_table where search_column IN (?)

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

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

и

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

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


1

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

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

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


1

Я только что разработал для этого специфичную для 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($1, ',')::text[]);

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

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

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

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


0

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

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

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

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


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

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

0

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


0

Сгенерируйте строку запроса в 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;
}

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

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

@ neu242 Разве вы не можете использовать ps.setObject(1,items)вместо перебора списка, а затем установить paramteres?
Неха Чоудхари

0

Существуют различные альтернативные подходы, которые мы можем использовать для предложения 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, поскольку ограниченный / динамический ввод ограничивается запятой.
Брэндон

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

0

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

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

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

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

0

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

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

    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», чтобы исключить вероятность появления такого слова в запросе. Надеюсь, у вас есть решение.

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


0

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

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

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

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

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

0

PreparedStatement не предоставляет хорошего способа работы с предложением SQL IN. Согласно 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);

0

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, в которых строка запроса строится вручную.


0

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

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

0

Мой пример для баз данных SQLite и Oracle.

Первый цикл For предназначен для создания объектов PreparedStatement.

Второй цикл For предназначен для предоставления значений для параметров PreparedStatement.

List<String> roles = Arrays.asList("role1","role2","role3");
Map<String, String> menu = getMenu(conn, roles);

public static Map<String, String> getMenu(Connection conn, List<String> roles ) {
    Map<String, String> menu = new LinkedHashMap<String, String>();

    PreparedStatement stmt;
    ResultSet rset;
    String sql;
    try {
        if (roles == null) {
            throw new Exception();
        }
        int size = roles.size();
        if (size == 0) {
            throw new Exception("empty list");
        }
        StringBuilder sb = new StringBuilder();
        sb.append("select page_controller, page_name from pages "
                + " where page_controller in (");
        for (int i = 0; i < size; i++) {
            sb.append("?,");
        }
        sb.setLength(sb.length() - 1);
        sb.append(") order by page_id");
        sql = sb.toString();
        stmt = conn.prepareStatement(sql);
        for (int i = 0; i < size; i++) {
            stmt.setString(i + 1, roles.get(i));
        }
        rset = stmt.executeQuery();
        while (rset.next()) {
            menu.put(rset.getString(1), rset.getString(2));
        }

        conn.close();
    } catch (Exception ex) {
        logger.info(ex.toString());
        try {
            conn.close();
        } catch (SQLException e) {
        }
        return menu;
    }
    return menu;
}

-3

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.