JDBC 批次操作

如果您將多個呼叫批次處理到相同的預備語句,大多數 JDBC 驅動程式都能提供更佳的效能。透過將更新分組為批次,您可以限制往返資料庫的次數。

使用 JdbcTemplate 的基本批次操作

您可以透過實作特殊介面 BatchPreparedStatementSetter 的兩個方法,並將該實作作為第二個參數傳遞到您的 batchUpdate 方法呼叫中,來完成 JdbcTemplate 批次處理。您可以使用 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 方法可讓您發出批次結束的訊號。

使用物件列表的批次操作

JdbcTemplateNamedParameterJdbcTemplate 都提供了一種提供批次更新的替代方法。您無需實作特殊的批次介面,而是在呼叫中以列表形式提供所有參數值。框架會迴圈處理這些值並使用內部預備語句設定器。API 會有所不同,具體取決於您是否使用具名參數。對於具名參數,您需要提供 SqlParameterSource 陣列,每個批次成員一個條目。您可以使用 SqlParameterSourceUtils.createBatch 便利方法來建立此陣列,傳入 Bean 樣式物件陣列(getter 方法對應於參數)、以 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 語句中定義的順序相同。

以下範例與先前的範例相同,不同之處在於它使用傳統 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 上自動設定值時,每個值的對應 JDBC 類型都需要從給定的 Java 類型衍生而來。雖然這通常運作良好,但仍可能存在問題(例如,對於 Map 包含的 null 值)。在這種情況下,Spring 預設會呼叫 ParameterMetaData.getParameterType,這對於您的 JDBC 驅動程式來說可能很耗費資源。如果您在應用程式中遇到特定的效能問題,您應該使用最新的驅動程式版本,並考慮將 spring.jdbc.getParameterType.ignore 屬性設定為 true(作為 JVM 系統屬性或透過 SpringProperties 機制)。

從 6.1.2 版開始,Spring 繞過了 PostgreSQL 和 MS SQL Server 上的預設 getParameterType 解析。這是一個常見的最佳化,旨在避免僅為了參數類型解析而進一步往返 DBMS,已知這在 PostgreSQL 和 MS SQL Server 上會產生非常顯著的差異,尤其是在批次操作中。如果您碰巧看到副作用,例如,當將位元組陣列設定為 null 而沒有明確的類型指示時,您可以明確地將 spring.jdbc.getParameterType.ignore=false 旗標設定為系統屬性(如上所述),以還原完整的 getParameterType 解析。

或者,您可以考慮明確指定對應的 JDBC 類型,可以透過 BatchPreparedStatementSetter(如先前所示)、透過給定基於 List<Object[]> 呼叫的明確類型陣列、透過自訂 MapSqlParameterSource 實例上的 registerSqlType 呼叫、透過從 Java 宣告的屬性類型衍生 SQL 類型的 BeanPropertySqlParameterSource(即使對於空值也是如此),或透過提供個別 SqlParameterValue 實例而不是純粹的空值。

使用多個批次的批次操作

先前的批次更新範例處理的批次非常大,以至於您想要將它們分解為幾個較小的批次。您可以使用先前提及的方法透過多次呼叫 batchUpdate 方法來完成此操作,但現在有一種更方便的方法。此方法除了 SQL 語句之外,還接受一個包含參數的物件 Collection、每個批次要進行的更新次數,以及一個 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