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 类的一个实例:

  1. public class ActorMappingQuery extends MappingSqlQuery<Actor> {
  2. public ActorMappingQuery(DataSource ds) {
  3. // 这里需要一个 id 参数
  4. super(ds, "select id, first_name, last_name from t_actor where id = ?");
  5. // 定义了 ID 参数的类型
  6. declareParameter(new SqlParameter("id", Types.INTEGER));
  7. compile();
  8. }
  9. // 将结果自定义处理成 bean
  10. @Override
  11. protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
  12. Actor actor = new Actor();
  13. actor.setId(rs.getLong("id"));
  14. actor.setFirstName(rs.getString("first_name"));
  15. actor.setLastName(rs.getString("last_name"));
  16. return actor;
  17. }
  18. }

该类扩展了 MappingSqlQuery,其参数为 Actor 类型。这个客户查询的构造函数需要一个 DataSource 作为唯一的参数。在这个构造函数中,你可以用数据源和应该运行的 SQL 来调用超类的构造函数,以检索这个查询的行。这个 SQL 是用来创建 PreparedStatement 的,所以它可以包含执行过程中要传入的任何参数的占位符。你必须通过使用 declareParameter 方法来声明每个参数,并传入一个 SqlParameter。SqlParameter 需要一个名字,以及java.sql.Types 中定义的 JDBC 类型。在你定义了所有的参数之后,你可以调用 compile()方法,这样语句就可以被准备好,随后就可以运行。这个类在编译后是 线程安全 的,所以,只要这些实例是在 DAO 初始化时创建的,它们就可以作为实例变量保留并被重复使用。下面的例子展示了如何定义这样一个类:

  1. private ActorMappingQuery actorMappingQuery;
  2. @Autowired
  3. public void setDataSource(DataSource dataSource) {
  4. this.actorMappingQuery = new ActorMappingQuery(dataSource);
  5. }
  6. public Customer getCustomer(Long id) {
  7. // 这里需要调用方法 传入 sql 中需要的参数,触发查询,并获得信息
  8. // 这里的 Customer 可能是 Actor 的父类
  9. return actorMappingQuery.findObject(id);
  10. }

前面的例子中的方法是用作为唯一参数传入的 id 来检索客户。因为我们只想返回一个对象,所以我们调用以 id 为参数的 findObject方便方法。如果我们有一个返回对象列表的查询,并且需要额外的参数,我们将使用其中一个执行方法,该方法需要一个作为 varargs 传入的参数值数组。下面的例子显示了这样一个方法:

  1. public List<Actor> searchForActors(int age, String namePattern) {
  2. // execute 方法接收 一个可变参数作为参数值,以这种方式就可以按照 SQL 里面占位符 ? 出现的顺序传入对应的值
  3. List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
  4. return actors;
  5. }

使用 SqlUpdate

Using SqlUpdate

SqlUpdate 类封装了一个 SQL 更新。和查询一样,更新对象是可重复使用的,而且和所有的 RdbmsOperation 类一样,更新可以有参数,并以 SQL 方式定义。这个类提供了一些类似于查询对象的 execute(.)方法的 update(.)方法。SqlUpdate 类是具体的。它可以被子类化—例如,添加一个自定义的更新方法。然而,你不必对 SqlUpdate 类进行子类化,因为它可以通过设置 SQL 和声明参数轻松实现参数化。下面的例子创建了一个名为 execute 的自定义更新方法:

  1. import java.sql.Types;
  2. import javax.sql.DataSource;
  3. import org.springframework.jdbc.core.SqlParameter;
  4. import org.springframework.jdbc.object.SqlUpdate;
  5. public class UpdateCreditRating extends SqlUpdate {
  6. public UpdateCreditRating(DataSource ds) {
  7. setDataSource(ds);
  8. setSql("update customer set credit_rating = ? where id = ?");
  9. declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
  10. declareParameter(new SqlParameter("id", Types.NUMERIC));
  11. compile();
  12. }
  13. /**
  14. * @param id for the Customer to be updated
  15. * @param rating the new value for credit rating
  16. * @return number of rows updated
  17. */
  18. public int execute(int id, int rating) {
  19. // update 有很多重载方法,比如还可以使用 updateByNamedParam 来提供一个参数 map ,传递任意个参数
  20. return update(rating, id);
  21. }
  22. }

使用存储过程

Using StoredProcedure

存储过程类是一个抽象的超类,用于 RDBMS 存储过程的对象抽象。

继承的 sql 属性是 RDBMS 中存储过程的名称。

为了给存储过程类定义一个参数,你可以使用一个 SqlParameter 或者它的一个子类。你必须在构造函数中指定参数名称和 SQL 类型,如下面的代码片段所示:

  1. new SqlParameter("in_id", Types.NUMERIC),
  2. 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()方法运行该过程,并从结果图中提取返回的日期。结果图通过使用参数名称作为关键字,为每个已声明的输出参数(在本例中,只有一个)设置了一个条目。下面的列表显示了我们的自定义存储过程类:

  1. import java.sql.Types;
  2. import java.util.Date;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. import javax.sql.DataSource;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.jdbc.core.SqlOutParameter;
  8. import org.springframework.jdbc.object.StoredProcedure;
  9. public class StoredProcedureDao {
  10. private GetSysdateProcedure getSysdate;
  11. @Autowired
  12. public void init(DataSource dataSource) {
  13. this.getSysdate = new GetSysdateProcedure(dataSource);
  14. }
  15. public Date getSysdate() {
  16. return getSysdate.execute();
  17. }
  18. private class GetSysdateProcedure extends StoredProcedure {
  19. private static final String SQL = "sysdate";
  20. public GetSysdateProcedure(DataSource dataSource) {
  21. setDataSource(dataSource);
  22. setFunction(true);
  23. setSql(SQL);
  24. declareParameter(new SqlOutParameter("date", Types.DATE));
  25. compile();
  26. }
  27. public Date execute() {
  28. // sysdate 没有输入参数,所以提供了一个空的 Map...
  29. Map<String, Object> results = execute(new HashMap<String, Object>());
  30. Date sysdate = (Date) results.get("date");
  31. return sysdate;
  32. }
  33. }
  34. }

下面这个存储过程的例子有两个输出参数(在这种情况下是 Oracle REF 游标):

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import javax.sql.DataSource;
  4. import oracle.jdbc.OracleTypes;
  5. import org.springframework.jdbc.core.SqlOutParameter;
  6. import org.springframework.jdbc.object.StoredProcedure;
  7. public class TitlesAndGenresStoredProcedure extends StoredProcedure {
  8. private static final String SPROC_NAME = "AllTitlesAndGenres";
  9. public TitlesAndGenresStoredProcedure(DataSource dataSource) {
  10. super(dataSource, SPROC_NAME);
  11. declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
  12. declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
  13. compile();
  14. }
  15. public Map<String, Object> execute() {
  16. //同样,这个没有输入参数,所以提供了一个空的 Map
  17. return super.execute(new HashMap<String, Object>());
  18. }
  19. }

注意在 TitlesAndGenresStoredProcedure 构造函数中使用的 declarationParameter(..) 方法的重载变体是如何传递 RowMapper 实现实例的。这是一个非常方便和强大的方式来重用现有的功能。接下来的两个例子提供了两个 RowMapper 实现的代码。

TitleMapper 类为所提供的 ResultSet 中的每一行映射到一个 Title 域对象,如下所示:

  1. import java.sql.ResultSet;
  2. import java.sql.SQLException;
  3. import com.foo.domain.Title;
  4. import org.springframework.jdbc.core.RowMapper;
  5. public final class TitleMapper implements RowMapper<Title> {
  6. public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
  7. Title title = new Title();
  8. title.setId(rs.getLong("id"));
  9. title.setName(rs.getString("name"));
  10. return title;
  11. }
  12. }

GenreMapper 类为提供的 ResultSet 中的每一条记录将一个 ResultSet 映射到一个 Genre 域对象,如下所示:

  1. import java.sql.ResultSet;
  2. import java.sql.SQLException;
  3. import com.foo.domain.Genre;
  4. import org.springframework.jdbc.core.RowMapper;
  5. public final class GenreMapper implements RowMapper<Genre> {
  6. public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
  7. return new Genre(rs.getString("name"));
  8. }
  9. }

为了向一个在 RDBMS 中定义了一个或多个输入参数的存储过程传递参数,你可以编码一个强类型的 execute(..)方法,该方法将委托给超类中的非类型的 execute(Map)方法,如下图所示:

  1. import java.sql.Types;
  2. import java.util.Date;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. import javax.sql.DataSource;
  6. import oracle.jdbc.OracleTypes;
  7. import org.springframework.jdbc.core.SqlOutParameter;
  8. import org.springframework.jdbc.core.SqlParameter;
  9. import org.springframework.jdbc.object.StoredProcedure;
  10. public class TitlesAfterDateStoredProcedure extends StoredProcedure {
  11. private static final String SPROC_NAME = "TitlesAfterDate";
  12. private static final String CUTOFF_DATE_PARAM = "cutoffDate";
  13. public TitlesAfterDateStoredProcedure(DataSource dataSource) {
  14. super(dataSource, SPROC_NAME);
  15. declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
  16. declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
  17. compile();
  18. }
  19. public Map<String, Object> execute(Date cutoffDate) {
  20. Map<String, Object> inputs = new HashMap<String, Object>();
  21. inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
  22. return super.execute(inputs);
  23. }
  24. }