Альтернативи пункту "ReadyStatement IN"?


343

Які найкращі шляхи вирішення для використання INпункту SQL з екземплярами 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 можна скласти один раз для ефективності. Здійснюючи застереження в динамічному, це фактично нівелює підготовлене твердження.

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 (?,?,?)та виконайте його. Потрібна одна підготовлена ​​заява на список розміру-IN. Швидкий і очевидний.
  • Підготуйте 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-списку; скажімо, зі значеннями 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 (?)"? чи з'єднання.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працює з a 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 Ні, це не буде. Null не відповідає навіть null, відповідно до стандарту ANSI.
Давуд ібн Карім

Ви можете зіставити NULL, використовуючи ключове слово IS NULL. Хороший спосіб виявити рядки, які не існують у об'єднаній таблиці, - це використовувати лівий приєднання разом із IS NULL. 'ВИБІР a.URL, b.URL ВІД ТАБЛИ_А ЛІВНЕ ПРИЄДНАЙТЕСЬ ТАБЛО_B b НА a_A.URL = b_B.URL, БЕ B.URL NULL' Це покаже всі рядки таблиці A, які не відповідають таблиці B.
Jens Tandstad

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завжди хибно).
Руслан Стельмаченко

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 під час кожного виклику
  • на багатьох платформах кількість параметрів оператора в () обмежена
  • на всіх платформах загальний розмір тексту SQL обмежений, що унеможливлює надсилання 2000 заповнювачів на парами
  • надсилання змінних зв'язування 1000-10k неможливо, оскільки у драйвера JDBC є свої обмеження

Підхід в () може бути досить хорошим у деяких випадках, але не є ракетним доказам :)

Ракетно-стійке рішення полягає в тому, щоб передати довільну кількість параметрів в окремий виклик (наприклад, передавши клавішу парам, наприклад), а потім мати подання (або будь-яким іншим способом) для представлення їх у SQL та використання у вашому критерії.

Варіант жорстокої сили тут http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

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

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

Тоді ви можете передати довільну кількість відомих комами ідентифікаторів клієнта в параметрі та:

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

Трюк тут:

  • нам потрібен виклик, який приймає довгу рядок, і зберігаємо десь там, де сеанс DB може отримати доступ до нього (наприклад, проста змінна пакунок або dbms_session.set_context)
  • тоді нам потрібен вид, який може розібрати це до рядків
  • і тоді у вас є представлення, яке містить ідентифікатори, які ви запитуєте, тому все, що вам потрібно, - це просте приєднання до запиту таблиці.

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

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

де aux_in_list.getpayload посилається на початковий рядок введення.


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


6

Ось як я вирішив це у власній заявці. В ідеалі вам слід використовувати StringBuilder замість + для Strings.

    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
Не працює з усіма базами даних, але це "правильний" підхід.
skaffman

Ви маєте на увазі всіх водіїв. Деякі водії мають власні еквіваленти цьогорічного (минулого століття?) Стандарту. Інший спосіб - зав'язати партію значень у тимчасову таблицю, але не всі бази даних підтримують це ...
Том Хоутін - пропуск

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.
Gee Bee

3

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

Звичайно, якщо ви це робите, ви знаходитесь лише в кроці від створення гігантського ланцюжка ORу вашому запиті, але не маючи потрібної кількості ?в рядку запиту, я не бачу, як ще можна обійтись.


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

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

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

Білл, це рішення є корисним, якщо я не хочу повторно використовувати PreparedStatement. Ще одне рішення полягає в тому, щоб зробити один виклик парами кілька разів і накопичити результати на стороні клієнта. Ймовірно, було б ефективнішим створити / виконати нову заяву із власною кількістю? кожен раз, хоча.
Кріс Маццола

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

Весна дозволяє передавати 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(?))

Функція таблиці oracle перетворює пропущений масив у таку таблицю, як значення, що може бути використане в 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 як

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

і

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

або використовувати збережену процедуру, це було б найкращим рішенням, оскільки оператори sql будуть складені та збережені на сервері DataBase


1

Я зіткнувся з низкою обмежень, пов'язаних з підготовленою заявою:

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

Серед запропонованих рішень я вибрав би те, що не знижує продуктивність запитів і робить меншу кількість запитів. Це буде №4 (пакет декількох запитів) за посиланням @Don або вказує значення NULL для непотрібних "?" марки, як запропонував @ Володимир Дюжев


1

Я щойно розробив для цього варіант PostgreSQL. Це трохи хак, і він має свої плюси і мінуси та обмеження, але він, здається, працює і не обмежується певною мовою розробки, платформи чи PG.

Хитрість, звичайно, полягає в тому, щоб знайти спосіб передати довільну довжину колекції значень як єдиний параметр і дозволити db розпізнати його як кілька значень. Я працюю над рішенням - побудувати розділену рядок зі значень колекції, передавати її як єдиний параметр і використовувати 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[]);

Знову ж таки, без сумніву, це хак, але він працює і дозволяє все-таки використовувати заздалегідь складені підготовлені заяви, які приймають * ах * дискретні параметри, із супутніми безпеками та (можливо) перевагами для продуктивності. Це доцільно і справді виконати? Звичайно, це залежить, оскільки у вас розбір рядків і можливо кастинг триває до того, як ваш запит навіть запуститься. Якщо ви розраховуєте надіслати три, п'ять, кілька десятків значень, звичайно, це, мабуть, добре. Кілька тисяч? Так, може, не так вже й багато. YMMV, обмеження та виключення застосовуються, жодних явних та явних гарантій.

Але це працює.


0

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

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

який ви можете потім передати, щоб підготувати (), а потім використовувати setXXX () у циклі, щоб встановити всі значення. Це виглядає приємно, але багато "великих" комерційних систем звичайно роблять подібні дії, поки вони не досягнуть специфічних для БД лімітів, таких як 32 КБ (я думаю, що це є) для заяв в Oracle.

Звичайно, вам потрібно переконатися, що набір ніколи не буде необґрунтовано великим, або робити помилки з помилками в тому випадку, якщо він є.


Так, ви праві. Моєю метою в даному випадку було повторне використання Підготовленої статті з різною кількістю предметів кожного разу.
Кріс Маццола

3
Використання "АБО" пригнічує наміри. Дотримуйтесь "IN", оскільки його легше читати, а наміри - більш зрозумілі. Єдина причина перемикання - це якщо плани запитів були іншими.
Джеймс Шек

0

Слідом за ідеєю Адама. Зробіть для підготовленого оператора свого роду виберіть my_column з my_table, де search_column у (#) Створіть рядок x та заповніть його числом "?,?,?" залежно від списку значень. Тоді просто змініть # у запиті для нового String x a populate


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?
Neha Choudhary

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

У деяких ситуаціях може допомогти regexp. Ось приклад, який я перевірив на Oracle, і він працює.

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

Але в цьому є ряд недоліків:

  1. Будь-який стовпець, який він застосував, має бути принаймні неявно перетворений у varchar / char.
  2. Потрібно бути обережними зі спеціальними символами.
  3. Це може уповільнити продуктивність - у моєму випадку версія IN використовує сканування індексу та діапазону, а версія REGEXP - повне сканування.

0

Вивчивши різні рішення на різних форумах і не знайшовши хорошого рішення, я відчуваю, що наступний хак, який я придумав, - це найпростіший підхід і кодування:

Приклад: припустимо, у вас є кілька параметрів для передачі в пункті "IN". Просто покладіть манекенну рядок всередині пункту "IN", скажімо, "PARAM" позначає список параметрів, які будуть надходити на місце цієї манекенової рядки.

    select * from TABLE_A where ATTR IN (PARAM);

Ви можете зібрати всі параметри в одну змінну String у вашому коді Java. Це можна зробити так:

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

Ви можете додати всі свої параметри, розділені комами, в одну змінну String, '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", щоб переконатися, що немає можливості такого слова входити в запит. Сподіваюся, ви отримали рішення.

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


0

Просто для повноти і тому, що я не бачив, щоб хто-небудь запропонував це:

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

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

У такому випадку ви можете просто об'єднати це значення і не використовувати сервіси або підготовлену заяву для нього або використовувати їх для інших параметрів цього запиту.

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

Це рішення краще, ніж інші некрасиві рішення циклу, а рядок запитів будується за допомогою ручних ітерацій


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 - це створення об'єктів ReadyStatement.

Другий цикл 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.