# 3.7将JDBC操作建模为Java对象

org.springframework.jdbc.object包包含一些类，这些类使您可以更面向对象地访问数据库。 例如，您可以执行查询并以包含业务对象的列表的形式返回结果，该业务对象的关系列数据映射到业务对象的属性。 您还可以运行存储过程并运行update，delete和insert语句。

> 许多Spring开发人员认为，下面描述的各种RDBMS操作类（StoredProcedure类除外）通常可以用直接的JdbcTemplate调用代替。 通常，编写直接在JdbcTemplate上调用方法的DAO方法（与将查询封装为完整的类相反）更容易。
>
> 但是，如果您通过使用RDBMS操作类获得可测量的价值，则应继续使用这些类。

## 3.7.1 了解SqlQuery

SqlQuery是可重用的，线程安全的类，它封装了SQL查询。 子类必须实现newRowMapper（..）方法以提供RowMapper实例，该实例可以为通过在查询执行期间创建的ResultSet进行迭代而获得的每一行创建一个对象。 很少直接使用SqlQuery类，因为MappingSqlQuery子类为将行映射到Java类提供了更为方便的实现。 扩展SqlQuery的其他实现是MappingSqlQueryWithParameters和UpdatableSqlQuery。

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

}
```

该类扩展了使用Actor类型参数化的MappingSqlQuery。 此客户查询的构造函数将DataSource作为唯一参数。 在此构造函数中，可以使用数据源和应执行以检索此查询的行的SQL调用超类上的构造函数。 此SQL用于创建PreparedStatement，因此它可能包含在执行期间要传递的任何参数的占位符。 您必须使用传入SqlParameter的defineParameter方法声明每个参数。 SqlParameter具有名称，并且具有java.sql.Types中定义的JDBC类型。 定义所有参数后，可以调用compile（）方法，以便可以准备语句并稍后运行。 此类在编译后是线程安全的，因此，只要在初始化DAO时创建这些实例，就可以将它们保留为实例变量并可以重用。 下面的示例演示如何定义此类：

```java
private ActorMappingQuery actorMappingQuery;

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

public Customer getCustomer(Long id) {
    return actorMappingQuery.findObject(id);
}
```

前面示例中的方法检索具有传入的id作为唯一参数的customer。 由于只希望返回一个对象，因此我们将id作为参数调用findObject便捷方法。 相反，如果有一个查询返回一个对象列表并采用其他参数，则将使用其中一种执行方法，该方法采用以varargs形式传入的参数值数组。 以下示例显示了这种方法：

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

## 3.7.3 使用SqlUpdate

SqlUpdate类封装了SQL更新。 与查询一样，更新对象是可重用的，并且与所有RdbmsOperation类一样，更新可以具有参数并在SQL中定义。 此类提供了许多类似于查询对象的execute（..）方法的update（..）方法。 SQLUpdate类是具体的。 可以将其子类化-例如，添加自定义更新方法。 但是，您不必子类化SqlUpdate类，因为可以通过设置SQL和声明参数来轻松地对其进行参数化。 以下示例创建一个名为execute的自定义更新方法：

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

## 3.7.4 使用StoredProcedure

StoredProcedure类是RDBMS存储过程的对象抽象的超类。此类是抽象的，并且其各种execute（..）方法具有受保护的访问权限，还可以防止使用除了通过提供更严格类型的子类之外的其他形式的访问。

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

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

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

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

第一行（带有SqlParameter）声明一个IN参数。您可以将IN参数用于存储过程调用和使用SqlQuery及其子类的查询（了解SqlQuery中介绍）。

第二行（带有SqlOutParameter）声明将在存储过程调用中使用的out参数。还有一个用于InOut参数的SqlInOutParameter（为过程提供in值并返回值的参数）。

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

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

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

}
```

下面的StoredProcedure示例包含两个输出参数（在本例中为Oracle REF游标）：

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

请注意如何在TitlesAndGenresStoredProcedure构造函数中使用的clarifyParameter（..）方法的重载变体如何传递给RowMapper实现实例。 这是重用现有功能的非常方便而强大的方法。 接下来的两个示例提供了两个RowMapper实现的代码。

TitleMapper类将提供的ResultSet中每一行的ResultSet映射到Title域对象，如下所示：

```java
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域对象，如下所示：

```java
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）方法，例如 以下示例显示：

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