使用 JDBC 参数化 IN 子句的最佳方法是啥? [复制]

Posted

技术标签:

【中文标题】使用 JDBC 参数化 IN 子句的最佳方法是啥? [复制]【英文标题】:What is the best approach using JDBC for parameterizing an IN clause? [duplicate]使用 JDBC 参数化 IN 子句的最佳方法是什么? [复制] 【发布时间】:2010-05-18 21:18:46 【问题描述】:

说我有一个表格查询

SELECT * FROM MYTABLE WHERE MYCOL in (?)

我想将参数参数化为 in。

是否有一种直接的方法可以在 Java 中使用 JDBC 执行此操作,并且可以在不修改 SQL 本身的情况下在多个数据库上工作?

最接近的question I've found had to do with C#,我想知道Java/JDBC 是否有不同之处。

【问题讨论】:

【参考方案1】:

在 JDBC 中确实没有直接的方法可以做到这一点。 一些 JDBC 驱动程序似乎支持IN 子句中的PreparedStatement#setArray()。我只是不确定哪些是。

您可以只使用带有String#join()Collections#nCopies() 的辅助方法来生成IN 子句的占位符,并使用另一个辅助方法来使用PreparedStatement#setObject() 在循环中设置所有值。

public static String preparePlaceHolders(int length) 
    return String.join(",", Collections.nCopies(length, "?"));


public static void setValues(PreparedStatement preparedStatement, Object... values) throws SQLException 
    for (int i = 0; i < values.length; i++) 
        preparedStatement.setObject(i + 1, values[i]);
    

你可以这样使用它:

private static final String SQL_FIND = "SELECT id, name, value FROM entity WHERE id IN (%s)";

public List<Entity> find(Set<Long> ids) throws SQLException 
    List<Entity> entities = new ArrayList<Entity>();
    String sql = String.format(SQL_FIND, preparePlaceHolders(ids.size()));

    try (
        Connection connection = dataSource.getConnection();
        PreparedStatement statement = connection.prepareStatement(sql);
    ) 
        setValues(statement, ids.toArray());

        try (ResultSet resultSet = statement.executeQuery()) 
            while (resultSet.next()) 
                entities.add(map(resultSet));
            
        
    

    return entities;


private static Entity map(ResultSet resultSet) throws SQLException 
    Enitity entity = new Entity();
    entity.setId(resultSet.getLong("id"));
    entity.setName(resultSet.getString("name"));
    entity.setValue(resultSet.getInt("value"));
    return entity;

请注意,某些数据库在IN 子句中对允许的值数量有限制。例如,Oracle 对 1000 个项目有此限制。

【讨论】:

这种方式会不会导致SQL注入?? @Kaylan:没有单个代码行将用户控制的输入原始添加到 SQL 查询字符串。所以绝对没有 SQL 注入风险。 很好。感谢您的澄清 jtds 驱动程序的最大参数变量列表为 2,000。 mysql(驱动程序 5.1.37)和 PostgreSQL(驱动程序 9.1-901)之间,只有 PostgreSQL 对 PreparedStatement#setArray() 有一些支持【参考方案2】:

由于没有人回答 大型 IN 子句(超过 100 个),我将提出我的解决方案来解决这个问题,该解决方案非常适用于 JDBC。简而言之,我将 tmp 表上的 IN 替换为 INNER JOIN

我所做的是创建我称之为批处理 ids 的表,根据 RDBMS,我可能会创建一个 tmp 表或内存表。

表格有两列。一列带有来自 IN 子句的 ID,另一列带有我即时生成的批次 ID。

SELECT * FROM MYTABLE M INNER JOIN IDTABLE T ON T.MYCOL = M.MYCOL WHERE T.BATCH = ?

在您选择之前,将您的 id 放入具有给定批次 id 的表中。 然后,您只需将原始查询 IN 子句替换为与您的 ids 表匹配的 INNER JOIN WHERE batch_id 等于您当前的批次。完成后,您可以批量删除条目。

【讨论】:

+1 这对于大型数据集非常有效,并且不会破坏您的数据库 嗯,半连接(INEXISTS 谓词)不会比内连接更好吗? @LukasEder YMMV 和我的 SQL 伪代码。一如既往的测试/基准测试。这是一个有趣的想法。说到这里,当我们这样做时,我应该去看看我们的实际 SQL 是做什么的。 确实如此。在 MySQL 上,半连接仍然有变慢的风险。但是内部连接有不正确的风险;) @Male,当您需要从包含所有结果的 JDBC 函数(例如,使用商定/固定的 API)输出完整的 ResultSet 时,这将是一个不错的选择:填写 temp-表并进行连接,而不是批量输入,而无法合并 ResultSet。 (注意:当您只有 SELECT 权限时,您将无法创建临时表)【参考方案3】:

执行此操作的标准方法是(如果您使用 Spring JDBC)是使用 org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate 类。

使用这个类,可以定义一个 List 作为你的 SQL 参数,并使用 NamedParameterJdbcTemplate 来替换一个命名参数。例如:

public List<MyObject> getDatabaseObjects(List<String> params) 
    NamedParameterJdbcTemplate jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    String sql = "select * from my_table where my_col in (:params)";
    List<MyObject> result = jdbcTemplate.query(sql, Collections.singletonMap("params", params), myRowMapper);
    return result;

【讨论】:

【参考方案4】:

我通过使用尽可能多的 ? 构造 SQL 字符串来解决这个问题。

SELECT * FROM MYTABLE WHERE MYCOL in (?,?,?,?)

首先,我搜索了可以传递到语句中的数组类型,但所有 JDBC 数组类型都是特定于供应商的。所以我留在了多个?

【讨论】:

这就是我们现在正在做的事情,但我希望有一种统一的方法可以在没有自定义 SQL 的情况下做到这一点...... 另外,如果它类似于 Oracle,它必须重新解析大多数语句。【参考方案5】:

我从docs.spring(19.7.3)得到了答案

SQL 标准允许基于包含变量值列表的表达式来选择行。一个典型的例子是 select * from T_ACTOR where id in (1, 2, 3)。 JDBC 标准不直接支持准备好的语句使用此变量列表;您不能声明可变数量的占位符。您需要准备好所需数量的占位符的多种变体,或者您需要在知道需要多少占位符后动态生成 SQL 字符串。 NamedParameterJdbcTemplate 和 JdbcTemplate 中提供的命名参数支持采用后一种方法。将值作为原始对象的 java.util.List 传递。此列表将用于在语句执行期间插入所需的占位符并传入值。

希望对你有帮助。

【讨论】:

【参考方案6】:

AFAIK,JDBC 中没有标准支持将集合作为参数处理。如果您可以只传入一个 List 并将其扩展,那就太好了。

Spring 的 JDBC 访问支持将集合作为参数传递。您可以看看这是如何完成的,以获取安全编码的灵感。

见Auto-expanding collections as JDBC parameters

(本文先讨论 Hibernate,然后再讨论 JDBC。)

【讨论】:

【参考方案7】:

看到我的试用成功了,据说列表大小有潜在的限制。 列表 l = Arrays.asList(new Integer[]12496,12497,12498,12499); 地图参数 = Collections.singletonMap("goodsid",l);

    NamedParameterJdbcTemplate  namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(getJdbcTemplate().getDataSource());
    String sql = "SELECT bg.goodsid FROM beiker_goods bg WHERE bg.goodsid in(:goodsid)";
    List<Long> list = namedParameterJdbcTemplate.queryForList(sql, param2, Long.class);

【讨论】:

【参考方案8】:

我们可以使用不同的替代方法。

    执行单个查询 - 速度慢且不推荐 使用存储过程 - 特定于数据库 动态创建 PreparedStatement 查询 - 性能良好,但缓存的好处松散,需要重新编译 在 PreparedStatement 查询中使用 NULL - 我认为这是一种具有最佳性能的好方法。

查看有关这些here 的更多详细信息。

【讨论】:

【参考方案9】:

sormula 让这一切变得简单(参见Example 4):

ArrayList<Integer> partNumbers = new ArrayList<Integer>();
partNumbers.add(999);
partNumbers.add(777);
partNumbers.add(1234);

// set up
Database database = new Database(getConnection());
Table<Inventory> inventoryTable = database.getTable(Inventory.class);

// select operation for list "...WHERE PARTNUMBER IN (?, ?, ?)..."
for (Inventory inventory: inventoryTable.
    selectAllWhere("partNumberIn", partNumbers))    

    System.out.println(inventory.getPartNumber());

【讨论】:

【参考方案10】:

我能想到的一种方法是使用 java.sql.PreparedStatement 和一些陪审团操纵

PreparedStatement PreparedStmt = conn.prepareStatement("SELECT * FROM MYTABLE WHERE MYCOL in (?)");

...然后...

preparedStmt.setString(1, [你的字符串参数]);

http://java.sun.com/docs/books/tutorial/jdbc/basics/prepared.html

【讨论】:

这将不起作用,因为它可能会创建像 ... WHERE MYCOL IN ('2,3,5,6') 这样的查询,而这不是您想要做的。

以上是关于使用 JDBC 参数化 IN 子句的最佳方法是啥? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

重写大型 IN 子句的最高效方法是啥?

使用 MySql、PHP 和 ADODB 在准备好的语句中参数化 IN 子句

使用带有休眠 SQL 查询的多列的参数化 IN 子句

PHP,Python中的Mysql IN子句参数化

如何在单个 JDBC 连接中执行多个 JPA 查询

带有 IN 子句中参数列表的 PreparedStatement [重复]