使用 SimpleJdbc 類簡化 JDBC 操作

SimpleJdbcInsertSimpleJdbcCall 類透過利用可以透過 JDBC 驅動程式檢索的資料庫元資料提供了簡化的配置。這意味著您無需預先配置太多,儘管如果您希望在程式碼中提供所有詳細資訊,則可以覆蓋或關閉元資料處理。

使用 SimpleJdbcInsert 插入資料

我們首先介紹具有最少配置選項的 SimpleJdbcInsert 類。您應該在資料訪問層的初始化方法中例項化 SimpleJdbcInsert。對於此示例,初始化方法是 setDataSource 方法。您無需子類化 SimpleJdbcInsert 類。相反,您可以建立一個新例項並透過使用 withTableName 方法設定表名。此類的配置方法遵循 fluid 樣式,該樣式返回 SimpleJdbcInsert 例項,這使您可以連結所有配置方法。以下示例僅使用一個配置方法(我們稍後將顯示多個方法的示例)

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(3);
		parameters.put("id", actor.getId());
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		insertActor.execute(parameters);
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource).withTableName("t_actor")

	fun add(actor: Actor) {
		val parameters = mutableMapOf<String, Any>()
		parameters["id"] = actor.id
		parameters["first_name"] = actor.firstName
		parameters["last_name"] = actor.lastName
		insertActor.execute(parameters)
	}

	// ... additional methods
}

這裡使用的 execute 方法將一個普通的 java.util.Map 作為其唯一引數。這裡需要注意的重要一點是,用於 Map 的鍵必須與資料庫中定義的表的列名匹配。這是因為我們讀取元資料以構造實際的插入語句。

使用 SimpleJdbcInsert 檢索自動生成的鍵

下一個示例使用與上一個示例相同的插入,但不是傳入 id,而是檢索自動生成的鍵並將其設定到新的 Actor 物件上。在建立 SimpleJdbcInsert 時,除了指定表名之外,它還使用 usingGeneratedKeyColumns 方法指定生成的鍵列的名稱。以下清單顯示了它是如何工作的

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(2);
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor").usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = mapOf(
				"first_name" to actor.firstName,
				"last_name" to actor.lastName)
		val newId = insertActor.executeAndReturnKey(parameters);
		return actor.copy(id = newId.toLong())
	}

	// ... additional methods
}

使用第二種方法執行插入時,主要區別在於您不將 id 新增到 Map 中,並且呼叫 executeAndReturnKey 方法。這會返回一個 java.lang.Number 物件,您可以使用它建立域類中使用的數字型別的例項。您不能指望所有資料庫在這裡都返回特定的 Java 類。java.lang.Number 是您可以依賴的基類。如果您有多個自動生成的列或生成的非數字值,則可以使用從 executeAndReturnKeyHolder 方法返回的 KeyHolder

SimpleJdbcInsert 指定列

您可以透過使用 usingColumns 方法指定列名列表來限制插入的列,如以下示例所示

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingColumns("first_name", "last_name")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(2);
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor")
			.usingColumns("first_name", "last_name")
			.usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = mapOf(
				"first_name" to actor.firstName,
				"last_name" to actor.lastName)
		val newId = insertActor.executeAndReturnKey(parameters);
		return actor.copy(id = newId.toLong())
	}

	// ... additional methods
}

插入的執行與您依賴元資料來確定要使用哪些列的執行相同。

使用 SqlParameterSource 提供引數值

使用 Map 提供引數值效果很好,但它不是最方便使用的類。Spring 提供了 SqlParameterSource 介面的幾種實現,您可以改用它們。第一個是 BeanPropertySqlParameterSource,如果您有一個包含值的 JavaBean 相容類,它是一個非常方便的類。它使用相應的 getter 方法提取引數值。以下示例顯示瞭如何使用 BeanPropertySqlParameterSource

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor")
			.usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = BeanPropertySqlParameterSource(actor)
		val newId = insertActor.executeAndReturnKey(parameters)
		return actor.copy(id = newId.toLong())
	}

	// ... additional methods
}

另一個選項是 MapSqlParameterSource,它類似於 Map,但提供了更方便的 addValue 方法,可以連結。以下示例顯示瞭如何使用它

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		SqlParameterSource parameters = new MapSqlParameterSource()
				.addValue("first_name", actor.getFirstName())
				.addValue("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor")
			.usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = MapSqlParameterSource()
					.addValue("first_name", actor.firstName)
					.addValue("last_name", actor.lastName)
		val newId = insertActor.executeAndReturnKey(parameters)
		return actor.copy(id = newId.toLong())
	}

	// ... additional methods
}

如您所見,配置是相同的。只有執行程式碼需要更改以使用這些替代輸入類。

使用 SimpleJdbcCall 呼叫儲存過程

SimpleJdbcCall 類使用資料庫中的元資料查詢 inout 引數的名稱,這樣您就不必顯式宣告它們。如果您更喜歡宣告引數,或者您有無法自動對映到 Java 類的引數,則可以宣告引數。第一個示例顯示了一個簡單的過程,該過程僅從 MySQL 資料庫返回 VARCHARDATE 格式的標量值。該示例過程讀取指定的參與者條目,並以 out 引數的形式返回 first_namelast_namebirth_date 列。以下清單顯示了第一個示例

CREATE PROCEDURE read_actor (
	IN in_id INTEGER,
	OUT out_first_name VARCHAR(100),
	OUT out_last_name VARCHAR(100),
	OUT out_birth_date DATE)
BEGIN
	SELECT first_name, last_name, birth_date
	INTO out_first_name, out_last_name, out_birth_date
	FROM t_actor where id = in_id;
END;

in_id 引數包含您正在查詢的參與者的 idout 引數返回從表中讀取的資料。

您可以以類似於宣告 SimpleJdbcInsert 的方式宣告 SimpleJdbcCall。您應該在資料訪問層的初始化方法中例項化和配置該類。與 StoredProcedure 類相比,您無需建立子類,也無需宣告可在資料庫元資料中查詢的引數。以下 SimpleJdbcCall 配置示例使用上述儲存過程(除了 DataSource 之外,唯一的配置選項是儲存過程的名稱)

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		this.procReadActor = new SimpleJdbcCall(dataSource)
				.withProcedureName("read_actor");
	}

	public Actor readActor(Long id) {
		SqlParameterSource in = new MapSqlParameterSource()
				.addValue("in_id", id);
		Map out = procReadActor.execute(in);
		Actor actor = new Actor();
		actor.setId(id);
		actor.setFirstName((String) out.get("out_first_name"));
		actor.setLastName((String) out.get("out_last_name"));
		actor.setBirthDate((Date) out.get("out_birth_date"));
		return actor;
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val procReadActor = SimpleJdbcCall(dataSource)
			.withProcedureName("read_actor")


	fun readActor(id: Long): Actor {
		val source = MapSqlParameterSource().addValue("in_id", id)
		val output = procReadActor.execute(source)
		return Actor(
				id,
				output["out_first_name"] as String,
				output["out_last_name"] as String,
				output["out_birth_date"] as Date)
	}

		// ... additional methods
}

您為呼叫執行編寫的程式碼涉及建立一個包含 IN 引數的 SqlParameterSource。您必須將為輸入值提供的名稱與儲存過程中宣告的引數名稱匹配。大小寫不必匹配,因為您使用元資料來確定如何在儲存過程中引用資料庫物件。儲存過程源中指定的內容不一定是以其在資料庫中儲存的方式。某些資料庫將名稱轉換為全大寫,而其他資料庫使用小寫或使用指定的大小寫。

execute 方法採用 IN 引數並返回一個 Map,其中包含按名稱(如儲存過程中指定)鍵入的任何 out 引數。在這種情況下,它們是 out_first_nameout_last_nameout_birth_date

execute 方法的最後一部分建立了一個 Actor 例項,用於返回檢索到的資料。同樣,重要的是使用儲存過程中宣告的 out 引數的名稱。此外,儲存在結果對映中的 out 引數名稱的大小寫與資料庫中 out 引數名稱的大小寫匹配,這可能因資料庫而異。為了使您的程式碼更具可移植性,您應該執行不區分大小寫的查詢,或者指示 Spring 使用 LinkedCaseInsensitiveMap。要執行後者,您可以建立自己的 JdbcTemplate 並將 setResultsMapCaseInsensitive 屬性設定為 true。然後,您可以將此自定義的 JdbcTemplate 例項傳遞給 SimpleJdbcCall 的建構函式。以下示例顯示了此配置

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_actor");
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
		isResultsMapCaseInsensitive = true
	}).withProcedureName("read_actor")

	// ... additional methods
}

透過執行此操作,您可以避免返回的 out 引數名稱所用大小寫中的衝突。

顯式宣告用於 SimpleJdbcCall 的引數

在本章前面,我們描述瞭如何從元資料推斷引數,但您可以根據需要顯式宣告它們。您可以透過使用 declareParameters 方法建立和配置 SimpleJdbcCall 來實現,該方法將可變數量的 SqlParameter 物件作為輸入。有關如何定義 SqlParameter 的詳細資訊,請參閱下一節

如果使用的資料庫不是 Spring 支援的資料庫,則需要顯式宣告。目前,Spring 支援以下資料庫的儲存過程呼叫的元資料查詢:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。我們還支援 MySQL、Microsoft SQL Server 和 Oracle 的儲存函式元資料查詢。

您可以選擇顯式宣告一個、一些或所有引數。在不顯式宣告引數的情況下,仍將使用引數元資料。要繞過對潛在引數的所有元資料查詢處理,並且只使用宣告的引數,您可以將 withoutProcedureColumnMetaDataAccess 方法作為宣告的一部分進行呼叫。假設您為資料庫函式聲明瞭兩個或更多不同的呼叫簽名。在這種情況下,您呼叫 useInParameterNames 來指定要包含在給定簽名中的 IN 引數名稱列表。

以下示例顯示了完全宣告的過程呼叫並使用前面示例中的資訊

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_actor")
				.withoutProcedureColumnMetaDataAccess()
				.useInParameterNames("in_id")
				.declareParameters(
						new SqlParameter("in_id", Types.NUMERIC),
						new SqlOutParameter("out_first_name", Types.VARCHAR),
						new SqlOutParameter("out_last_name", Types.VARCHAR),
						new SqlOutParameter("out_birth_date", Types.DATE)
				);
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

		private val procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
			isResultsMapCaseInsensitive = true
		}).withProcedureName("read_actor")
				.withoutProcedureColumnMetaDataAccess()
				.useInParameterNames("in_id")
				.declareParameters(
						SqlParameter("in_id", Types.NUMERIC),
						SqlOutParameter("out_first_name", Types.VARCHAR),
						SqlOutParameter("out_last_name", Types.VARCHAR),
						SqlOutParameter("out_birth_date", Types.DATE)
	)

		// ... additional methods
}

這兩個示例的執行和最終結果是相同的。第二個示例顯式指定所有詳細資訊,而不是依賴元資料。

如何定義 SqlParameters

要為 SimpleJdbc 類和 RDBMS 操作類(在將 JDBC 操作建模為 Java 物件中介紹)定義引數,您可以使用 SqlParameter 或其子類之一。為此,您通常在建構函式中指定引數名稱和 SQL 型別。SQL 型別透過使用 java.sql.Types 常量指定。在本章前面,我們看到了類似於以下內容的宣告

  • Java

  • Kotlin

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

第一行與 SqlParameter 宣告一個 IN 引數。您可以將 IN 引數用於儲存過程呼叫以及使用 SqlQuery 及其子類(在瞭解 SqlQuery 中介紹)的查詢。

第二行(與 SqlOutParameter)宣告一個 out 引數,用於儲存過程呼叫。還有一個 SqlInOutParameter 用於 InOut 引數(向過程提供 IN 值並返回值的引數)。

只有宣告為 SqlParameterSqlInOutParameter 的引數才用於提供輸入值。這與 StoredProcedure 類不同,後者(出於向後相容性原因)允許為宣告為 SqlOutParameter 的引數提供輸入值。

對於 IN 引數,除了名稱和 SQL 型別之外,您可以為數字資料指定比例或為自定義資料庫型別指定型別名稱。對於 out 引數,您可以提供 RowMapper 來處理從 REF 遊標返回的行的對映。另一個選項是指定 SqlReturnType,它提供了定義返回值的自定義處理的機會。

使用 SimpleJdbcCall 呼叫儲存函式

您可以以幾乎與呼叫儲存過程相同的方式呼叫儲存函式,不同之處在於您提供函式名稱而不是過程名稱。您使用 withFunctionName 方法作為配置的一部分來指示您想要呼叫一個函式,並生成函式呼叫的相應字串。使用專門的呼叫 (executeFunction) 來執行函式,它將函式返回值作為指定型別的物件返回,這意味著您不必從結果對映中檢索返回值。類似的便利方法(名為 executeObject)也適用於只有一個 out 引數的儲存過程。以下示例(針對 MySQL)基於名為 get_actor_name 的儲存函式,該函式返回參與者的全名

CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
	DECLARE out_name VARCHAR(200);
	SELECT concat(first_name, ' ', last_name)
		INTO out_name
		FROM t_actor where id = in_id;
	RETURN out_name;
END;

要呼叫此函式,我們再次在初始化方法中建立 SimpleJdbcCall,如以下示例所示

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall funcGetActorName;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
				.withFunctionName("get_actor_name");
	}

	public String getActorName(Long id) {
		SqlParameterSource in = new MapSqlParameterSource()
				.addValue("in_id", id);
		String name = funcGetActorName.executeFunction(String.class, in);
		return name;
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val jdbcTemplate = JdbcTemplate(dataSource).apply {
		isResultsMapCaseInsensitive = true
	}
	private val funcGetActorName = SimpleJdbcCall(jdbcTemplate)
			.withFunctionName("get_actor_name")

	fun getActorName(id: Long): String {
		val source = MapSqlParameterSource().addValue("in_id", id)
		return funcGetActorName.executeFunction(String::class.java, source)
	}

	// ... additional methods
}

使用的 executeFunction 方法返回一個 String,其中包含函式呼叫的返回值。

SimpleJdbcCall 返回 ResultSet 或 REF 遊標

呼叫返回結果集的儲存過程或函式有點棘手。有些資料庫在 JDBC 結果處理期間返回結果集,而有些資料庫需要明確註冊特定型別的 out 引數。這兩種方法都需要額外的處理來遍歷結果集並處理返回的行。使用 SimpleJdbcCall,您可以使用 returningResultSet 方法並宣告一個 RowMapper 實現,用於特定引數。如果在結果處理期間返回結果集,則沒有定義名稱,因此返回的結果必須與您宣告 RowMapper 實現的順序匹配。指定的名稱仍用於將處理後的結果列表儲存在從 execute 語句返回的結果對映中。

下一個示例(針對 MySQL)使用一個儲存過程,該過程不帶 IN 引數並返回 t_actor 表中的所有行

CREATE PROCEDURE read_all_actors()
BEGIN
 SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;

要呼叫此過程,您可以宣告 RowMapper。由於您要對映到的類遵循 JavaBean 規則,因此您可以使用透過在 newInstance 方法中傳入所需的類來建立的 BeanPropertyRowMapper。以下示例顯示瞭如何執行此操作

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadAllActors;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_all_actors")
				.returningResultSet("actors",
				BeanPropertyRowMapper.newInstance(Actor.class));
	}

	public List getActorsList() {
		Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
		return (List) m.get("actors");
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

		private val procReadAllActors = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
			isResultsMapCaseInsensitive = true
		}).withProcedureName("read_all_actors")
				.returningResultSet("actors",
						BeanPropertyRowMapper.newInstance(Actor::class.java))

	fun getActorsList(): List<Actor> {
		val m = procReadAllActors.execute(mapOf<String, Any>())
		return m["actors"] as List<Actor>
	}

	// ... additional methods
}

execute 呼叫傳入一個空的 Map,因為此呼叫不帶任何引數。然後從結果對映中檢索參與者列表並返回給呼叫者。

© . This site is unofficial and not affiliated with VMware.