【问题标题】:PreparedStatement with list of parameters in a IN clause [duplicate]带有 IN 子句中参数列表的 PreparedStatement [重复]
【发布时间】:2011-03-07 15:05:32
【问题描述】:

执行查询时如何在 JDBC 中的preparedStatement 中设置in 子句的值。

例子:

connection.prepareStatement("Select * from test where field in (?)");

如果这个子句可以包含多个值,我该怎么做。有时我事先知道参数列表,有时我事先不知道。这种情况如何处理?

【问题讨论】:

标签: java jdbc prepared-statement in-clause


【解决方案1】:

我要做的是添加一个“?”每个可能的值。

var stmt = String.format("select * from test where field in (%s)",
                         values.stream()
                         .map(v -> "?")
                         .collect(Collectors.joining(", ")));

使用 StringBuilder 的替代方法(这是 10 多年前的原始答案)

List values = ... 
StringBuilder builder = new StringBuilder();

for( int i = 0 ; i < values.size(); i++ ) {
    builder.append("?,");
}

String placeHolders =  builder.deleteCharAt( builder.length() -1 ).toString();
String stmt = "select * from test where field in ("+ placeHolders + ")";
PreparedStatement pstmt = ... 

然后愉快地设置参数

int index = 1;
for( Object o : values ) {
   pstmt.setObject(  index++, o ); // or whatever it applies 
}
   

   

【讨论】:

  • 根据列表的最大长度,这可能会导致大量准备好的语句,可能会影响数据库性能。
  • 另外,好像少了括号……
  • 我听说有一个很好的做法,其中有几个带有不同数量问号的 SQL 语句 - 例如,10、40、160、800。其余的填充为零(零不用作ID,通常)或任何给定的参数。这减少了存储在数据库缓存中的预处理语句的数量。
  • 虽然追加",?"然后删除第一个字符要容易一些。
  • 我认为这是个坏主意,可能会毁掉您在大公司的生产。池具有最大数量的准备好的语句(并且 ofc 不使用 -1 ),您可以使用该技巧使该最大值饱和。对我来说,这个答案是危险的,可能是邪恶的。
【解决方案2】:

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

http://docs.oracle.com/javase/6/docs/api/java/sql/PreparedStatement.html#setArray(int, java.sql.Array)

代码:

PreparedStatement statement = connection.prepareStatement("Select * from test where field in (?)");
Array array = statement.getConnection().createArrayOf("VARCHAR", new Object[]{"A1", "B2","C3"});
statement.setArray(1, array);
ResultSet rs = statement.executeQuery();

【讨论】:

  • 这个解决方案听起来不错,但我从 mysql 驱动程序 5.1.39 得到一个 java.sql.SQLFeatureNotSupportedException
  • 也不适用于 Oracle。
  • 不适用于 H2 和 IN 子句
  • 这对什么数据库有用?
  • 在 PostgreSQL 中工作。但是你必须写WHERE field = ANY (?) 而不是WHERE field IN (?),参见。 stackoverflow.com/a/36930781/5296296
【解决方案3】:

您不能将查询中的? 替换为任意数量的值。每个? 是一个仅用于单个值的占位符。要支持任意数量的值,您必须动态构建一个包含 ?, ?, ?, ... , ? 的字符串,其中问号的数量与您在 in 子句中想要的值的数量相同。

【讨论】:

    【解决方案4】:

    您不希望将 PreparedStatment 与使用 IN 子句的动态查询一起使用,至少您确定您总是低于 5 个变量或类似的小值,但即使这样,我认为这是一个坏主意(不可怕,但不好) .由于元素的数量很大,它会更糟(和可怕)。

    在你的 IN 子句中想象成百上千种可能性:

    1. 这会适得其反,您会损失性能和内存,因为您每次都会缓存新请求,而 PreparedStatement 不仅用于 SQL 注入,还与性能有关。在这种情况下,Statement 更好。

    2. 您的池有 PreparedStatment 的限制(默认为 -1,但您必须限制它),您将达到此限制!如果你没有限制或非常大的限制,你会有一些内存泄漏的风险,在极端情况下会出现 OutofMemory 错误。因此,如果它是用于由 3 个用户使用的小型个人项目,它并不引人注目,但如果您在一家大公司并且您的应用程序被数千​​人和数百万请求使用,那么您不希望这样。

    一些阅读。 IBM : Memory utilization considerations when using prepared statement caching

    【讨论】:

      【解决方案5】:

      你需要jdbc4然后你可以使用setArray!

      在我的情况下它不起作用,因为 postgres 中的 UUID 数据类型似乎仍然有它的弱点,但对于通常的类型它是有效的。

      ps.setArray(1, connection.createArrayOf("$VALUETYPE",myValuesAsArray));
      

      当然用正确的值替换 $VALUETYPE 和 myValuesAsArray。

      在 Marks 评论后备注:

      您的数据库和驱动程序需要支持这一点!我尝试过 Postgres 9.4,但我认为这已经在前面介绍过了。您需要一个 jdbc 4 驱动程序,否则 setArray 将不可用。我用的是spring boot自带的postgresql 9.4-1201-jdbc41驱动

      【讨论】:

      • 这是否有效很大程度上取决于数据库。您可能需要包含您使用的数据库 + 驱动程序。
      【解决方案6】:

      目前,MySQL 不允许在一个方法调用中设置多个值。 所以你必须让它在你自己的控制之下。我通常为预定义数量的参数创建一个准备好的语句,然后根据需要添加任意数量的批次。

          int paramSizeInClause = 10; // required to be greater than 0!
          String color = "FF0000"; // red
          String name = "Nathan"; 
          Date now = new Date();
          String[] ids = "15,21,45,48,77,145,158,321,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,358,1284,1587".split(",");
      
          // Build sql query 
          StringBuilder sql = new StringBuilder();
          sql.append("UPDATE book SET color=? update_by=?, update_date=? WHERE book_id in (");
          // number of max params in IN clause can be modified 
          // to get most efficient combination of number of batches
          // and number of parameters in each batch
          for (int n = 0; n < paramSizeInClause; n++) {
              sql.append("?,");
          }
          if (sql.length() > 0) {
              sql.deleteCharAt(sql.lastIndexOf(","));
          }
          sql.append(")");
      
          PreparedStatement pstm = null;
          try {
              pstm = connection.prepareStatement(sql.toString());
              int totalIdsToProcess = ids.length;
              int batchLoops = totalIdsToProcess / paramSizeInClause + (totalIdsToProcess % paramSizeInClause > 0 ? 1 : 0);
              for (int l = 0; l < batchLoops; l++) {
                  int i = 1;
                  pstm.setString(i++, color);
                  pstm.setString(i++, name);
                  pstm.setTimestamp(i++, new Timestamp(now.getTime()));
                  for (int count = 0; count < paramSizeInClause; count++) {
                      int param = (l * paramSizeInClause + count);
                      if (param < totalIdsToProcess) {
                          pstm.setString(i++, ids[param]);
                      } else {
                          pstm.setNull(i++, Types.VARCHAR);
                      }
                  }
                  pstm.addBatch();
              }
          } catch (SQLException e) {
          } finally {
              //close statement(s)
          }
      

      如果您不喜欢在没有更多参数时设置 NULL,您可以修改代码以构建两个查询和两个准备好的语句。第一个是相同的,但余数(模数)的第二个语句。 在这个特定的示例中,一个查询 10 个参数,一个查询 8 个参数。您必须为第一个查询添加 3 个批次(前 30 个参数),然后为第二个查询添加一个批次(8 个参数)。

      【讨论】:

        【解决方案7】:
        public static ResultSet getResult(Connection connection, List values) {
            try {
                String queryString = "Select * from table_name where column_name in";
        
                StringBuilder parameterBuilder = new StringBuilder();
                parameterBuilder.append(" (");
                for (int i = 0; i < values.size(); i++) {
                    parameterBuilder.append("?");
                    if (values.size() > i + 1) {
                        parameterBuilder.append(",");
                    }
                }
                parameterBuilder.append(")");
        
                PreparedStatement statement = connection.prepareStatement(queryString + parameterBuilder);
                for (int i = 1; i < values.size() + 1; i++) {
                    statement.setInt(i, (int) values.get(i - 1));
                }
        
                return statement.executeQuery();
            } catch (Exception d) {
                return null;
            }
        }
        

        【讨论】:

          【解决方案8】:

          只要知道需要在 IN 子句中放入多少值,您就可以通过简单的 for 循环动态构建选择字符串(“IN (?)”部分)。然后,您可以实例化 PreparedStatement。

          【讨论】:

          • 如果您直接将用户输入放入 SQL 字符串中,这是否与 PreparedStatement 的要点相悖,sql 注入变得容易
          【解决方案9】:
          public class Test1 {
              /**
               * @param args
               */
              public static void main(String[] args) {
                  // TODO Auto-generated method stub
                  System.out.println("helow");
          String where="where task in ";
                  where+="(";
              //  where+="'task1'";
                  int num[]={1,2,3,4};
                  for (int i=0;i<num.length+1;i++) {
                      if(i==1){
                          where +="'"+i+"'";
                      }
                      if(i>1 && i<num.length)
                          where+=", '"+i+"'";
                      if(i==num.length){
                          System.out.println("This is last number"+i);
                      where+=", '"+i+"')";
                      }
                  }
                  System.out.println(where);  
              }
          }
          

          【讨论】:

          • new StringBuilder() 调用次数过多。
          【解决方案10】:

          你可以使用:

          for( int i = 0 ; i < listField.size(); i++ ) {
              i < listField.size() - 1 ? request.append("?,") : request.append("?");
          }
          

          然后:

          int i = 1;
          for (String field : listField) {
              statement.setString(i++, field);
          }
          

          示例:

          List<String> listField = new ArrayList<String>();
          listField.add("test1");
          listField.add("test2");
          listField.add("test3");
          
          StringBuilder request = new StringBuilder("SELECT * FROM TABLE WHERE FIELD IN (");
          
          for( int i = 0 ; i < listField.size(); i++ ) {
              request = i < (listField.size() - 1) ? request.append("?,") : request.append("?");
          }
          
          
          DNAPreparedStatement statement = DNAPreparedStatement.newInstance(connection, request.toString);
          
          int i = 1;
          for (String field : listField) {
              statement.setString(i++, field);
          }
          
          ResultSet rs = statement.executeQuery();
          

          【讨论】:

            【解决方案11】:

            试试这段代码

             String ids[] = {"182","160","183"};
                        StringBuilder builder = new StringBuilder();
            
                        for( int i = 0 ; i < ids.length; i++ ) {
                            builder.append("?,");
                        }
            
                        String sql = "delete from emp where id in ("+builder.deleteCharAt( builder.length() -1 ).toString()+")";
            
                        PreparedStatement pstmt = connection.prepareStatement(sql);
            
                        for (int i = 1; i <= ids.length; i++) {
                            pstmt.setInt(i, Integer.parseInt(ids[i-1]));
                        }
                        int count = pstmt.executeUpdate();
            

            【讨论】:

              【解决方案12】:

              许多数据库都有临时表的概念,即使假设您没有临时表,您也可以始终生成具有唯一名称的临时表,并在完成后将其删除。虽然创建和删除表的开销很大,但这对于非常大的操作可能是合理的,或者在您将数据库用作本地文件或内存 (SQLite) 的情况下。

              我正在研究的一个例子(使用 Java/SqlLite):

              String tmptable = "tmp" + UUID.randomUUID();
              
              sql = "create table " + tmptable + "(pagelist text not null)";
              cnn.createStatement().execute(sql);
              
              cnn.setAutoCommit(false);
              stmt = cnn.prepareStatement("insert into "+tmptable+" values(?);");
              for(Object o : rmList){
                  Path path = (Path)o;
                  stmt.setString(1, path.toString());
                  stmt.execute();
              }
              cnn.commit();
              cnn.setAutoCommit(true);
              
              stmt = cnn.prepareStatement(sql);
              stmt.execute("delete from filelist where path + page in (select * from "+tmptable+");");
              stmt.execute("drop table "+tmptable+");");
              

              请注意,我的表使用的字段是动态创建的。

              如果您能够重复使用该表,这将更加有效。

              【讨论】:

                【解决方案13】:
                Using Java 8 APIs, 
                
                    List<Long> empNoList = Arrays.asList(1234, 7678, 2432, 9756556, 3354646);
                
                    List<String> parameters = new ArrayList<>();
                    empNoList.forEach(empNo -> parameters.add("?"));   //Use forEach to add required no. of '?'
                    String commaSepParameters = String.join(",", parameters); //Use String to join '?' with ','
                
                StringBuilder selectQuery = new StringBuilder().append("SELECT COUNT(EMP_ID) FROM EMPLOYEE WHERE EMP_ID IN (").append(commaSepParameters).append(")");
                

                【讨论】:

                  【解决方案14】:

                  public static void main(String arg[]) {

                      Connection connection = ConnectionManager.getConnection(); 
                      PreparedStatement pstmt = null;
                            //if the field values are in ArrayList
                          List<String> fieldList = new ArrayList();
                  
                      try {
                  
                          StringBuffer sb = new StringBuffer();  
                  
                          sb.append("  SELECT *            \n");
                          sb.append("   FROM TEST          \n");
                          sb.append("  WHERE FIELD IN (    \n");
                  
                          for(int i = 0; i < fieldList.size(); i++) {
                              if(i == 0) {
                                  sb.append("    '"+fieldList.get(i)+"'   \n");
                              } else {
                                  sb.append("   ,'"+fieldList.get(i)+"'   \n");
                              }
                          }
                          sb.append("             )     \n");
                  
                          pstmt = connection.prepareStatement(sb.toString());
                          pstmt.executeQuery();
                  
                      } catch (SQLException se) {
                          se.printStackTrace();
                      }
                  
                  }
                  

                  【讨论】:

                  • 不要这样做!这将打开您的程序以进行 SQL 注入。如果您从用户输入执行此操作,他们可以使查询做任何他们想做的事情。
                  • 添加到大卫的评论:不要这样做!准备这样的声明是毫无价值和有害的。使用此处发布的其他解决方案之一,包括?占位符,或者,如果您不关心 SQL 注入攻击,那么只需使用 Statement 对象而无需准备。
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2010-12-17
                  • 2012-10-23
                  • 2020-08-27
                  • 1970-01-01
                  • 1970-01-01
                  • 2016-09-27
                  相关资源
                  最近更新 更多