Spring 框架中DAO JDBC 持久层使用

2019-08-01 0 By admin

Spring JDBC是Spring所提供的持久层技术。它的主要目的是降低使用 JDBC API的门槛,以一种更直接、更简洁的方式使用 JDBC API。
在 Spring JDBC里,仅需做那些与业务相关的DML操作的事,而将资源获取、Statement创建、资源释放及异常处理等繁杂而乏味的工作交给Spring JDBC。

一、使用Spring JDBC

1.1、简单的例子

    private void func() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(); // (1)
        jdbcTemplate.setDataSource(dataSource);
        String sql = "INSERT INTO tb_name (name, value) VALUES ('Lucas', '26')";
        jdbcTemplate.execute(sql);
    }

上述示例中,特意将数据源创建和模板实例创建的代码都列在这个例子中。
但在实际应用中,用户一般不会在DAO中做这些事情。由于JdbcTemplate是线程安全的,因而所有的DAO都可以共享同一个 JdbcTemplate实例,这样(1)的代码就可以从DAO中移除了,转而在 Spring配置文件中统一定义即可。

1.2、在DAO中使用JdbcTemplate

@Configuration
public class DatabaseConfig {
    @Bean
    public JdbcTemplate jdbcTemplate(@Autowired DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}

@Repository
public class MyTestDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void func() {
        jdbcTemplate.execute("INSERT INTO tb_name (name, value) VALUES ('Lucas', '26')");
    }
}

1.3、JdbcTemplate的属性

JdbcTemplate拥有几个可用于控制底层JDBC API的属性
queryTimeout
设置 JdbcTemplate所创建的Statement查询数据时的最大超时时间。默认为0,表示使用底层JDBC驱动程序的默认设置。
fetchSize
设置底层的ResultSet每次从数据库返回的行数。该属性对程序的性能影响很大,如果设置过大,因为一次性载入的数据都放到内存中,所以内存的消耗很大:反之,如果设置过小,从数据库读取的次数将增大,也会影响性能。默认值为0,表示使用底层JDBC驱动程序的默认设置。
maxRows
设置底层的Resultset从数据库返回的最大行数。默认值为0,表示使用底层JDBC驱动程序的默认设置。
ignoreWarnings
是否忽略SQL的警告信息。默认为true,即所有的警告信息都被记录到日志中,如果设置为false,则JdbcTemplate将抛出SQLWarningException。

二、数据操作

2.1、更改数据update

JdbcTemplate在内部是通过PreparedStatement执行SQL语句的,可以使用绑定了参数的SQL语句。

    private void func() {
        String value = "ZZX";
        // (1) 直接执行SQL
        jdbcTemplate.update("UPDATE tb_name SET name = 'ZZX' WHERE id = '1'");
        // (2) 绑定参数的SQL
        jdbcTemplate.update("UPDATE tb_name SET name = ? WHERE id = ?", new Object[]{value, 2});
        // (3) 指定参数的参数类型
        jdbcTemplate.update("UPDATE tb_name SET name = ? WHERE id = ?", new Object[]{value, 3},\
        new int[]{Types.VARCHAR, Types.INTEGER});
    }

2.2、带回调的update

需要指出的是,在实际使用中,应优先考虑使用不带回调接口的 JdbcTemplate方法。
首先,回调使代码显得臃肿;其次,回调并不能带来额外的好处。当使用简洁版的方法时,JdbcTemplate会在内部自动创建这些回调实例。以下是带回调带update方法:

1、int update(String sql, PreparedStatementSetter pss):
PreparedStatementSetter是一个回调接口,它定义了一个void set values( PreparedStatement ps)接口方法。 JabcTemplate使用SQL语句创建出 PreparedStatement实例后,将调用该回调接口执行绑定参数的操作。PreparedStatement绑定参数时,参数索引从1开始而非从0开始。
2、int update(PreparedStatementCreator psc):
PreparedStatementCreator也是一个回调接口,它负责创建一个PreparedStatement实例。该回调接口定义了一个PreparedStatement create PreparedStatement( Connection con)方法。
3、protected int update(PreparedStatementCreator psc, PreparedStatementSetter pss):
联合使用PreparedStatementCreatorPreparedStatementSetter回调。

2.3、获取自增主键

在JDBC3.0规范中,当新增记录时,允许将数据库自动产生的主键值绑定到Statement或PreparedStatement中。
1、在使用 Statement时,可以通过以下方法绑定主键值:int executeUpdate(String sql, int autoGeneratedKeys);
2、也可以通过Connection创建绑定自增主键值的PreparedStatement,如下:Preparedstatement prepareStatement(String sql, int autoGeneratedKeys)

Spring利用这一技术,提供了一个可以返回新增记录对应主键值的方法:int update(Preparedstatementcreator psc, KeyHolder generatedKey Holder)

private void func() {
        String sql = "INSERT INTO tb_name (name) VALUES (?)";
        // 创建一个主键持有对象
        KeyHolder keyHolder = new GeneratedKeyHolder();

        jdbcTemplate.update((Connection con) ->  {
            PreparedStatement statement = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            statement.setString(1, "ZZX");
            return statement;
            }, keyHolder);
        // 获取主键
        System.out.println(keyHolder.getKey().intValue());
    }

2.4、批量修改数据

如果需要一次性插入或更新多条记录,最好的选择是使用JdbcTemplate批量数据更改的方
法。JdbcTemplate的两个批量数据操作方法:

1、int[] batchUpdate(String[] sql):
多条SQL语句组成一个数组(这些SQL语句不带参数),该方法以批量方式执行这些SQL语句。 Spring在内部使用JDBC提供的批量更新API完成操作。
2、int[] batchUpdate(String sql, BatchPreparedStatementSetter pss):
使用该方法对于同一结构的带参SQL语句多次进行数据更新操作。BatchPreparedStatementSetter是一次性批量提交数据的,getSize()是整批的大小。

private void func() {
        String sql = "INSERT INTO tb_name (name) VALUES (?)";
        String[] values = {"A", "B", "C"};

        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, values[i]);
            }

            @Override
            public int getBatchSize() {
                return values.length;
            }
        });
    }

2.5、查询数据——使用RowCallbackHandler

当结果集中没有数据时,此时并不会抛出异常,而是此时RowCallbackHandler回调接口中定义的处理逻辑没有得到调用。

    private void func() {
        String sql = "SELECT * FROM tb_name WHERE id = 1";

        User user = new User();
        jdbcTemplate.query(sql, (ResultSet rs) -> {
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setValue(rs.getLong("value"));
        });
        System.out.println(user);
    }

2.6、批量查询数据——使用RowCallbackHandler

private void func() {
        String sql = "SELECT * FROM tb_name";

        List<User> users = new ArrayList<>();
        jdbcTemplate.query(sql, (ResultSet rs) -> {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setValue(rs.getLong("value"));
            users.add(user);
        });
        System.out.println(users);
    }

2.7、查询数据——使用RowMapper T

private void func() {
        String sql = "SELECT * FROM tb_name";

        List<User> users = jdbcTemplate.query(sql, (ResultSet rs, int rowNum) -> {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setValue(rs.getLong("value"));
            return user;
        });

        System.out.println(users);
    }

从功能上来说,RowCallbackHandler和RowMapper<T>并没有太大的区别,它们都用于定义结果集行的读取逻辑,将 Resultset中的数据映射到对象或List中。

2.8、处理数据的方式:RowMapper 和 RowCallbackHandler

通过JDBC查询返回一个Resultset结果集时,JDBC并不会一次性将所有匹配的数据都加载到JVM中,而是只返回一批次的数据(由JDBC驱动程序决定),当通过ResultSet#next()游标滚动结果集超过数据范围时,JDBC再获取一批数据。
这样以一种“批量化+串行化”的处理方式避免大结果集处理时JVM内存的过大开销。

当处理大结果集时,如果使用RowMapper,那么采用的方式是将结果集中的所有数据都放到一个Lis<T>对象中,这样将会占用大量的JVM内存,甚至可能直接引发OutOfMemoryException异常。
这时,可使用RowCallbackHandler接口,在processRow()接口方法内部一边获取数据一边完成处理,这样数据就不会在内存中堆积,可大大减少对JVM内存的占用。
采用RowMapper的操作方式是先获取数据,然后再处理数据;而 RowCallbackHandler的操作方式是一边获取数据一边处理,处理完就丢弃之。因此,可以将 Row Mapper看作采用批量化数据处理策略,而 RowHandler则采用流化处理策略。

2.9、查询单值数据

如果查询的结果集仅有一个值,如SELECT COUNT(*) FROM tb_name,这时可以使用更简单的方式获取结果集的值。 JdbcTemplate为获取结果集中的单值数据提供了3组方法,分别用于获取int、long的单值,其他类型的单值则以 Object类型返回。

2.10、调用存储过程

JdbcTemplate提供了两个调用存储过程的接口方法,分别介绍如下。
1、<T> T execute(String callString, CallableStatementCallback<T> action):
用户通过callString参数指定调用存储过程的SQL语句;第二个参数CallableStatementCallback<T>是一个回调接口,可以在接口的方法中进行输入参数绑定、输出参数注册及返回数据处理等操作。

2、<T> T execute(CallableStatementCreator csc, CallableStatementCallback<T> action):
该接口方法使用 CallableStatementCreator创建CallableStatement。CallableStatementCreator负责创建 CallableStatement实例、绑定参数、注册输出参数等工作,CallableStatementCallback<T>负责处理存储过程的返回结果。

Spring提供了创建CallableStatementcreator的工厂类 CallableStatementCreatorFactory,通过该工厂类可以简化CallableStatementCreator的实例创建工作。