【问题标题】:PreparedStatement IN clause alternatives?PreparedStatement IN 子句的替代方案?
【发布时间】:2018-03-08 14:36:16
【问题描述】:

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

考虑以下 SQL 语句:

SELECT my_column FROM my_table where search_column IN (?)

使用preparedStatement.setString( 1, "'A', 'B', 'C'" ); 本质上是对首先使用? 原因的一种解决方法的无效尝试。

有哪些解决方法?

【问题讨论】:

  • Oscar,如果您需要 IN 子句,我认为 (?,?,....) 的动态生成是最简单的解决方法,但我将其留给个人调用,因为我的性能已经足够了具体情况。
  • prepared statements 的一个优点是 sohuld 可以编译一次以提高效率。通过使 in 子句动态化,这有效地否定了准备好的语句。
  • 实际上,这适用于 MySQL(使用 setObject 将 String 数组设置为参数值)。你用的是什么数据库?
  • 这是一个相关的问题:stackoverflow.com/q/6956025/521799

标签: java security jdbc prepared-statement in-clause


【解决方案1】:

好的,所以我不记得我之前是如何(或在哪里)这样做的,所以我来到堆栈溢出以快速找到答案。我很惊讶我做不到。

所以,很久以前我解决IN问题的方法是这样的:

myColumn in (select regexp_substr(:myList,'[^,]+', 1, level) from dual connect by regexp_substr(:myList, '[^,]+', 1, level) 是不为空)

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

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

【讨论】:

    【解决方案2】:

    对各种可用选项的分析,以及每个选项的优缺点,请访问here

    建议的选项是:

    • 准备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 个不同大小的列表查询;比如说,有 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 和支持x = ANY(y) 的服务器,最好的选择是将PreparedStatement.setArray 用作described here

    不过,似乎没有任何方法可以使 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(
            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 方法对其进行扩展。

    【讨论】:

    • 另一个选项,如果列表的大小不经常更改 -- 为输入列表的最后一个大小准备并缓存一条语句。在每个后续查询中,如果大小相同,则重新使用准备好的语句,否则,关闭它并创建另一个。
    【解决方案3】:

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

    • 在每个 IN 列表中遇到 Oracle 最多 1000 个元素(与绑定值的数量无关)
    • 遇到任何驱动程序的最大绑定值数量,which I've documented in this answer
    • 遇到游标缓存争用问题,因为“硬解析”了太多不同的 SQL 字符串,并且无法再缓存执行计划(jOOQ 和最近 Hibernate 也通过提供 IN list padding 解决了这个问题)

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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

      • 看起来不错。这段代码的哪一部分是 PostreSQL 特有的? “哪里 search_column = ANY(?)”?还是connection.createArrayOf?还是别的什么?
      • 我认为它比 PostgreSQL 更特定于 JDBC4,因为 .createArrayOf() 部分,但我不确定用户 Arrays 的严格语义是否由 JDBC 规范定义。
      • 如果.createArrayOf 不起作用,您可以自己手动创建数组字面量,例如String arrayLiteral = "{A,\"B \", C,D}" (注意“B”有空格而C 没有) 然后是statement.setString(1,arrayLiteral),其中准备好的语句是... IN (SELECT UNNEST(?::VARCHAR[]))... IN (SELECT UNNEST(CAST(? AS VARCHAR[])))。 (PS:我不认为ANYSELECT 一起使用。)
      • 很好的解决方案!真的为我节省了一天。对于整数数组,我在 createArrayOf() 的第一个参数中使用了“int”,它看起来不错。不过,根据文档,第一个参数似乎是特定于数据库的。
      • 这似乎是最干净的解决方案。如果有人正在寻找 HSQLDB 特定的语法:我设法让它与 IN(UNNEST(?)) 一起使用
      【解决方案5】:

      这对我有用(伪代码):

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

      【讨论】:

        【解决方案6】:

        我刚刚为此制定了一个 PostgreSQL 特定的选项。这有点像 hack,有其自身的优缺点和局限性,但它似乎可以工作,并且不限于特定的开发语言、平台或 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[]);
        

        再次重申,毫无疑问,这是一种 hack,但它可以工作,并且允许您仍然使用预编译的预处理语句,这些语句采用 *ahem* 离散参数,并具有伴随的安全性和(可能)性能优势.它是可取的并且实际上是有效的吗?当然,这取决于您在查询运行之前已经进行了字符串解析并可能进行了强制转换。如果您希望发送三个、五个、几十个值,当然,这可能没问题。几千?是的,也许没有那么多。 YMMV,限制和排除适用,不提供任何明示或暗示的保证。

        但它有效。

        【讨论】:

          【解决方案7】:

          您可以使用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 时最好的解决方案...
          • 如果您要生成特定于一组参数的新 SQL 语句,为什么要使用占位符?
          • @AndyThomas 避免 SQL 注入
          • @GurwinderSingh - 啊,好点。谢谢你的课!
          • 我正在尝试 ps.close();,听说有必要;但很抱歉没有找到办法,你能告诉我吗?
          【解决方案8】:

          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 循环解决方案

          【讨论】:

          • .map(e -> "?").collect(Collectors.joining(", ")
          【解决方案9】:

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

          【讨论】:

            【解决方案10】:

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

            【讨论】:

              【解决方案11】:

              您可以使用 this 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();
              

              【讨论】:

              • 不是所有驱动都支持这个功能,如果不支持这个功能你会得到 SQLFeatureNotSupportedException
              • 很遗憾我的驱动不支持
              【解决方案12】:

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

              【讨论】:

                【解决方案13】:

                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 运算符,这非常快
                • 毕竟, 使用任何普通的 select 或 DML 访问数据库是一个很好的经验法则,因为它是 Oracle,它提供的光年比 MySQL 或类似的简单数据库引擎还要多。 PL/SQL 允许您以有效的方式从应用程序域模型中隐藏存储模型。

                这里的诀窍是:

                • 我们需要一个接受长字符串的调用,并将其存储在 db session 可以访问它的位置(例如简单的包变量或 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 并在视图中转换是最有效的解决方案。

                【讨论】:

                  【解决方案14】:

                  Spring 允许 passing java.util.Lists to NamedParameterJdbcTemplate ,它会根据参数的数量自动生成 (?, ?, ?, ..., ?)。

                  对于 Oracle,this blog posting 讨论了 oracle.sql.ARRAY 的使用(Connection.createArrayOf 不适用于 Oracle)。为此,您必须修改您的 SQL 语句:

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

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

                  【讨论】:

                    【解决方案15】:

                    这是一个完整的 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;
                    
                        }
                    
                    }
                    

                    【讨论】:

                      【解决方案16】:

                      只是为了完整性,因为我没有看到其他人建议它:

                      在实施上述任何复杂建议之前,请考虑 SQL 注入在您的场景中是否确实存在问题。

                      在许多情况下,提供给 IN (...) 的值是一个 id 列表,这些 id 的生成方式可以确保不可能进行注入...(例如,先前选择 some_id 的结果来自 some_table where some_condition。)

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

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

                      【讨论】:

                        【解决方案17】:

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

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

                            select * from TABLE_A where ATTR IN (PARAM);
                        

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

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

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

                        将所有参数收集到单个字符串中后,您只需将查询中的虚拟文本(即本例中的“PARAM”)替换为参数字符串,即 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”,以确保查询中不会出现这样的单词。希望你得到解决方案。

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

                        【讨论】:

                          【解决方案18】:

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

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

                          但它有很多缺点:

                          1. 它应用的任何列都应该转换为 varchar/char,至少是隐式的。
                          2. 需要注意特殊字符。
                          3. 它可能会降低性能 - 在我的例子中,IN 版本使用索引和范围扫描,而 REGEXP 版本使用完整扫描。

                          【讨论】:

                            【解决方案19】:

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

                            1. 使用单个查询 - 最慢的性能和资源密集型
                            2. 使用 StoredProcedure - 最快但特定于数据库
                            3. 为 PreparedStatement 创建动态查询 - 性能良好,但无法从缓存中受益,并且每次都会重新编译 PreparedStatement。
                            4. 在 PreparedStatement 查询中使用 NULL - 最佳性能,当您知道 IN 子句参数的限制时效果很好。如果没有限制,则可以批量执行查询。 示例代码sn -p是;

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

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

                            【讨论】:

                            • “为 PreparedStatement 创建动态查询 - 性能良好,但没有从缓存中受益,并且每次都重新编译 PreparedStatement。”缓存和避免重新编译是使准备好的语句表现良好的原因。因此,我不同意你的说法。但是,这将防止 SQL 注入,因为您将串联/动态输入限制为逗号。
                            • 我同意你的观点,但是这里的“良好性能”是针对这种特定情况的。它比方法 1 性能更好,但方法 2 最快。
                            【解决方案20】:

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

                            1. 准备好的语句只缓存在同一个会话 (Postgres) 中,因此它只适用于连接池
                            2. @BalusC 提出的许多不同的预处理语句可能会导致缓存溢出,并且之前缓存的语句将被丢弃
                            3. 必须优化查询并使用索引。听起来很明显,但是例如@Boris 在最佳答案之一中提出的 ANY(ARRAY...) 语句不能使用索引,尽管缓存,查询也会很慢
                            4. prepared statement 也缓存了查询计划,并且该语句中指定的任何参数的实际值都不可用。

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

                            【讨论】:

                              【解决方案21】:

                              而不是使用

                              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 语句将被编译并存储在数据库服务器中

                              【讨论】:

                                【解决方案22】:

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

                                【讨论】:

                                • 不再需要使用 StringBuilder。编译器无论如何都会将 + 符号转换为 StringBuilder.append(),因此不会影响性能。试试自己:)
                                • @neu242:哦,是的,编译器使用StringBuilder。但不是你想的那样。反编译generateQsForIn,您可以看到每个循环迭代两个 分配了新的StringBuilder,并在每个循环上调用了toStringStringBuilder 优化仅捕获 "x" + i+ "y" + j 之类的内容,但不会超出一个表达式。
                                • @neu242 你不能使用ps.setObject(1,items)而不是遍历列表然后设置paramteres吗?
                                【解决方案23】:

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

                                【讨论】:

                                  【解决方案24】:

                                  我的解决方法是:

                                  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,是的。它不会在其他数据库中工作。请注意,此实现对输入参数有限制 - 总长度限制为 32k 字符 - 以及性能限制,因为对流水线函数的调用会在 PL/SQL 和 Oracle 的 SQL 引擎之间进行上下文切换。
                                  【解决方案25】:

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

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

                                    ... WHERE A IN (?,?,?,?,?,?,?,?,?,?) ...

                                  2. 绑定所有实际参数

                                    setString(1,"foo"); setString(2,"bar");

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

                                  【讨论】:

                                  • "NULL 从不匹配任何东西" — 查询中的 NULL 会匹配数据库中的 NULL 值吗?
                                  • @CraigMcQueen 不,不会。根据 ANSI 标准,Null 甚至不匹配 null。
                                  • 您可以使用 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 中不匹配的所有行。跨度>
                                  • 不过要小心。 NOT ININ 处理空值的方式不同。运行它,看看会发生什么: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 INa NOT IN (1,2,3,null,null,null,null) 不同(它总是不返回任何行,因为 any_value != NULL 总是假的)。
                                  【解决方案26】:

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

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

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

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

                                  【讨论】:

                                  • 与一次查询 my_table 一个值相比,这有什么优势?
                                  • 查询优化器可以通过从加载的页面中检索所有可能的匹配来减少 I/O 负载。表扫描或索引扫描可以执行一次,而不是每个值执行一次。通过批处理操作可以减少插入值的开销,并且可能少于几个查询。
                                  • 看起来不错,但可能存在并发问题。 jdbc 规范是否包含在内存中创建临时匿名表的方法?或类似的东西,如果可能的话,不是 jdbc-vendor 特定的?
                                  【解决方案27】:

                                  我从未尝试过,但 .setArray() 会满足您的需求吗?

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

                                  【讨论】:

                                  • 不适用于所有数据库,但它是“正确”的方法。
                                  • 你的意思是所有的司机。一些驱动程序具有与这一年(上个世纪?)标准相当的专有等价物。另一种方法是将一批值放入临时表中,但并非所有数据库都支持...
                                  • java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… 根据 Sun 的说法,阵列内容 [通常] 保留在服务器端并根据需要提取。 PreparedStatement.setArray() 可以从之前的 ResultSet 中返回一个 Array,而不是在客户端创建一个新的 Array。
                                  【解决方案28】:

                                  遵循亚当的想法。使您准备好的语句排序为 select my_column from my_table where search_column in (#) 创建一个字符串 x 并用一些“?,?,?”填充它取决于您的值列表 然后只需将查询中的 # 更改为您的新字符串 x 并填充

                                  【讨论】:

                                    【解决方案29】:

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

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

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

                                    当然,您需要确保集合永远不会过大,或者在发生错误时进行错误捕获。

                                    【讨论】:

                                    • 是的,你是对的。在这种情况下,我的目标是每次使用不同数量的项目重用 PreparedStatement。
                                    • 使用“OR”会混淆意图。坚持使用“IN”,因为它更易于阅读且意图更清晰。切换的唯一原因是查询计划是否不同。
                                    【解决方案30】:

                                    尝试使用 instr 函数?

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

                                    然后

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

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

                                    【讨论】:

                                    • 哦,我知道它不会使用索引
                                    • 它不适用于某些字符串,例如,如果字符串包含','。
                                    猜你喜欢
                                    • 2020-08-27
                                    相关资源
                                    最近更新 更多