跳到主要内容

将 JDBC 操作建模为 Java 对象

ChatGPT-4o 中英对照 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 的实现包括 MappingSqlQueryWithParametersUpdatableSqlQuery

使用 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;
}
}
java

该类扩展了 MappingSqlQuery,并使用 Actor 类型进行参数化。这个自定义查询的构造函数只接受一个 DataSource 作为参数。在这个构造函数中,你可以调用父类的构造函数,传入 DataSource 和用于检索此查询行的 SQL。这个 SQL 用于创建一个 PreparedStatement,因此它可以包含在执行期间传入的任何参数的占位符。你必须使用 declareParameter 方法声明每个参数,并传入一个 SqlParameterSqlParameter 需要一个名称和在 java.sql.Types 中定义的 JDBC 类型。在定义所有参数之后,你可以调用 compile() 方法,以便可以准备语句并在稍后运行。这个类在编译后是线程安全的,因此,只要在 DAO 初始化时创建这些实例,它们就可以作为实例变量保存并被重用。以下示例展示了如何定义这样的类:

private ActorMappingQuery actorMappingQuery;

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

public Actor getActor(Long id) {
return actorMappingQuery.findObject(id);
}
java

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

public List<Actor> searchForActors(int age, String namePattern) {
return actorSearchMappingQuery.execute(age, namePattern);
}
java

使用 SqlUpdate

SqlUpdate 类封装了一个 SQL 更新操作。与查询类似,更新对象是可重用的,并且与所有 RdbmsOperation 类一样,更新可以有参数并在 SQL 中定义。这个类提供了许多 update(..) 方法,类似于查询对象的 execute(..) 方法。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) {
return update(rating, id);
}
}
java

使用 StoredProcedure

StoredProcedure 类是 RDBMS 存储过程对象抽象的 abstract 超类。

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

要为 StoredProcedure 类定义一个参数,可以使用 SqlParameter 或其子类之一。必须在构造函数中指定参数名称和 SQL 类型,如以下代码片段所示:

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
java

SQL 类型使用 java.sql.Types 常量指定。

第一行(包含 SqlParameter)声明了一个 IN 参数。你可以在存储过程调用和使用 SqlQuery 及其子类(在理解 SqlQuery中介绍)进行查询时使用 IN 参数。

第二行(带有 SqlOutParameter)声明了一个用于存储过程调用的 out 参数。对于 InOut 参数(即向过程提供 in 值并返回一个值的参数),还有一个 SqlInOutParameter

对于 in 参数,除了名称和 SQL 类型,你还可以为数值数据指定一个小数位数,或者为自定义数据库类型指定一个类型名称。对于 out 参数,你可以提供一个 RowMapper 来处理从 REF 游标返回的行的映射。另一种选择是指定一个 SqlReturnType,允许你自定义处理返回值。

下一个简单 DAO 的示例使用 StoredProcedure 来调用一个函数(sysdate()),该函数在任何 Oracle 数据库中都存在。要使用存储过程功能,你需要创建一个扩展 StoredProcedure 的类。在这个示例中,StoredProcedure 类是一个内部类。然而,如果你需要重用 StoredProcedure,你可以将其声明为顶级类。这个示例没有输入参数,但通过使用 SqlOutParameter 类将输出参数声明为日期类型。execute() 方法运行过程并从结果 Map 中提取返回的日期。结果 Map 使用参数名称作为键,为每个声明的输出参数(在这种情况下,只有一个)创建一个条目。以下列表显示了我们自定义的 StoredProcedure 类:

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;
}
}

}
java

下面的 StoredProcedure 示例有两个输出参数(在这种情况下,是 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() {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(new HashMap<String, Object>());
}
}
java

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

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

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

GenreMapper 类将 ResultSet 映射为 Genre 域对象,针对提供的 ResultSet 中的每一行,映射过程如下:

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

要将参数传递给在 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);
}
}
java