15.6 像Java对象那样操作JDBC

org.springframework.jdbc.object包能让你更加面向对象化的访问数据库。举个例子,用户可以执行查询并返回一个list, 该list作为一个结果集将把从数据库中取出的列数据映射到业务对象的属性上。你也可以执行存储过程,包括更新、删除、插入语句。

备注:许多Spring的开发者认为下面将描述的各种RDBMS操作类(StoredProcedure类除外)可以直接被JdbcTemplate代替; 相对于把一个查询操作封装成一个类而言,直接调用JdbcTemplate方法将更简单而且更容易理解。但这仅仅是一种观点而已, 如果你认为可以从直接使用RDBMS操作类中获取一些额外的好处,你不妨根据自己的需要和喜好进行不同的选择。

15.6.1 SqlQuery

SqlQuery类主要封装了SQL查询,本身可重用并且是线程安全的。子类必须实现newRowMapper方法,这个方法提供了一个RowMapper实例,用于在查询执行返回时创建的结果集迭代过程中每一行映射并创建一个对象。SqlQuery类一般不会直接使用;因为MappingSqlQuery子类已经提供了一个更方便从列映射到Java类的实现。其他继承SqlQuery的子类有MappingSqlQueryWithParameters和UpdatableSqlQuery。

15.6.2 MappingSqlQuery

MappingSqlQuery是一个可重用的查询类,它的子类必须实现mapRow(..)方法,将结果集返回的每一行转换成指定的对象类型。下面的例子展示了一个自定义的查询例子,将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 = ?");
        super.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;
    }

}

这个类继承了MappingSqlQuery,并且传入Actor类型的泛型参数。这个自定义查询类的构造函数将DataSource作为唯一的传入参数。这个构造器中你调用父类的构造器,传入DataSource以及相应的SQL参数。该SQL用于创建PreparedStatement,因此它可能包含任何在执行过程中传入参数的占位符。你必须在SqlParameter中使用declareParameter方法定义每个参数。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) {
    return actorMappingQuery.findObject(id);
}

这个例子中的方法通过唯一的传入参数id获取customer实例。因为我们只需要返回一个对象,所以就简单的调用findObject类就可以了,这个方法只需要传入id参数。如果我们需要一次查询返回一个列表的话,就需要使用传入可变参数数组的执行方法。

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

15.6.3 SqlUpdate

SqlUpdate封装了SQL的更新操作。和查询一样,更新对象是可以被重用的,就像所有的rdbms操作类,更新操作能够传入参数并且在SQL定义。类似于SqlQuery诸多execute(..)方法,这个类提供了一系列update(..)方法。SQLUpdate类不是抽象类,它可以被继承,比如,实现自定义的更新方法。但是你并不需要继承SqlUpdate类来达到这个目的,你可以更简单的在SQL中设置自定义参数来实现。

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

15.6.4 StoredProcedure

StoredProcedure类是所有RDBMS存储过程的抽象类。该类提供了多种execute(..)方法,其访问类型都是protected的。

为了定义一个存储过程类,你需要使用SqlParameter或者它的一个子类。你必须像下面的代码例子那样在构造函数中指定参数名和SQL类型。SQL类型使用java.sql.Types 常量定义。

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

SqlParameter的第一行定义了一个输入参数。输入参数可以同时被存储过程调用和使用SqlQuery的查询语句使用,他的子类会在下面的章节提到。

第二行SqlOutParameter 参数定义了一个在存储过程调用中使用的输出参数。SqlInOutParameter 还有一个InOut参数,该参数提供了一个输入值,同时也有返回值。

对应输入参数,除了名字和SQL类型,你还能指定返回区间数值类型和自定义数据库类型。对于输出参数你可以使用RowMapper来处理REF游标返回的行映射关系。另一个选择是指定SqlReturnType,能够让你定义自定义的返回值类型。

下面的程序演示了如何调用Oracle中的sysdate()函数。为了使用存储过程函数你需要创建一个StoredProcedure的子类。在这个例子中,StoredProcedure是一个内部类,但是如果你需要重用StoredProcedure你需要定义成一个顶级类。这个例子没有输入参数,但是使用SqlOutParameter类定义了一个时间类型的输出参数。execute()方法执行了存储过程,并且从结果集Map中获取返回的时间数据。结果集Map中包含每个输出参数对应的项,在这个例子中就只有一项,使用了参数名作为key.

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

}

下面是一个包含两个输出参数的存储过程例子。

import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

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构造函数中 declareParameter(..)的SqlOutParameter参数, 该参数使用RowMapper接口的实现。这是一种非常方便有效的重用方式。两种RowMapper实现的代码如下:

TitleMapper类将返回结果集的每一行映射成Title类

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import com.foo.domain.Title;

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类将返回结果集的每一行映射成Genre类

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import com.foo.domain.Genre;

public final class GenreMapper implements RowMapper<Genre> {

    public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Genre(rs.getString("name"));
    }
}

为了将参数传递给RDBMS中定义的一个或多个输入参数给存储过程,你可以定义一个强类型的execute(..)方法,该方法将调用基类的protected execute(Map parameters)方法。例如:

import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

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