JDBC批量操作
大多数JDBC驱动程序在将多次对同一预编译语句的调用批量处理时能够提供更佳的性能。通过将更新操作分组为批次,可以减少与数据库之间的往返次数。
使用JdbcTemplate进行基本批量操作
你通过实现一个特殊接口BatchPreparedStatementSetter的两种方法来完成JdbcTemplate的批处理,并将这种实现作为第二个参数传递给batchUpdate方法的调用中。你可以使用getBatchSize方法来获取当前批次的大小。你可以使用setValues方法来设置预处理语句参数的值。这个方法会被调用getBatchSize方法中指定的次数。以下示例根据列表中的条目更新t_actor表,整个列表被用作一个批次:
- Java
- Kotlin
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
object: BatchPreparedStatementSetter {
override fun setValues(ps: PreparedStatement, i: Int) {
ps.setString(1, actors[i].firstName)
ps.setString(2, actors[i].lastName)
ps.setLong(3, actors[i].id)
}
override fun getBatchSize() = actors.size
})
}
// ... additional methods
}
如果你处理一系列更新或从文件中读取数据,你可能有一个首选的批量大小,但最后一个批次可能没有那么多条记录。在这种情况下,你可以使用InterruptibleBatchPreparedStatementSetter接口,该接口允许你在输入源耗尽时中断批量处理。isBatchExhausted方法可以用来指示批次结束。
对对象列表的批量操作
JdbcTemplate 和 NamedParameterJdbcTemplate 都提供了另一种执行批量更新的方法。无需实现专门的批量接口,你只需在调用时将所有参数值作为列表提供即可。框架会遍历这些值,并使用内部准备好的语句设置器来执行操作。API 的具体实现会根据你是否使用命名参数而有所不同。对于命名参数,你需要提供一个 SqlParameterSource 的数组,每个批量操作项对应一个数组元素。你可以使用 SqlParameterSourceUtils.createBatch 这种便捷方法来创建这个数组,该方法可以接受以下类型的参数:包含参数对应的 getter 方法的 bean 风格对象数组、以 String 作为键的 Map 实例(其值即为相应的参数),或者两者的混合形式。
以下示例展示了使用命名参数进行批量更新的操作:
- Java
- Kotlin
public class JdbcActorDao implements ActorDao {
private NamedParameterTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int[] batchUpdate(List<Actor> actors) {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
对于使用经典?占位符的SQL语句,你需要传递一个包含对象数组的列表,该对象数组中的每个元素对应SQL语句中的一个占位符的值。这个对象数组必须为SQL语句中的每个占位符提供一个条目,并且这些条目的顺序必须与SQL语句中定义的顺序相同。
以下示例与前面的示例相同,不同之处在于它使用了经典的JDBC ? 占位符:
- Java
- Kotlin
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
List<Object[]> batch = new ArrayList<>();
for (Actor actor : actors) {
Object[] values = new Object[] {
actor.getFirstName(), actor.getLastName(), actor.getId()};
batch.add(values);
}
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
batch);
}
// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): IntArray {
val batch = mutableListOf<Array<Any>>()
for (actor in actors) {
batch.add(arrayOf(actor.firstName, actor.lastName, actor.id))
}
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?", batch)
}
// ... additional methods
}
我们之前描述的所有批量更新方法都会返回一个int类型的数组,该数组包含了每个批量操作所影响的行数。这个计数是由JDBC驱动程序提供的。如果无法获得这个计数,JDBC驱动程序将返回值-2。
在这种情况下,由于PreparedStatement上的值是自动设置的,因此需要从给定的Java类型中推导出每个值对应的JDBC类型。虽然这通常能够正常工作,但也可能存在一些问题(例如,当Map中包含null值时)。默认情况下,Spring会调用ParameterMetaData.getParameterType方法,但对于某些JDBC驱动程序来说,这种方法可能会带来较大的性能开销。如果你的应用程序遇到了特定的性能问题,应该使用较新版本的驱动程序,并考虑将spring.jdbc-parameterType.ignore属性设置为true(可以通过JVM系统属性或SpringProperties机制来实现)。
从6.1.2版本开始,Spring在处理PostgreSQL和MS SQL Server时绕过了默认的getParameterType解析机制。这是一种常见的优化手段,可以避免仅仅为了确定参数类型而与数据库管理系统(DBMS)进行额外的往返通信。尤其在批量操作中,这种优化能够显著提升性能。如果你遇到了某些副作用(例如,在没有指定具体类型的情况下将字节数组设置为null时),可以显式地将spring.jdbc_parameterType.ignore属性设置为false(如上所述),以恢复完整的parameterType解析功能。
另外,你也可以考虑显式指定相应的JDBC类型:可以通过BatchPreparedStatementSetter来实现(如前文所示);可以通过向基于List<Object[]>的调用方法传递显式的类型数组;可以通过在自定义的MapSqlParameterSource实例上调用registerSqlType方法;或者使用BeanPropertySqlParameterSource,即使对于null值,它也能从Java声明的属性类型中推导出相应的SQL类型;也可以通过提供单独的SqlParameterValue实例来代替简单的null值。
多批次批量操作
前面的批量更新示例涉及那些非常庞大的数据批次,以至于你需要将它们拆分成几个较小的批次。你可以通过之前提到的方法来实现这一点,即多次调用 batchUpdate 方法,但现在有一种更为便捷的方法。这种方法除了需要 SQL 语句外,还需要一个包含参数的对象集合、每个批次要执行的更新次数,以及一个 ParameterizedPreparedStatementSetter 来设置预编译语句中参数的值。框架会遍历提供的值,并将更新调用拆分成指定大小的批次来执行。
以下示例展示了一个使用100为批量大小的批量更新操作:
- Java
- Kotlin
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[][] batchUpdate(final Collection<Actor> actors) {
int[][] updateCounts = jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors,
100,
(PreparedStatement ps, Actor actor) -> {
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
});
return updateCounts;
}
// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource)
fun batchUpdate(actors: List<Actor>): Array<IntArray> {
return jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors, 100) { ps, argument ->
ps.setString(1, argument.firstName)
ps.setString(2, argument.lastName)
ps.setLong(3, argument.id)
}
}
// ... additional methods
}
此调用的批量更新方法返回一个int数组的数组,其中每个批次都包含一个数组,该数组记录了每次更新所影响的行数。顶层数组的长度表示运行的批次总数,而第二层数组的长度则表示该批次中的更新次数。每个批次中的更新次数应与为所有批次提供的批次大小一致(最后一个批次可能除外,其更新次数可能会较少),具体取决于所提供的更新对象的总数。每次更新语句的更新计数由JDBC驱动程序报告;如果无法获得该计数,JDBC驱动程序将返回值-2。