將 JDBC 操作建模為 Java 物件

org.springframework.jdbc.object 套件包含的類別可讓您以更物件導向的方式存取資料庫。例如,您可以執行查詢並將結果以清單形式傳回,其中清單包含業務物件,而關聯式資料行資料會映射到業務物件的屬性。您也可以執行預存程序並執行更新、刪除和插入陳述式。

許多 Spring 開發人員認為,以下描述的各種 RDBMS 操作類別(除了 StoredProcedure 類別之外)通常可以用直接的 JdbcTemplate 呼叫來取代。通常,編寫直接在 JdbcTemplate 上呼叫方法的 DAO 方法(而不是將查詢封裝為完整的類別)更簡單。

但是,如果您從使用 RDBMS 操作類別中獲得可衡量的價值,則應繼續使用這些類別。

理解 SqlQuery

SqlQuery 是一個可重複使用、執行緒安全的類別,它封裝了一個 SQL 查詢。子類別必須實作 newRowMapper(..) 方法,以提供一個 RowMapper 實例,該實例可以為從迭代查詢執行期間建立的 ResultSet 獲得的每一列建立一個物件。SqlQuery 類別很少直接使用,因為 MappingSqlQuery 子類別為將列映射到 Java 類別提供了更方便的實作。其他擴充 SqlQuery 的實作包括 MappingSqlQueryWithParametersUpdatableSqlQuery

使用 MappingSqlQuery

MappingSqlQuery 是一個可重複使用的查詢,其中具體的子類別必須實作抽象的 mapRow(..) 方法,以將提供的 ResultSet 的每一列轉換為指定類型的物件。以下範例顯示了一個自訂查詢,該查詢將來自 t_actor 關聯的資料映射到 Actor 類別的實例

  • Java

  • Kotlin

public class ActorMappingQuery extends MappingSqlQuery<Actor> {

	public ActorMappingQuery(DataSource ds) {
		super(ds, "select id, first_name, last_name from t_actor where id = ?");
		declareParameter(new SqlParameter("id", Types.INTEGER));
		compile();
	}

	@Override
	protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
		Actor actor = new Actor();
		actor.setId(rs.getLong("id"));
		actor.setFirstName(rs.getString("first_name"));
		actor.setLastName(rs.getString("last_name"));
		return actor;
	}
}
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(ds, "select id, first_name, last_name from t_actor where id = ?") {

	init {
		declareParameter(SqlParameter("id", Types.INTEGER))
		compile()
	}

	override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
			rs.getLong("id"),
			rs.getString("first_name"),
			rs.getString("last_name")
	)
}

該類別擴充了以 Actor 類型參數化的 MappingSqlQuery。此客戶查詢的建構子採用 DataSource 作為唯一參數。在此建構子中,您可以使用 DataSource 和應執行以檢索此查詢的列的 SQL,在父類別上呼叫建構子。此 SQL 用於建立 PreparedStatement,因此它可能包含要在執行期間傳入的任何參數的佔位符。您必須使用 declareParameter 方法傳入 SqlParameter 來宣告每個參數。SqlParameter 採用名稱和 JDBC 類型,如 java.sql.Types 中定義的那樣。在定義所有參數後,您可以呼叫 compile() 方法,以便可以準備陳述式並稍後執行。此類別在編譯後是執行緒安全的,因此,只要在初始化 DAO 時建立這些實例,它們就可以作為實例變數保留並重複使用。以下範例示範如何定義這樣的類別

  • Java

  • Kotlin

private ActorMappingQuery actorMappingQuery;

@Autowired
public void setDataSource(DataSource dataSource) {
	this.actorMappingQuery = new ActorMappingQuery(dataSource);
}

public Actor getActor(Long id) {
	return actorMappingQuery.findObject(id);
}
private val actorMappingQuery = ActorMappingQuery(dataSource)

fun getActor(id: Long) = actorMappingQuery.findObject(id)

上述範例中的方法檢索具有作為唯一參數傳入的 id 的演員。由於我們只希望傳回一個物件,因此我們使用 id 作為參數呼叫 findObject 便利方法。如果我們有一個傳回物件列表並採用其他參數的查詢,我們將使用其中一個 execute 方法,該方法採用作為 varargs 傳入的參數值陣列。以下範例顯示了這樣的方法

  • Java

  • Kotlin

public List<Actor> searchForActors(int age, String namePattern) {
	return actorSearchMappingQuery.execute(age, namePattern);
}
fun searchForActors(age: Int, namePattern: String) =
			actorSearchMappingQuery.execute(age, namePattern)

使用 SqlUpdate

SqlUpdate 類別封裝了 SQL 更新。與查詢一樣,更新物件是可重複使用的,並且與所有 RdbmsOperation 類別一樣,更新可以具有參數,並且在 SQL 中定義。此類別提供了許多與查詢物件的 execute(..) 方法類似的 update(..) 方法。SqlUpdate 類別是具體的。它可以被子類別化——例如,新增自訂更新方法。但是,您不必子類別化 SqlUpdate 類別,因為可以通過設定 SQL 和宣告參數輕鬆地對其進行參數化。以下範例建立了一個名為 execute 的自訂更新方法

  • Java

  • Kotlin

import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

	public UpdateCreditRating(DataSource ds) {
		setDataSource(ds);
		setSql("update customer set credit_rating = ? where id = ?");
		declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
		declareParameter(new SqlParameter("id", Types.NUMERIC));
		compile();
	}

	/**
	 * @param id for the Customer to be updated
	 * @param rating the new value for credit rating
	 * @return number of rows updated
	 */
	public int execute(int id, int rating) {
		return update(rating, id);
	}
}
import java.sql.Types
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.SqlUpdate

class UpdateCreditRating(ds: DataSource) : SqlUpdate() {

	init {
		setDataSource(ds)
		sql = "update customer set credit_rating = ? where id = ?"
		declareParameter(SqlParameter("creditRating", Types.NUMERIC))
		declareParameter(SqlParameter("id", Types.NUMERIC))
		compile()
	}

	/**
	 * @param id for the Customer to be updated
	 * @param rating the new value for credit rating
	 * @return number of rows updated
	 */
	fun execute(id: Int, rating: Int): Int {
		return update(rating, id)
	}
}

使用 StoredProcedure

StoredProcedure 類別是 RDBMS 預存程序的物件抽象的 abstract 父類別。

繼承的 sql 屬性是 RDBMS 中預存程序的名稱。

要為 StoredProcedure 類別定義參數,您可以使用 SqlParameter 或其子類別之一。您必須在建構子中指定參數名稱和 SQL 類型,如下面的程式碼片段所示

  • 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),

SQL 類型使用 java.sql.Types 常數指定。

第一行(帶有 SqlParameter)宣告了一個 IN 參數。您可以使用 IN 參數進行預存程序呼叫,以及使用 SqlQuery 及其子類別(在 理解 SqlQuery 中涵蓋)的查詢。

第二行(帶有 SqlOutParameter)宣告了一個 out 參數,用於預存程序呼叫。還有一個 SqlInOutParameter 用於 InOut 參數(提供程序的 in 值並傳回值的參數)。

對於 in 參數,除了名稱和 SQL 類型外,您還可以為數字資料指定比例,或為自訂資料庫類型指定類型名稱。對於 out 參數,您可以提供一個 RowMapper 來處理從 REF 指標傳回的列的映射。另一個選項是指定一個 SqlReturnType,它允許您定義傳回值的自訂處理。

下一個簡單 DAO 的範例使用 StoredProcedure 呼叫一個函數 (sysdate()),該函數隨附於任何 Oracle 資料庫。要使用預存程序功能,您必須建立一個擴充 StoredProcedure 的類別。在此範例中,StoredProcedure 類別是一個內部類別。但是,如果您需要重複使用 StoredProcedure,您可以將其宣告為頂層類別。此範例沒有輸入參數,但輸出參數使用 SqlOutParameter 類別宣告為日期類型。execute() 方法執行程序並從結果 Map 中提取傳回的日期。結果 Map 為每個宣告的輸出參數(在本例中只有一個)都有一個條目,使用參數名稱作為索引鍵。以下列表顯示了我們的自訂 StoredProcedure 類別

  • Java

  • Kotlin

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class StoredProcedureDao {

	private GetSysdateProcedure getSysdate;

	@Autowired
	public void init(DataSource dataSource) {
		this.getSysdate = new GetSysdateProcedure(dataSource);
	}

	public Date getSysdate() {
		return getSysdate.execute();
	}

	private class GetSysdateProcedure extends StoredProcedure {

		private static final String SQL = "sysdate";

		public GetSysdateProcedure(DataSource dataSource) {
			setDataSource(dataSource);
			setFunction(true);
			setSql(SQL);
			declareParameter(new SqlOutParameter("date", Types.DATE));
			compile();
		}

		public Date execute() {
			// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
			Map<String, Object> results = execute(new HashMap<String, Object>());
			Date sysdate = (Date) results.get("date");
			return sysdate;
		}
	}

}
import java.sql.Types
import java.util.Date
import java.util.Map
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

class StoredProcedureDao(dataSource: DataSource) {

	private val SQL = "sysdate"

	private val getSysdate = GetSysdateProcedure(dataSource)

	val sysdate: Date
		get() = getSysdate.execute()

	private inner class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() {

		init {
			setDataSource(dataSource)
			isFunction = true
			sql = SQL
			declareParameter(SqlOutParameter("date", Types.DATE))
			compile()
		}

		fun execute(): Date {
			// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
			val results = execute(mutableMapOf<String, Any>())
			return results["date"] as Date
		}
	}
}

以下 StoredProcedure 範例有兩個輸出參數(在本例中為 Oracle REF 指標)

  • Java

  • Kotlin

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

	private static final String SPROC_NAME = "AllTitlesAndGenres";

	public TitlesAndGenresStoredProcedure(DataSource dataSource) {
		super(dataSource, SPROC_NAME);
		declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
		declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
		compile();
	}

	public Map<String, Object> execute() {
		// again, this sproc has no input parameters, so an empty Map is supplied
		return super.execute(new HashMap<String, Object>());
	}
}
import java.util.HashMap
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

class TitlesAndGenresStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {

	companion object {
		private const val SPROC_NAME = "AllTitlesAndGenres"
	}

	init {
		declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
		declareParameter(SqlOutParameter("genres", OracleTypes.CURSOR, GenreMapper()))
		compile()
	}

	fun execute(): Map<String, Any> {
		// again, this sproc has no input parameters, so an empty Map is supplied
		return super.execute(HashMap<String, Any>())
	}
}

請注意,已在 TitlesAndGenresStoredProcedure 建構子中使用的 declareParameter(..) 方法的重載變體是如何傳入 RowMapper 實作實例的。這是一種非常方便且強大的方式來重複使用現有功能。接下來的兩個範例提供了兩個 RowMapper 實作的程式碼。

TitleMapper 類別將 ResultSet 映射到提供的 ResultSet 中每一列的 Title 網域物件,如下所示

  • Java

  • Kotlin

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;

public final class TitleMapper implements RowMapper<Title> {

	public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
		Title title = new Title();
		title.setId(rs.getLong("id"));
		title.setName(rs.getString("name"));
		return title;
	}
}
import java.sql.ResultSet
import com.foo.domain.Title
import org.springframework.jdbc.core.RowMapper

class TitleMapper : RowMapper<Title> {

	override fun mapRow(rs: ResultSet, rowNum: Int) =
			Title(rs.getLong("id"), rs.getString("name"))
}

GenreMapper 類別將 ResultSet 映射到提供的 ResultSet 中每一列的 Genre 網域物件,如下所示

  • Java

  • Kotlin

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;

public final class GenreMapper implements RowMapper<Genre> {

	public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
		return new Genre(rs.getString("name"));
	}
}
import java.sql.ResultSet
import com.foo.domain.Genre
import org.springframework.jdbc.core.RowMapper

class GenreMapper : RowMapper<Genre> {

	override fun mapRow(rs: ResultSet, rowNum: Int): Genre {
		return Genre(rs.getString("name"))
	}
}

要將參數傳遞給在其 RDBMS 定義中具有一個或多個輸入參數的預存程序,您可以編寫一個強型別的 execute(..) 方法,該方法將委派給父類別中未型別的 execute(Map) 方法,如下列範例所示

  • Java

  • Kotlin

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

	private static final String SPROC_NAME = "TitlesAfterDate";
	private static final String CUTOFF_DATE_PARAM = "cutoffDate";

	public TitlesAfterDateStoredProcedure(DataSource dataSource) {
		super(dataSource, SPROC_NAME);
		declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
		declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
		compile();
	}

	public Map<String, Object> execute(Date cutoffDate) {
		Map<String, Object> inputs = new HashMap<String, Object>();
		inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
		return super.execute(inputs);
	}
}
import java.sql.Types
import java.util.Date
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.StoredProcedure

class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {

	companion object {
		private const val SPROC_NAME = "TitlesAfterDate"
		private const val CUTOFF_DATE_PARAM = "cutoffDate"
	}

	init {
		declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE))
		declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
		compile()
	}

	fun execute(cutoffDate: Date) = super.execute(
			mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate))
}