Modeling JDBC Operations as Java Objects
org.springframework.jdbc.object
包包含了让你以更加面向对象的方式访问数据库的类。例如,你可以运行查询,并将结果以列表的形式返回,其中包含业务对象,并将关系列数据映射到业务对象的属性上。你也可以运行存储过程和运行更新、删除和插入语句。
:::info 许多 Spring 开发者认为,下面描述的各种 RDBMS 操作类(除了 StoredProcedure 类之外)通常可以用直接调用 JdbcTemplate 来代替。通常情况下,写一个直接调用 JdbcTemplate 上的方法的 DAO 方法更简单(而不是把查询封装成一个完整的类)。
然而,如果你从使用 RDBMS 操作类中获得了可衡量的价值,你应该继续使用这些类。 :::
了解 SqlQuery
Understanding SqlQuery
SqlQuery 是一个可重用的、线程安全的类,它封装了一个 SQL 查询。子类必须实现 newRowMapper(..)
方法,以提供一个 RowMapper 实例,该实例可以为每条记录创建一个对象,这些记录是在执行查询时创建的结果集上迭代得到的。SqlQuery 类很少被直接使用,因为 MappingSqlQuery 子类提供了一个更方便的实现,用于将行映射到 Java 类。扩展 SqlQuery 的其他实现是 MappingSqlQueryWithParameters 和 UpdatableSqlQuery。
使用 MappingSqlQuery
Using MappingSqlQuery
MappingSqlQuery 是一个可重用的查询,其中具体的子类必须实现抽象的 mapRow(..)
方法,以将提供的 ResultSet 的每一行转换成指定类型的对象。下面的例子显示了一个自定义查询,它将 t_actor 关系中的数据映射到 Actor 类的一个实例:
public class ActorMappingQuery extends MappingSqlQuery<Actor> {
public ActorMappingQuery(DataSource ds) {
// 这里需要一个 id 参数
super(ds, "select id, first_name, last_name from t_actor where id = ?");
// 定义了 ID 参数的类型
declareParameter(new SqlParameter("id", Types.INTEGER));
compile();
}
// 将结果自定义处理成 bean
@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;
}
}
该类扩展了 MappingSqlQuery,其参数为 Actor 类型。这个客户查询的构造函数需要一个 DataSource 作为唯一的参数。在这个构造函数中,你可以用数据源和应该运行的 SQL 来调用超类的构造函数,以检索这个查询的行。这个 SQL 是用来创建 PreparedStatement 的,所以它可以包含执行过程中要传入的任何参数的占位符。你必须通过使用 declareParameter 方法来声明每个参数,并传入一个 SqlParameter。SqlParameter 需要一个名字,以及java.sql.Types 中定义的 JDBC 类型。在你定义了所有的参数之后,你可以调用 compile()
方法,这样语句就可以被准备好,随后就可以运行。这个类在编译后是 线程安全 的,所以,只要这些实例是在 DAO 初始化时创建的,它们就可以作为实例变量保留并被重复使用。下面的例子展示了如何定义这样一个类:
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
// 这里需要调用方法 传入 sql 中需要的参数,触发查询,并获得信息
// 这里的 Customer 可能是 Actor 的父类
return actorMappingQuery.findObject(id);
}
前面的例子中的方法是用作为唯一参数传入的 id 来检索客户。因为我们只想返回一个对象,所以我们调用以 id 为参数的 findObject
方便方法。如果我们有一个返回对象列表的查询,并且需要额外的参数,我们将使用其中一个执行方法,该方法需要一个作为 varargs 传入的参数值数组。下面的例子显示了这样一个方法:
public List<Actor> searchForActors(int age, String namePattern) {
// execute 方法接收 一个可变参数作为参数值,以这种方式就可以按照 SQL 里面占位符 ? 出现的顺序传入对应的值
List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}
使用 SqlUpdate
Using SqlUpdate
SqlUpdate 类封装了一个 SQL 更新。和查询一样,更新对象是可重复使用的,而且和所有的 RdbmsOperation 类一样,更新可以有参数,并以 SQL 方式定义。这个类提供了一些类似于查询对象的 execute(.)
方法的 update(.)
方法。SqlUpdate 类是具体的。它可以被子类化—例如,添加一个自定义的更新方法。然而,你不必对 SqlUpdate 类进行子类化,因为它可以通过设置 SQL 和声明参数轻松实现参数化。下面的例子创建了一个名为 execute 的自定义更新方法:
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) {
// update 有很多重载方法,比如还可以使用 updateByNamedParam 来提供一个参数 map ,传递任意个参数
return update(rating, id);
}
}
使用存储过程
Using StoredProcedure
存储过程类是一个抽象的超类,用于 RDBMS 存储过程的对象抽象。
继承的 sql 属性是 RDBMS 中存储过程的名称。
为了给存储过程类定义一个参数,你可以使用一个 SqlParameter 或者它的一个子类。你必须在构造函数中指定参数名称和 SQL 类型,如下面的代码片段所示:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SQL 类型是使用 java.sql.Types 常量指定的。
第一行(带 SqlParameter)声明了一个 IN 参数。你可以在存储过程调用和使用 SqlQuery 及其子类的查询中使用IN参数(在 使用 SqlQuery 中涉及)。
第二行(SqlOutParameter)声明了一个用于存储过程调用的输出参数。还有一个 SqlInOutParameter 用于 InOut 参数(为存储过程提供一个 in 值的参数,同时也返回一个值)。
对于 in 参数,除了名称和 SQL 类型外,你可以为数字数据指定一个刻度,或者为自定义数据库类型指定一个类型名称。对于输出参数,你可以提供一个 RowMapper 来处理从 REF 游标返回的行的映射。另一个选择是指定一个 SqlReturnType,让你定义对返回值的自定义处理。
下一个简单 DAO 的例子使用一个存储过程来调用一个函数(sysdate()
),这个函数是任何 Oracle 数据库都有的。为了使用存储过程的功能,你必须创建一个扩展存储过程的类。在这个例子中,StoredProcedure 类是一个内部类。然而,如果你需要重复使用存储过程,你可以把它声明为一个顶层类。这个例子没有输入参数,但是通过使用 SqlOutParameter 类,将一个输出参数声明为日期类型。execute()
方法运行该过程,并从结果图中提取返回的日期。结果图通过使用参数名称作为关键字,为每个已声明的输出参数(在本例中,只有一个)设置了一个条目。下面的列表显示了我们的自定义存储过程类:
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() {
// sysdate 没有输入参数,所以提供了一个空的 Map...
Map<String, Object> results = execute(new HashMap<String, Object>());
Date sysdate = (Date) results.get("date");
return sysdate;
}
}
}
下面这个存储过程的例子有两个输出参数(在这种情况下是 Oracle REF 游标):
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() {
//同样,这个没有输入参数,所以提供了一个空的 Map
return super.execute(new HashMap<String, Object>());
}
}
注意在 TitlesAndGenresStoredProcedure 构造函数中使用的 declarationParameter(..)
方法的重载变体是如何传递 RowMapper 实现实例的。这是一个非常方便和强大的方式来重用现有的功能。接下来的两个例子提供了两个 RowMapper 实现的代码。
TitleMapper 类为所提供的 ResultSet 中的每一行映射到一个 Title 域对象,如下所示:
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;
}
}
GenreMapper 类为提供的 ResultSet 中的每一条记录将一个 ResultSet 映射到一个 Genre 域对象,如下所示:
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"));
}
}
为了向一个在 RDBMS 中定义了一个或多个输入参数的存储过程传递参数,你可以编码一个强类型的 execute(..)
方法,该方法将委托给超类中的非类型的 execute(Map)
方法,如下图所示:
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);
}
}