将 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 接受一个名称和在 java.sql.Types 中定义的 JDBC 类型。定义完所有参数后,您可以调用 compile() 方法,以便可以准备语句并在以后运行。此类在编译后是线程安全的,因此,只要在初始化 DAO 时创建这些实例,就可以将它们保留为实例变量并重复使用。以下示例显示了如何定义这样的类

  • Java

  • Kotlin

private ActorMappingQuery actorMappingQuery;

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

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

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

前面的示例中的方法检索具有作为唯一参数传入的id的客户。由于我们只希望返回一个对象,因此我们使用id作为参数调用findObject便捷方法。如果我们有一个返回对象列表并接受其他参数的查询,我们将使用一个接受作为可变参数传入的参数值数组的execute方法。以下示例显示了这样的方法

  • 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 映射到 Title 域对象,对于提供的 ResultSet 中的每一行,如下所示

  • 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 映射到 Genre 域对象,对于提供的 ResultSet 中的每一行,如下所示

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