将 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 类的实例:
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。在构造函数中,你可以通过 declareParameter 声明每个参数,并调用 compile() 方法使语句可以被预编译并随后运行。一旦编译完成,该类就是线程安全的。
使用示例如下:
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)使用 SqlUpdate
SqlUpdate 类封装了一个 SQL 更新。与查询一样,更新对象是可重用的,且可以拥有参数。该类是具体的,虽然可以被子类化以添加自定义更新方法,但你也可以直接通过设置 SQL 和声明参数来使用它。
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);
}
}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() 函数:
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");
}
}
}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 会抛出异常提示你。