PreparedStatement IN 子句替代方案?

PreparedStatement IN clause alternatives?

提问人:Chris Mazzola 提问时间:10/7/2008 最后编辑:JonasChris Mazzola 更新时间:1/16/2023 访问量:378682

问:

将 SQL 子句与 实例一起使用的最佳解决方法是什么,由于 SQL 注入攻击安全问题,多个值不支持该实例: 一个占位符表示一个值,而不是值列表。INjava.sql.PreparedStatement?

请考虑以下 SQL 语句:

SELECT my_column FROM my_table where search_column IN (?)

使用本质上是一种非工作尝试,旨在解决首先使用的原因。preparedStatement.setString( 1, "'A', 'B', 'C'" );?

有哪些解决方法?

Java 安全性 JDBC 准备语句 子句内

评论

2赞 Chris Mazzola 7/9/2009
奥斯卡,如果您需要 IN 子句,我认为动态生成 (?,?,....) 是最简单的解决方法,但我将其留给单个调用,因为性能在我的特定情况下已经足够了。
7赞 1/9/2010
预准备语句的优点之一是 sohuld 可以编译一次以提高效率。通过使 in 子句动态化,这有效地否定了准备好的语句。
2赞 Frans 7/17/2012
实际上,这适用于MySQL(使用setObject将String数组设置为参数值)。你用的是什么数据库?
0赞 Peter Hart 3/9/2013
这是 Oracle 特有的答案
0赞 Lukas Eder 11/11/2013
这是一个相关的问题:stackoverflow.com/q/6956025/521799

答:

5赞 Paul Tomblin 10/7/2008 #1

我从未尝试过,但是.setArray()会做你要找的吗?

更新:显然不是。 setArray 似乎仅适用于来自您从上一个查询中检索到的 ARRAY 列或具有 ARRAY 列的子查询的 java.sql.Array。

评论

4赞 skaffman 10/7/2008
不适用于所有数据库,但这是“正确”的方法。
0赞 Tom Hawtin - tackline 10/7/2008
你是说所有司机。一些驱动程序具有这一年(上个世纪?)标准的专有等效项。另一种方法是将一批值打包到一个临时表中,但并非所有数据库都支持......
0赞 Chris Mazzola 10/10/2008
java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/......根据 Sun 的说法,Array 内容 [通常] 保留在服务器端,并根据需要进行拉取。PreparedStatement.setArray() 可以从以前的 ResultSet 发回一个 Array,而不是在客户端创建一个新的 Array。
3赞 Adam Bellaire 10/7/2008 #2

我想您可以(使用基本的字符串操作)在 中生成查询字符串,使其数字与列表中的项目数相匹配。PreparedStatement?

当然,如果你这样做,你离在查询中生成一个巨大的链子只有一步之遥,但是在查询字符串中没有正确数量的 ,我看不出你还能如何解决这个问题。OR?

评论

0赞 Chris Mazzola 10/7/2008
对我来说并不是一个真正的解决方案,因为我想发送不同数量的?每次我打电话给ps。但不要以为我没有考虑过。:P
5赞 Bill Karwin 10/8/2008
另一个技巧:你可以使用大量的参数占位符 - 与你将拥有的最长值列表一样多 - 如果你的值列表更短,你可以重复值:...其中 SEARCHFIELD 在 (?, ?, ?, ?, ?, ?, ?, ?) 中,然后提供值:A、B、C、D、A、B、C、D
1赞 Bill Karwin 10/8/2008
但总的来说,我赞成 Adam 的解决方案:动态生成 SQL,并连接?占位符以匹配必须传递的值数。
0赞 Chris Mazzola 10/10/2008
Bill,如果我不想重用 PreparedStatement,该解决方案是可行的。另一种解决方案是多次调用单个参数,并在客户端累积结果。构建/执行自定义编号为 ?不过每次。
1赞 stjohnroe 10/7/2008 #3

尝试使用 INSTR 函数?

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

然后

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

诚然,这有点肮脏,但它确实减少了 sql 注入的机会。无论如何都可以在 oracle 中工作。

评论

0赞 stjohnroe 10/8/2008
哦,我知道它不会使用索引
0赞 David Portabella 6/4/2012
它不适用于某些字符串,例如,如果字符串包含“,”。
0赞 Carl Smotricz 10/7/2008 #4

只是为了完整起见:只要值集不是太大,你也可以简单地字符串构造一个语句,比如

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

然后,您可以将其传递给 prepare(),然后在循环中使用 setXXX() 来设置所有值。这看起来很恶心,但许多“大型”商业系统通常会做这种事情,直到它们达到特定于数据库的限制,例如 Oracle 中的语句为 32 KB(我认为是)。

当然,您需要确保集合永远不会过大,或者在它太大的情况下进行错误捕获。

评论

0赞 Chris Mazzola 10/10/2008
是的,你是对的。在这种情况下,我的目标是每次都对不同数量的项目重复使用 PreparedStatement。
4赞 James Schek 10/10/2008
使用“OR”会混淆意图。坚持使用“IN”,因为它更易于阅读,意图更明确。切换的唯一原因是查询计划是否不同。
0赞 Yorch 10/7/2008 #5

遵循亚当的想法。使准备好的语句从my_table search_column in (#) 中选择 my_column 创建一个字符串 x 并用数字“?,?,?”填充它,具体取决于您的值列表 然后,只需在查询中更改新字符串 x 的 # 即可填充

10赞 James Schek 10/8/2008 #6

一个令人不快但肯定可行的解决方法是使用嵌套查询。创建一个包含列的临时表 MYVALUES。将值列表插入到 MYVALUES 表中。然后执行

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

丑陋,但如果你的值列表非常大,这是一个可行的选择。

这种技术还有一个额外的优点,即优化器可能提供更好的查询计划(检查一个页面是否有多个值,表只扫描一次,而不是每个值一次,等等),如果数据库不缓存准备好的语句,则可以节省开销。您的“INSERTS”需要批量完成,并且可能需要调整 MYVALUES 表以具有最小的锁定或其他高开销保护。

评论

0赞 Paul Tomblin 10/10/2008
与一次查询一个值相比,这有什么优势my_table?
3赞 James Schek 10/10/2008
查询优化器可以通过从加载的页面中检索所有可能的匹配项来减少 I/O 负载。表扫描或索引扫描可以执行一次,而不是每个值执行一次。插入值的开销可以通过批处理操作来减少,并且可能小于多个查询。
1赞 David Portabella 6/4/2012
它看起来不错,但并发性可能存在问题。JDBC 规范是否包含一种在内存中创建临时匿名表的方法?或者类似的东西,如果可能的话,不是特定于 jdbc-vendor 的?
0赞 Christopher Schultz 5/27/2023
@DavidPortabella 嘿,已经 11 年了,但临时表只对当前连接可见,所以只要你只对单个(逻辑)事务使用单个连接,你就应该没问题。您可能希望每次都完全安全。DROP TABLE myvalues IF EXISTSCREATE TEMPORARY TABLE myvalues
20赞 Vladimir Dyuzhev 10/10/2008 #7

没有简单的方法AFAIK。 如果目标是保持较高的语句缓存率(即不为每个参数计数创建语句),则可以执行以下操作:

  1. 创建一个包含几个(例如 10 个)参数的语句:

    ...其中 A 在 (?,?,?,?,?,?,?,?,?,?) ...

  2. 绑定所有实际参数

    setString(1,“foo”); setString(2,“bar”);

  3. 将其余部分绑定为 NULL

    setNull(3,类型.VARCHAR) ... setNull(10,类型.VARCHAR)

NULL 从不匹配任何内容,因此它由 SQL 计划生成器优化。

当您将 List 传递到 DAO 函数时,该逻辑很容易自动化:

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

while( i < MAX_PARAMS ) {
  ps.setNull(i+1,Types.VARCHAR);
  i++;
}

评论

0赞 Craig McQueen 10/14/2013
“NULL 从不匹配任何内容” — 查询中的值是否与数据库中的值匹配?NULLNULL
5赞 Dawood ibn Kareem 2/12/2014
@CraigMcQueen 不,不会。根据 ANSI 标准,Null 甚至与 null 不匹配。
0赞 Jens Tandstad 11/11/2014
可以使用 IS NULL 关键字匹配 NULL。检测联接表中不存在的行的一个好方法是将 LEFT JOIN 与 IS NULL 一起使用。'SELECT a.URL, b.URL FROM TABLE_A a LEFT JOIN TABLE_B b ON a_A.URL = b_B.URL WHERE b.URL is NULL' 这将显示表 A 中与表 B 中没有匹配项的所有行。
6赞 Brandon 6/10/2016
不过要小心。 并且不要以相同的方式处理 null。运行这个,看看会发生什么:然后删除并观看魔术。NOT ININselect 'Matched' as did_it_match where 1 not in (5, null);null
0赞 Ruslan Stelmachenko 8/8/2018
或者,您可以将所有额外的参数设置为任何前一个参数的值。任何像样的数据库引擎都会将它们过滤掉。所以和 一样。它也适用于 intained(它总是不返回任何行,因为总是 false)。a IN (1,2,3,3,3,3,3)a IN (1,2,3)NOT INa NOT IN (1,2,3,null,null,null,null)any_value != NULL
223赞 Dónal 10/10/2008 #8

对各种可用选项的分析以及每个选项的优缺点可在 JavaRanch Journal 上 Jeanne Boyarsky 的 Batching Select Statements in JDBC 条目中找到。

建议的选项包括:

  • 准备,为每个值执行它,并在客户端对结果执行 UNION。只需要一个准备好的语句。缓慢而痛苦。SELECT my_column FROM my_table WHERE search_column = ?
  • 准备并执行它。每个 size-of-IN 列表需要一个准备好的语句。快速而明显。SELECT my_column FROM my_table WHERE search_column IN (?,?,?)
  • 准备并执行它。[或者用分号代替那些分号。每个 size-of-IN 列表需要一个准备好的语句。愚蠢的慢,严格来说比 ,所以我不知道博主为什么会建议它。SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...UNION ALLWHERE search_column IN (?,?,?)
  • 使用存储过程构造结果集。
  • 准备 N 个不同大小的 IN-List 查询;比如说,有 2、10 和 50 个值。要搜索具有 6 个不同值的 IN 列表,请填充 size-10 查询,使其看起来像 。任何像样的服务器都会在运行查询之前优化掉重复的值。SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6)

这些选项都不是理想的。

如果您使用的是 JDBC4 和支持 的服务器,最好的选择是按照 Boris 的 anwser 中的描述使用。x = ANY(y)PreparedStatement.setArray

不过,似乎没有任何方法可以与 IN 列表一起使用。setArray


有时 SQL 语句在运行时加载(例如,从属性文件加载),但需要可变数量的参数。在这种情况下,请首先定义查询:

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

接下来,加载查询。然后在运行之前确定参数的数量。知道参数计数后,运行:

sql = any( sql, count );

例如:

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

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

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

对于某些不支持通过 JDBC 4 规范传递数组的数据库,此方法有助于将慢速子句条件转换为较快子句条件,然后可以通过调用该方法进行扩展。= ?IN (?)any

评论

0赞 Andy Thomas 5/29/2021
另一种选择是,如果列表的大小不经常更改 -- 为输入列表的最后一个大小准备并缓存一个语句。在每个后续查询中,如果大小相同,请重用预准备语句,否则,关闭它并创建另一个语句。
1赞 neu242 12/16/2009 #9

在 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赞 neu242 12/18/2009
无需再使用 StringBuilder。无论如何,编译器都会将 + 符号转换为 StringBuilder.append(),因此不会对性能造成影响。试试自己:)
5赞 A.H. 11/29/2012
@neu242:哦,是的,编译器使用 .但不是你想的那样。反编译,您可以看到,每次循环迭代都会分配两个新的,并在每个循环迭代上调用两个新循环。优化只捕获类似的东西,但不会超出一个表达式。StringBuildergenerateQsForInStringBuildertoStringStringBuilder"x" + i+ "y" + j
0赞 Neha Choudhary 7/17/2013
@neu242 你不能使用而不是遍历列表,然后设置 ?ps.setObject(1,items)paramteres
5赞 Javier Ibanez 2/24/2011 #10

我的解决方法是:

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

问候

哈维尔·伊巴涅斯

评论

0赞 Gee Bee 2/7/2018
这是 PL/SQL,是的。它不适用于其他数据库。请注意,此实现具有输入参数的限制(总长度限制为 32k 个字符)以及性能限制,因为对流水线函数的调用会在 PL/SQL 和 Oracle 的 SQL 引擎之间进行上下文切换。
1赞 Jeff Miller 11/22/2011 #11

Sormula 通过允许您提供 java.util.Collection 对象作为参数来支持 SQL IN 运算符。它创建一个带有 ?对于集合的每个元素。请参阅示例 4(示例中的 SQL 是一个注释,用于阐明 Sormula 创建但未使用的内容)。

141赞 Boris 4/20/2012 #12

PostgreSQL的解决方案:

final PreparedStatement statement = connection.prepareStatement(
        "SELECT my_column FROM my_table where search_column = ANY (?)"
);
final String[] values = getValues();
statement.setArray(1, connection.createArrayOf("text", values));

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

final PreparedStatement statement = connection.prepareStatement(
        "SELECT my_column FROM my_table " + 
        "where search_column IN (SELECT * FROM unnest(?))"
);
final String[] values = getValues();
statement.setArray(1, connection.createArrayOf("text", values));

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

评论

1赞 David Portabella 6/4/2012
看起来不错。此代码的哪一部分是特定于 PostreSQL 的?“where search_column = ANY(?)”?还是connection.createArrayOf?还是别的什么?
1赞 lvella 7/19/2012
由于该部分的原因,我认为它比特定于 PostgreSQL 的 JDBC4 更特定于 JDBC4,但我不确定用户的严格语义是否由 JDBC 规范定义。.createArrayOf()Array
6赞 ADTC 8/1/2013
如果不起作用,您可以自己手动创建数组文字,例如(请注意,“B”有一个空格,而 C 没有),然后是准备好的语句是 or 。(PS:我不认为适用于 ..createArrayOfString arrayLiteral = "{A,\"B \", C,D}"statement.setString(1,arrayLiteral)... IN (SELECT UNNEST(?::VARCHAR[]))... IN (SELECT UNNEST(CAST(? AS VARCHAR[])))ANYSELECT
0赞 Emmanuel Touzery 8/26/2013
很棒的解决方案!真的为我挽救了这一天。对于整数数组,我在 createArrayOf() 的第一个参数中使用了“int”,它看起来不错。不过,根据文档,第一个参数似乎是特定于数据库的。
2赞 aureianimus 3/4/2014
这似乎是最干净的解决方案。如果有人正在寻找HSQLDB特定的语法:我设法让它与IN(UNNEST(?)一起使用
1赞 kapil das 8/25/2013 #13

而不是使用

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赞 Alexander 9/12/2013 #14

我遇到了一些与准备好的声明相关的限制:

  1. 预准备语句仅缓存在同一会话 (Postgres) 中,因此它实际上仅适用于连接池
  2. @BalusC 提出的许多不同的预准备语句可能会导致缓存过满,并且以前缓存的语句将被删除
  3. 必须优化查询并使用索引。听起来很明显,但是例如,@Boris在顶级答案之一中提出的 ANY(ARRAY...) 语句不能使用索引,并且尽管缓存,查询速度会很慢
  4. 预准备语句也会缓存查询计划,并且语句中指定的任何参数的实际值都不可用。

在建议的解决方案中,我会选择不会降低查询性能且查询次数较少的解决方案。这将是 @Don 链接中的 #4(批处理几个查询)或为 @Vladimir Dyuzhev 提议的不需要的“?”标记指定 NULL 值

0赞 Pankaj 1/26/2014 #15

我们可以对 PreparedStatement 中的 IN 子句使用不同的替代方法。

  1. 使用单个查询 - 性能最慢且占用大量资源
  2. 使用 StoredProcedure - 最快但特定于数据库
  3. 为 PreparedStatement 创建动态查询 - 性能良好,但未获得缓存的好处,并且每次都会重新编译 PreparedStatement。
  4. 在 PreparedStatement 查询中使用 NULL - 最佳性能,当您知道 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);
        }
    

您可以在此处查看有关这些替代方法的更多详细信息。

评论

0赞 Brandon 7/11/2014
“为 PreparedStatement 创建动态查询 - 性能良好,但无法获得缓存的好处,并且每次都会重新编译 PreparedStatement。” 缓存和避免重新编译是使准备好的语句表现良好的原因。因此,我不同意你的说法。但是,这将阻止 SQL 注入,因为您将连接/动态输入限制为逗号。
0赞 Pankaj 7/11/2014
我同意你的看法,但是这里的“良好性能”是针对这种特定情况的。它比方法 1 性能更好,但方法 2 最快。
0赞 Vasili 1/2/2015 #16

在某些情况下,正则表达式可能会有所帮助。 这是我在 Oracle 上检查过的一个示例,它有效。

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

但它有许多缺点:

  1. 它应用的任何列都应该转换为 varchar/char,至少是隐式的。
  2. 需要小心特殊字符。
  3. 它会降低性能 - 在我的情况下,IN 版本使用索引和范围扫描,而 REGEXP 版本进行完全扫描。
0赞 bnsk 3/15/2015 #17

在检查了不同论坛中的各种解决方案但没有找到一个好的解决方案后,我觉得我想出的以下技巧是最容易遵循和编码的:

示例:假设您有多个参数要在“IN”子句中传递。只需在“IN”子句中放置一个虚拟字符串,例如,“PARAM”表示将代替此虚拟字符串的参数列表。

    select * from TABLE_A where ATTR IN (PARAM);

您可以在 Java 代码中将所有参数收集到单个 String 变量中。这可以按如下方式完成:

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

在我们的例子中,您可以将所有参数附加到一个字符串变量“param1”中,用逗号分隔。

将所有参数收集到单个 String 中后,只需将查询中的虚拟文本(在本例中为“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赞 epeleg 4/2/2015 #18

只是为了完整起见,因为我没有看到其他人提出建议:

在实现上述任何复杂建议之前,请考虑 SQL 注入在您的方案中是否确实是一个问题。

在许多情况下,提供给 IN (...) 的值是一个 ID 列表,这些 ID 是以一种可以确保无法注入的方式生成的......(例如,从some_table some_condition的上一个选择some_id的结果。

如果是这种情况,您可以只连接此值,而不使用服务或预准备语句,或者将它们用于此查询的其他参数。

query="select f1,f2 from t1 where f3=? and f2 in (" + sListOfIds + ");";
2赞 dwjohnston 4/20/2015 #19

下面是 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;

    }

}
3赞 Dr. Hans-Peter Störr 6/1/2015 #20

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

oracle 表函数将传递的数组转换为可在语句中使用的类似表的值。IN

9赞 Gee Bee 2/17/2016 #21

in() 运算符的局限性是万恶之源。

它适用于微不足道的情况,您可以通过“自动生成准备好的语句”来扩展它,但它总是有其局限性。

  • 如果要创建具有可变参数数的语句,则每次调用时都会产生 SQL 解析开销
  • 在许多平台上,in() 运算符的参数数量是有限的
  • 在所有平台上,总 SQL 文本大小都是有限的,因此不可能为 in 参数发送 2000 个占位符
  • 无法向下发送 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;

然后,您可以在参数中传递任意数量的逗号分隔的客户 ID,并且:

  • 不会有解析延迟,因为 select 的 SQL 是稳定的
  • 没有流水线函数的复杂性 - 它只是一个查询
  • SQL使用简单的连接,而不是IN运算符,这非常快
  • 毕竟,这是一个很好的经验法则,不要使用任何普通的选择或DML来攻击数据库,因为它是Oracle,它提供了比MySQL或类似的简单数据库引擎更多的光年。PL/SQL 允许您以有效的方式从应用程序域模型中隐藏存储模型。

这里的诀窍是:

  • 我们需要一个接受长字符串的调用,并将其存储在数据库会话可以访问它的地方(例如,简单的包变量或dbms_session.set_context)
  • 然后我们需要一个视图,可以将其解析为行
  • 然后,您有一个包含要查询的 ID 的视图,因此您只需要对查询的表进行简单的联接即可。

视图如下所示:

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 并在视图中进行转换是最有效的解决方案。

7赞 m.sabouri 4/8/2016 #22

以下是我在自己的应用程序中解决它的方法。理想情况下,应使用 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 之类的变量而不是具体数字会很有帮助。

3赞 Panky031 6/10/2016 #23

您可以使用此 javadoc 中提到的 setArray 方法:

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赞 unnamed 10/26/2017
并非所有驱动程序都支持此功能,如果不支持该功能,则会收到 SQLFeatureNotSupportedException
0赞 EdXX 1/13/2018
不幸的是,我的驱动程序不支持它
-3赞 smooth_smoothie 2/2/2017 #24

我的解决方法 (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是包含输入/键/字段等的数组

0赞 pedram bashiri 6/8/2017 #25

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);
1赞 Raheel 3/1/2018 #26

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 循环解决方案,其中查询字符串是通过手动迭代构建的

评论

1赞 static void main 8/7/2021
.map(e -> “?”)。collect(Collectors.joining(“, ”)
15赞 Gurwinder Singh 4/29/2018 #27

您可以使用以下命令生成占位符集合并连接它们:Collections.nCopiesString.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
     */
}

评论

0赞 jansohn 3/6/2020
似乎是迄今为止使用Oracle JDBC的最佳解决方案...
0赞 Andy Thomas 5/29/2021
如果要生成特定于一组参数的新 SQL 语句,为什么要使用占位符呢?
4赞 Gurwinder Singh 5/29/2021
@AndyThomas避免SQL注入
0赞 Andy Thomas 5/30/2021
@GurwinderSingh - 啊,好点子。谢谢你的课程!
0赞 static void main 8/7/2021
我正在尝试ps.close();,听说有必要;但很抱歉没有找到这样做的方法,你能说出来吗?
1赞 Joel Fouse 1/11/2019 #28

我刚刚为此制定了一个特定于 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,限制和排除适用,不作任何明示或暗示的保证。

但它有效。

-1赞 Nikita Shah 1/15/2020 #29

这对我有用(伪装代码):

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)
}
1赞 Lukas Eder 4/22/2021 #30

似乎还没有其他人建议使用现成的查询构建器,例如 jOOQQueryDSL,甚至是开箱即用管理动态 IN 列表Criteria Query,可能包括管理所有可能出现的边缘情况,例如:

  • 遇到 Oracle 每个列表最多 1000 个元素(与绑定值的数量无关)IN
  • 遇到任何驱动程序的最大绑定值数,我已在此答案中记录了这一点
  • 遇到游标缓存争用问题,因为太多不同的 SQL 字符串被“硬解析”并且执行计划无法再缓存(jOOQ 并且最近 Hibernate 也通过提供 IN 列表填充来解决这个问题)

(免责声明:我在jOOQ背后的公司工作)

0赞 Rod Meyer 8/20/2021 #31

好吧,所以我不记得我以前是怎么(或在哪里)这样做的,所以我来到 stack overflow 以快速找到答案。我很惊讶我不能。

所以,很久以前我是如何解决 IN 问题的,就是用这样的语句:

其中 myColumn in ( select regexp_substr(:myList,'[^,]+', 1, level) from dual connect by regexp_substr(:myList, '[^,]+', 1, level) is not null)

将 myList 参数设置为逗号分隔的字符串:A,B,C,D...

注意:您必须设置两次参数!

0赞 Kris Khairallah 12/13/2022 #32

这不是理想的做法,但它很简单,而且大多数时候对我来说效果很好。

where ? like concat( "%|", TABLE_ID , "|%" ) 

然后你通过?ID 如下:|1|,|2|,|3|,...|

0赞 Akash 12/14/2022 #33

你可以只使用 PreparedStatement 的 setArray() 方法

PreparedStatement statement = connection.prepareStatement("Select * from emp where field in (?)");
statement.setArray(1, Arrays.asList(1,2,3,4,5));
ResultSet rs = statement.executeQuery();