Skip to content

将 JDBC 操作建模为 Java 对象 (Modeling JDBC Operations as Java Objects) ​

org.springframework.jdbc.object 包包含了一些类,让你能以更面向对象的方式访问数据库。例如,你可以运行查询并将结果作为列表返回,其中包含业务对象,关系列数据已映射到业务对象的属性上。你还可以运行存储过程,以及运行更新、删除和插入语句。

注意

许多 Spring 开发者认为,下面描述的各种 RDBMS 操作类(StoredProcedure 类除外)通常可以用直接的 JdbcTemplate 调用来代替。通常情况下,编写一个直接调用 JdbcTemplate 方法的 DAO 方法会更简单(而不是将查询封装为一个完整的类)。

但是,如果你发现使用这些 RDBMS 操作类能带来可衡量的价值,你应该继续使用它们。

理解 SqlQuery ​

SqlQuery 是一个可重用、线程安全的类,它封装了一个 SQL 查询。子类必须实现 newRowMapper(..) 方法,以提供一个 RowMapper 实例。该实例可以在遍历查询执行期间创建的 ResultSet 时,每行创建一个对象。SqlQuery 类很少直接使用,因为 MappingSqlQuery 子类为将行映射到 Java 类提供了更方便的实现。其他继承 SqlQuery 的实现包括 MappingSqlQueryWithParameters 和 UpdatableSqlQuery。

使用 MappingSqlQuery ​

MappingSqlQuery 是一个可重用的查询,具体的子类必须实现抽象方法 mapRow(..),将提供的 ResultSet 的每一行转换为指定类型的对象。以下示例展示了一个自定义查询,它将 t_actor 关系中的数据映射到 Actor 类的实例:

java
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;
	}
}
kotlin
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。在构造函数中,你可以通过 declareParameter 声明每个参数,并调用 compile() 方法使语句可以被预编译并随后运行。一旦编译完成,该类就是线程安全的。

使用示例如下:

java
private ActorMappingQuery actorMappingQuery;

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

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

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

使用 SqlUpdate ​

SqlUpdate 类封装了一个 SQL 更新。与查询一样,更新对象是可重用的,且可以拥有参数。该类是具体的,虽然可以被子类化以添加自定义更新方法,但你也可以直接通过设置 SQL 和声明参数来使用它。

java
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();
	}

	public int execute(int id, int rating) {
		return update(rating, id);
	}
}
kotlin
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()
	}

	fun execute(id: Int, rating: Int): Int {
		return update(rating, id)
	}
}

使用 StoredProcedure ​

StoredProcedure 类是 RDBMS 存储过程的对象抽象的抽象基类。你要使用的存储过程名通过 sql 属性指定。

以下是一个简单的 DAO 示例,使用 StoredProcedure 调用 Oracle 数据库自带的 sysdate() 函数:

java
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() {
			Map<String, Object> results = execute(new HashMap<String, Object>());
			return (Date) results.get("date");
		}
	}
}
kotlin
class StoredProcedureDao(dataSource: DataSource) {

	private val getSysdate = GetSysdateProcedure(dataSource)

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

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

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

		fun execute(): Date {
			val results = execute(mutableMapOf<String, Any>())
			return results["date"] as Date
		}
	}
}

补充教学 ​

1. 为什么要“对象化”? ​

如果你习惯了 Hibernate 或 JPA 这种“一切皆对象”的思维方式,你可能会觉得 JdbcTemplate 的字符串 SQL 略显粗糙。 RDBMS 操作类的初衷就是将 SQL 操作封装成一个一等公民(First-class citizen)对象。 这样做的好处是:

  • 自包含:SQL 语句、参数声明、结果映射(RowMapper)全部内聚在一个类中。
  • 强类型:你可以为这个类定义专门的 execute(...) 方法,提供类型安全的参数检查。
  • 可复用:这些类通常在 DAO 初始化时创建一次,之后在整个应用生命周期内被反复调用。

2. 何时该回避这种方式? ​

正如官方注意所说,现代 Spring 开发中,JdbcTemplate 几乎可以覆盖 80% 的 RDBMS 操作类场景,且代码更加简洁灵活。

  • 如果你的 SQL 只有一行,写一个专门的 SqlUpdate 子类可能显得过于笨重。
  • 如果你的 RowMapper 逻辑很简单,直接在 jdbcTemplate.query 里写个 Lambda 表达式会轻便得多。

结论:除非你的存储过程逻辑异常复杂,或者你非常推崇极致的面向对象封装风格,否则建议优先考虑 JdbcTemplate 或 JdbcClient。

3. compile() 的重要性 ​

在这些对象化的类中,compile() 方法就像是最后一道哨位。 它会验证你是否提供了 DataSource 和 SQL,并根据声明的参数生成底层的 PreparedStatement 结构。一旦编译完成,该对象就不再允许修改配置,从而保证了多线程环境下的安全性。如果你忘记调用它,在执行时 Spring 会抛出异常提示你。

Based on Spring Framework.