PreparedStatement IN子句的替代?

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

考虑下面的SQL语句:

 SELECT my_column FROM my_table where search_column IN (?) 

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

什么解决方法可用?

对各种可用选项的分析,以及每种选项的优缺点都可以在这里find 。

build议的选项是:

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

虽然这些选项都不是超好的。

重复的问题已经在这些地方得到了回答,同样是相当理智的select,但仍然没有一个是超级的:

  • PreparedStatement和IN子句中的参数列表
  • 如何设置准备好的语句中的参数列表?

如果您使用JDBC4和支持x = ANY(y)的服务器,则正确的答案是使用PreparedStatement.setArray如下所述:

  • PreparedStatement IN子句的替代?

尽pipe如此,似乎没有办法使setArray和IN列表一起工作。

PostgreSQL解决scheme:

 final PreparedStatement statement = connection.prepareStatement( "SELECT my_column FROM my_table where search_column = ANY (?)" ); final String[] values = getValues(); statement.setArray(1, connection.createArrayOf("text", values)); final ResultSet rs = statement.executeQuery(); try { while(rs.next()) { // do some... } } finally { rs.close(); } 

要么

 final PreparedStatement statement = connection.prepareStatement( "SELECT my_column FROM my_table " + "where search_column IN (SELECT * FROM unnest(?))" ); final String[] values = getValues(); statement.setArray(1, connection.createArrayOf("text", values)); final ResultSet rs = statement.executeQuery(); try { while(rs.next()) { // do some... } } finally { rs.close(); } 

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

  1. 用几个(例如10个)参数创build一个语句:

    …在哪里(?,?,?,?,?,?,?,?,?)…

  2. 绑定所有的实际参数

    的SetString(1, “富”); 的SetString(2, “巴”);

  3. 其余为NULL

    setNull(3,Types.VARCHAR)… setNull(10,Types.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++; } 

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

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

丑,但如果你的价值清单非常大,一个可行的select。

这个技巧还有一个好处,就是优化器可能会有更好的查询计划(如果你的数据库没有caching准备好的语句,可以节省开销)。 你的“INSERTS”需要批量完成,MYVALUES表可能需要调整,以获得最小的locking或其他高开销保护。

我从来没有尝试过,但会.setArray()做你在找什么?

更新 :显然不是。 setArray似乎只能用来自先前查询检索到的ARRAY列的java.sql.Array,或者带有ARRAY列的子查询。

我的解决方法是:

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

现在,您可以使用一个variables来获取表中的一些值:

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

问候,

哈维尔·伊瓦涅斯

in()运算符的局限性是所有邪恶的根源。

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

  • 如果你正在创build一个可变数量的参数的语句,那么每次调用都会产生一个sql语法分析
  • 在许多平台上,in()运算符的参数数量是有限的
  • 在所有平台上,总的SQL文本大小是有限的,不可能发送2000个占位符中的参数
  • 发送1000-10k的绑定variables是不可能的,因为JDBC驱动程序有其局限性

in()方法对于某些情况可能足够好,但不是火箭certificate:)

防火墙的解决scheme是在一个单独的调用中传递任意数量的参数(例如通过传递参数),然后有一个视图(或任何其他方式)在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; 

那么你可以在参数中传递任意数量的逗号分隔的客户ID,并且:

  • 将不会得到parsing延迟,因为select的SQL是稳定的
  • 没有stream水线function的复杂性 – 这只是一个查询
  • SQL正在使用一个简单的连接,而不是一个IN操作符,这个操作符相当快
  • 毕竟,这是一个很好的经验法则,因为它是Oracle,它提供比MySQL或类似的简单数据库引擎更多的光年,所以没有任何简单的select或DML命中数据库。 PL / SQL允许您以有效的方式从应用程序域模型中隐藏存储模型。

这里的诀窍是:

  • 我们需要一个接受长string的调用,并将其存储在数据库会话可以访问的地方(例如简单的包variables或dbms_session.set_context)
  • 那么我们需要一个可以parsing这个行的视图
  • 然后你有一个包含你正在查询的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指向原始inputstring。


一个可能的方法是传递pl / sql数组(仅由Oracle支持),但是不能在纯SQL中使用这些数组,因此总是需要转换步骤。 转换不能在SQL中完成,因此毕竟,传递所有参数为string的clob并将其转换为视图是最有效的解决scheme。

以下是我在自己的应用程序中解决它的方法。 理想情况下,您应该使用StringBuilder,而不是使用+作为string。

  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的variables而不是具体数字会有所帮助。

我想你可以(使用基本的string操作)在PreparedStatement生成查询string有一些? 与您列表中的项目数量相匹配。

当然,如果你这样做,你只是在你的查询中产生一个巨大的链式OR ,而没有正确的数量? 在查询string中,我不知道如何解决这个问题。

尝试使用instr函数?

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

然后

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

诚然,这是一个肮脏的黑客,但它确实减less了SQL注入的机会。 无论如何在oracle中工作。

Sormula支持SQL IN操作符,允许您提供一个java.util.Collection对象作为参数。 它创build一个准备好的声明, 为每个元素收集。 请参见示例4 (示例中的SQL是一个注释,用于说明创build的内容,但不被Sormula使用)。

我遇到了一些与准备好的陈述有关的限制:

  1. 准备好的语句只在相同的会话(Postgres)中被caching,所以它只能在连接池中工作
  2. @BalusC提出的许多不同的预处理语句可能会导致caching溢出,并且以前的caching语句将被丢弃
  3. 查询必须进行优化并使用索引。 听起来很明显,不过例如@Boris在其中一个最佳答案中提出的ANY(ARRAY …)语句不能使用索引,尽pipecaching的查询速度会很慢
  4. 准备的语句也会caching查询计划,并且语句中指定的任何参数的实际值都不可用。

在所提出的解决scheme中,我会select不降低查询性能并减less查询次数的解决scheme。 这将是@Don链接中的#4(批量查询)或为不需要的'?'指定NULL值 由@Vladimir Dyuzhev提出的标记

下面是Java中一个完整的解决scheme,为您创build准备好的语句:

 /*usage: Util u = new Util(500); //500 items per bracket. String sqlBefore = "select * from myTable where ("; List<Integer> values = new ArrayList<Integer>(Arrays.asList(1,2,4,5)); string sqlAfter = ") and foo = 'bar'"; PreparedStatement ps = u.prepareStatements(sqlBefore, values, sqlAfter, connection, "someId"); */ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class Util { private int numValuesInClause; public Util(int numValuesInClause) { super(); this.numValuesInClause = numValuesInClause; } public int getNumValuesInClause() { return numValuesInClause; } public void setNumValuesInClause(int numValuesInClause) { this.numValuesInClause = numValuesInClause; } /** Split a given list into a list of lists for the given size of numValuesInClause*/ public List<List<Integer>> splitList( List<Integer> values) { List<List<Integer>> newList = new ArrayList<List<Integer>>(); while (values.size() > numValuesInClause) { List<Integer> sublist = values.subList(0,numValuesInClause); List<Integer> values2 = values.subList(numValuesInClause, values.size()); values = values2; newList.add( sublist); } newList.add(values); return newList; } /** * Generates a series of split out in clause statements. * @param sqlBefore ""select * from dual where (" * @param values [1,2,3,4,5,6,7,8,9,10] * @param "sqlAfter ) and id = 5" * @return "select * from dual where (id in (1,2,3) or id in (4,5,6) or id in (7,8,9) or id in (10)" */ public String genInClauseSql(String sqlBefore, List<Integer> values, String sqlAfter, String identifier) { List<List<Integer>> newLists = splitList(values); String stmt = sqlBefore; /* now generate the in clause for each list */ int j = 0; /* keep track of list:newLists index */ for (List<Integer> list : newLists) { stmt = stmt + identifier +" in ("; StringBuilder innerBuilder = new StringBuilder(); for (int i = 0; i < list.size(); i++) { innerBuilder.append("?,"); } String inClause = innerBuilder.deleteCharAt( innerBuilder.length() - 1).toString(); stmt = stmt + inClause; stmt = stmt + ")"; if (++j < newLists.size()) { stmt = stmt + " OR "; } } stmt = stmt + sqlAfter; return stmt; } /** * Method to convert your SQL and a list of ID into a safe prepared * statements * * @throws SQLException */ public PreparedStatement prepareStatements(String sqlBefore, ArrayList<Integer> values, String sqlAfter, Connection c, String identifier) throws SQLException { /* First split our potentially big list into lots of lists */ String stmt = genInClauseSql(sqlBefore, values, sqlAfter, identifier); PreparedStatement ps = c.prepareStatement(stmt); int i = 1; for (int val : values) { ps.setInt(i++, val); } return ps; } } 

Spring允许将java.util.Lists传递给NamedParameterJdbcTemplate ,它根据参数的数量自动生成(?,?,?,…,?)。

对于Oracle而言, 这篇博客文章讨论了使用oracle.sql.ARRAY(Connection.createArrayOf不适用于Oracle)。 为此,你必须修改你的SQL语句:

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

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

你可以使用这个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(); 

只是为了完整性:只要值的集合不是太大,你可以简单地构造一个类似的语句

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

然后你可以传递给prepare(),然后在循环中使用setXXX()来设置所有的值。 这看起来很糟糕,但是许多“大”的商业系统通常会做这种事情,直到达到DB特定的限制,例如32 KB(我认为是这样)在Oracle中的语句。

当然你需要确保这个集合不会不合理地大,或者在事件发生的时候做错误的捕获。

遵循亚当的想法。 使你准备好的语句从my_table中selectmy_column,其中search_column在(#)中创build一个stringx并填充一些“?,?,?” 取决于您的值列表然后,只需更改查询中的#为您的新的stringx填充

在PreparedStatement中生成查询string,使其中的一些数字与您列表中的项目数量相匹配。 这是一个例子:

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

而不是使用

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

或者使用存储过程,这将是最好的解决scheme,因为sql语句将被编译并存储在DataBase服务器中

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

  1. 使用单个查询 – 最慢的性能和资源密集型
  2. 使用StoredProcedure – 最快但是数据库特定
  3. 为PreparedStatement创builddynamic查询 – 性能良好,但无法获得caching,每次都会重新编译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); } 

你可以在这里查看更多有关这些替代方法的细节。

对于某些情况,正则expression式可能会有所帮助。 这是我在Oracle上查看的一个例子,它的工作原理。

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

但是它有一些缺点:

  1. 它应用的任何列应至less隐式转换为varchar / char。
  2. 需要注意特殊字符。
  3. 它会降低性能 – 在我的情况下,IN版本使用索引和范围扫描,REGEXP版本做全面扫描。

在考察了各种论坛上的各种解决scheme后,并没有find一个好的解决scheme,我觉得下面的黑客我想出来,是最容易遵循和编码:

例子:假设你有多个参数在“IN”子句中传递。 只需在“IN”子句中放置一个虚拟string,例如“PARAM”就表示将要进入这个虚拟string的参数列表。

  select * from TABLE_A where ATTR IN (PARAM); 

您可以将所有参数收集到Java代码中的单个stringvariables中。 这可以如下完成:

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

您可以将所有以逗号分隔的参数附加到单个stringvariables“param1”中,在我们的例子中。

将所有参数收集到一个string后,您可以用查询string(即param1)replace查询中的虚拟文本,即本例中的“PARAM”。 这是你需要做的:

  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”,以确保在查询中不会出现这样的单词。 希望你有解决办法。

注意:尽pipe这不是一个准备好的查询,但是它完成了我希望我的代码执行的工作。

只是为了完整性,因为我没有看到别人的build议:

在实现上述任何复杂的build议之前,请考虑一下SQL注入在您的场景中确实是一个问题。

在许多情况下,提供给IN(…)的值是以一种方式生成的id列表,您可以确保无法注入…(例如,先前从some_table中selectsome_id的结果some_condition。)

如果是这种情况,你可能只是连接这个值,而不是使用服务或准备好的语句,或用于这个查询的其他参数。

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

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

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