Spring RE 09 事务小结与Spring中使用JDBCTemplate

Spring RE 09 事务小结与Spring中使用JDBCTemplate

上个星期博主到杭州务工了五天, 杭州不愧996圣地, 我去了之后天天三四点钟结束, 恍惚之间有种与世隔绝的感觉. 每天生物钟混乱导致食欲不振, 嗓子痛, 口腔溃疡. 看来这辈子领导是当不上了, 领导一个个都是精力充沛, 可以从中午谈判到晚上7点不吃饭, 出去喝完酒回来再谈到三点钟, 第二天早上9点精

上个星期博主到杭州务工了五天, 杭州不愧996圣地, 我去了之后天天三四点钟结束, 恍惚之间有种与世隔绝的感觉. 每天生物钟混乱导致食欲不振, 嗓子痛, 口腔溃疡.

看来这辈子领导是当不上了, 领导一个个都是精力充沛, 可以从中午谈判到晚上7点不吃饭, 出去喝完酒回来再谈到三点钟, 第二天早上9点精神抖擞的出现在公司. 我这体质确实比不上啊. 周日回来之后瘫了两天, 晚上10点睡觉, 早上还是爬不起来...

财务这行呢, 其实不需要任何技术, 就是反复的算啊算啊, 毫无意义. 所以代码还得继续好好学, 中断了一个星期, 现在还得继续研究Spring.

之前了解了事务XML配置及注解背后的类, 现在来总结一下使用事务的一些要点, 然后开始看具体的数据库操作了. 说到这里, 还必须去好好学学Hibernate才行.

  1. 事务注意事项
  2. 创建JDBCTemplate对象
  3. 基本操作 - 改
  4. 基本操作 - 增
  5. 基本操作 - 查
  6. 其他知识点

事务注意事项

这些注意事项如果都用代码, 就会比较繁杂, 我就直接把结论列出来:

  1. 使用Hibernate访问数据库一定要配置HibernateTransactionManager事务管理器, 否则默认的事务管理策略即合并事务和readOnly会造成无法修改数据.
  2. 事务传播机制, 设置为PROPAGATION_REQUIRED的时候, 嵌套调用被@Transactional注解标注的方法, 只要是在一个线程中, 都会加入到同一个事务中.
  3. 接着上一条, 如果是两个线程, 则会创建两个分离的事务.
  4. 如果同时使用高层的纯ORM比如Hibernate外加底层的JDBC或者MyBatis, 会绑定到同一个更高层的ORM的数据库连接包装对象中, 然而由于高层ORM带有缓存机制, 控制数据的写入就变得的比较麻烦. 所以要么不用, 如果用了, 就使用ORM修改数据, 使用底层技术来查找数据比较好.
  5. 使用注解即意味着使用AOP技术进行事务增强, 要注意实现类的事务方法必须是public的. 对于final, static, private的方法, 由于不能被子类继承, 也就无法被代理. 要注意不要把事务写到这其中. 由public 调用的 private则没有问题, 因此实际上出问题的是public static和public final这两种方法.
  6. 如果不直接操作底层, 建议使用模板来获取连接, 模板内部通过DataSourceUtils获取数据连接. 如果直接操作底层, 在事务方法内部也需要使用DataSourceUtils获取事务管理上下文的数据库连接, 以避免泄露问题.

学完了这里, 就知道了一个小小的@Transactional注解背后, 隐藏了这么多默默提供服务的类以及AOP这一强大工具, 还实现了事务传播. Spring 确实做得棒啊.

这里再简单回想一下, 需要先定义DataSource, 以及对应DataSource的事务管理器, 然后使用注解的时候, Spring就会通过TransactionDefiniation装载注解中的元信息, 使用TransactionStatus和PlatformTransactionManager类来管理事务.

后边就来具体操作吧.

创建JDBCTemplate对象

从之前的学习中可以知道, 使用Spring 的JDBC支持, 其实就是学习如何使用Spring提供的JDBCTemplate了.

想使用JDBCTemplate依然可以通过XML配置一个DataSource, 然后创建一个JDBCTemplate的Bean. 既然能够通过XML配置, 当然也可以通过代码直接使用了, 一个最简单的例子如下:

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import java.time.LocalDateTime;

public class Demo1 {

    public static void main(String[] args) {
        //这个DriverManagerDataSource类是Spring提供的实现类, 换成其他实现, 比如DBCP或者C3PO的DataSource一样可以.
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        //这些都是老套路了
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/sia5");
        dataSource.setUsername("root");
        dataSource.setPassword("fflym0709");

        //构造器注入
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        //SQL语句, 将第五个课程名称改为当前的时间
        String now = LocalDateTime.now().toString();
        String sql = "UPDATE sia5.course SET course_name = '"+now+"' WHERE id=5";
        //执行
        jdbcTemplate.execute(sql);
    }
}

其实这段小代码就非常直观的说明了JDBCTemplate的本质, 也是依赖一个DataSource, 然后对JDBC进行了封装的一个操作类.

XML配置如下, 使用的时候通过容器拿出来创建的JDBCTemplate对应的Bean来使用:

<!--DataSource依然需要-->
<bean class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" p:defaultAutoCommit="true" id="source"
      p:driverClassName="com.mysql.cj.jdbc.Driver"
      p:url="jdbc:mysql://localhost:3306/sia5"
      p:username="root"
      p:password="fflym0709"
/>

<!--配置一个JDBCTemplate的Bean-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="source" id="jdbcTemplate"/>

然后就可以老样子, 创建容器, 获取Bean来使用:

public static void main(String[] args) {
    Resource res = new FileSystemResource("D:\\Coding\\Java\\practice\\src\\main\\java\\spconfig.xml");
    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
    reader.loadBeanDefinitions(res);

    JdbcTemplate template = (JdbcTemplate) factory.getBean("jdbcTemplate");

    String now = LocalDateTime.now().toString();
    String sql = "UPDATE sia5.course SET course_name = '"+now+"' WHERE id=5";

    template.execute(sql);
}

有了XML配置, 使用注解进行构造注入创建一个DAO也很简单了, 就不再赘述了.

基本操作 - 改

上一节其实就是Spring如何整合JDBC. 现在就看一下基础操作.

原来JDBC的操作是获取连接, 然后获取Statement系列对象, 准备SQL语句, 执行后得到结果集, 然后从结果集中取出数据的过程.

在有了JDBCTemplate之后, 整体的操作简化为了三个步骤:

  1. 定义SQL语句
  2. 准备参数
  3. 执行并得到结果

数据操作不外乎增删改查, 增和删还是比较容易的, 重点看一下改和查.

update系列语句

这一节说是改, 实际上增删改, 全部都使用update()方法. 查使用query系列方法. 只有DDL语句才使用JDBCTemplate的execute()方法.

JDBCTemplate提供了很多重载的update()方法, 一个一个看过来.

首先最基本的update(sql)方法, 用于不附带参数的SQL语句, 例如:

public static void main(String[] args) {
    //此处创建容器和获取Bean的代码省略
    JdbcTemplate template = (JdbcTemplate) factory.getBean("jdbcTemplate");

    String sql = "UPDATE sia5.course SET course_name = 'Piano' WHERE id=5";
    //返回成功影响的行数
    template.update(sql);
}

如果要执行带参数的SQL语句, 就需要使用update(sql, params)的重载, params是一个Object[]类型:

String sql = "UPDATE sia5.course SET course_name = 'Piano' WHERE id=?";

Object[] params = new Object[]{3};

template.update(sql, params);

不过这样的话, 其实还有一个类型自动转换的问题, 这里因为是基本类型还问题不大, 所以一般使用下边一个重载, 最后一个参数明确的指出来SQL语句各个参数的类型:

String sql = "UPDATE sia5.course SET course_name = ? WHERE id=?";

Object[] params = new Object[]{"Computer History", 3};

template.update(sql, params, new int[]{Types.VARCHAR, Types.INTEGER});

这里可以看到, 占位符是两个, 第一个是一个字符串, 第二个是一个整数. 在params数组中, 也确实按照参数顺序写了一个字符串和一个int. 在update()方法中, 使用重载, 第三个参数传入一个int类型的数组, 其中按照参数的顺序, 采用java.sql.Types类中规定好的类型, 这样就可以告诉JDBCTemplate每个参数对应的数据库中的类型到底是什么, 以便进进行正确的转换.

所以带参的update方法, 都推荐使用一种三参数的重载.

此外还有类似的update(sql, Object... args), 用于不定参数.

然后还一个比较重要的, 就是可以自己来控制参数的回调接口: update(String sql, PreparedStatementSetter pss), 用于手工指定绑定的参数. 这个接口有一个方法 void setValues(PreparedStatement ps).

在内部, update()方法也是基于这个回调接口来绑定参数的, 如果自己使用这个重载, 可以更详细的控制参数绑定:

//创建一个演示用的List
List<String> courses = new ArrayList<>();
courses.add("Algorithm");
courses.add("Music Theory");
courses.add("Piano Practice");
courses.add("Computer History");
courses.add("Java Programming");

//SQL语句依然有两个参数
String sql = "UPDATE sia5.course SET course_name = ? WHERE id=?";

//使用带回调函数的方法
template.update(sql, new PreparedStatementSetter() {
    @Override
    public void setValues(PreparedStatement preparedStatement) throws SQLException {
        //setValues方法的参数PreparedStatement, 里边其实已经包裹了前边的sql语句, 所以就和JDBC设置参数是一样的
        System.out.println("绑定的参数1 course_name=" + courses.get(3));
        //只需要注意SQL参数位置起始是1
        preparedStatement.setString(1, courses.get(3));
        System.out.println("绑定的参数2 id=" + (courses.get(3).length() % 5 + 1));
        preparedStatement.setInt(2, courses.get(3).length() % 5 + 1);
    }
});

这个方法就是将参数绑定的具体控制, 通过PreparedStatementSetter接口中的方法来控制, 使用这个重载, 可以更详细的控制绑定参数的逻辑.

与这个类似的重载是使用 PreparedStatementCreator 接口, 这个接口从名称可以看出来, 可以自己根据需要创建PreparedStatement对象, 因此update()方法使用这个接口的话, 连sql都不需要传入, 可以彻底自己自定义:

//创建一个演示用的List
List<String> courses = new ArrayList<>();
courses.add("Algorithm");
courses.add("Music Theory");
courses.add("Piano Practice");
courses.add("Computer History");
courses.add("Java Programming");

//SQL语句依然有两个参数
String sql = "UPDATE sia5.course SET course_name = ? WHERE id=?";

template.update(new PreparedStatementCreator() {
    @Override
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        System.out.println("绑定的参数1 course_name=" + courses.get(2));
        preparedStatement.setString(1, courses.get(2));
        System.out.println("绑定的参数2 id=" + (courses.get(2).length() % 5 + 1));
        preparedStatement.setInt(2, courses.get(2).length() % 5 + 1);
        return preparedStatement;
    }
});

可以看到, PreparedStatementCreator接口的作用是从connection中创建一个PreparedStatement, 所以这里可以自己定义使用什么样的SQL语句和具体的参数绑定方法.

从这几种方法重载, 尤其是回调接口的重载可以看出来, JDBCTemplate确实是对JDBC的封装, 其内部的原理依然是通过连接创建Statement对象, 进而进行数据更新.

当然, 如果确定的知道参数绑定, 则没有必要使用这些回调接口. 查看源码可以知道, 在使用那些简单的重载方法时候, JDBCTemplate内部会自动创建这些回调接口(毕竟是框架, 已经搭好了). 只有确实需要详细控制的时候再使用.

总结一下常用的修改数据相关的update():

  1. update(String sql), 执行不带参数的SQL语句
  2. update(String sql, Object[] params), 执行带参数的SQL语句
  3. update(String sql, Object[] params, new int[]{Types.XXX}), 执行带参数, 且详细描述参数类型的SQL语句
  4. update(String sql, Object... params), 执行不定长参数的SQL语句, 不太常用
  5. update(String sql, PreparedStatementSetter pss), sql语句确定, 参数如何绑定由回调接口中的方法确定
  6. update(PreparedStatementCreator psc), 由于JDBC的数据库操作本质上都是从一个连接开始获取Statement然后执行, 所以这里将PreparedStatement的创建和参数绑定全部交给PreparedStatementCreator的createPreparedStatement()方法来完成, 可以在其中自由的编写从connection开始的全部逻辑.

如果需要批量更改, 可以使用 int[] batchUpdate(String[] sql), 可以组成一个数组, 或者使用一个带回调的 int[] batchUpdate(String sql, BatchPreparedStatementSetter pss), 这个回调接口会按次序一个一个处理过来.

一批都是一起提交的, 而不是再分成更小的批次. 如果有非常大的数据, 需要先将SQL语句分批, 对于每一批使用这个批量更改.

基本操作 - 增

添加数据的SQL操作, 也是使用update()语句. (注意, 只有DDL语句才使用JDBCTemplate的execute()语句, DDL语句就是数据定义语言, 即创建表, 删除表和改变表设计等语句.

单独的插入很容易, 这里要注意的是如何在插入后获取主键. JDBC 3.0规范中允许在插入后将数据库自动生成的主键绑定到一个值中:

public static void main(String[] args) throws SQLException {
    DataSource source = (DataSource) Util.getIOCContainer().getBean("source");

    Connection connection = source.getConnection();

    String sql = "INSERT into sia5.course (course_name) VALUES ('new course');";

    PreparedStatement preparedStatement = connection.prepareStatement(sql);

    preparedStatement.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);

    ResultSet resultSet = preparedStatement.getGeneratedKeys();

    if (resultSet.next()) {
        System.out.println(resultSet.getInt(1));
    }
}

新规范里executeUpdate多了一个重载, 即传入一个Statement.XXX的静态值, 如果设置为Statement.RETURN_GENERATED_KEYS, 则就会将新插入的数据的主键绑定到当前的Statement对象上.

在执行完这个语句的时候, 就可以通过Statement对象的getGeneratedKeys()获取一个ResultSet, 其中就存放着新插入的数据对应的主键.

Spring里是怎么做的呢, 其实也是提供了一个回调接口较早KeyHolder, 利用了一个update方法的重载:

String sql = "INSERT into sia5.course (course_name) VALUES ('new course');";

JdbcTemplate template = (JdbcTemplate) Util.getIOCContainer().getBean("jdbcTemplate");

//创建一个keyHolder对象
KeyHolder keyHolder = new GeneratedKeyHolder();

//update方法会自动将绑定的主键装入KeyHolder对象中
template.update(new PreparedStatementCreator() {
    @Override
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
        return connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
    }
}, keyHolder);

int key = keyHolder.getKey().intValue();

System.out.println("刚插入的主键=" + key);

注意红色部分的代码, 必须在prepareStatement方法中, 指定开启绑定主键的功能, 否则会报错.

在JDBC 3.0之前, 如果想要获取主键, 是需要在插入之后再执行一条查询主键的命令, 然后由于并发, 会返回当前的最后一条主键, 未必是插入时候的主键.

现在的JDBC 3.0绑定主键解决了这个问题, 然而今后开发中, 不应该使用表自增主键, 最好将生成主键的任务交给业务层, 比如使用UUID或者其他类似的功能.

基本操作 - 查

在原生JDBC操作数据库的时候, 得到的都是一个ResultSet结果集, 再从结果集中取出数据.

由于结果集就是对一行一行数据的封装, 所以JDBCTemplate这里提供了回调接口, 可以直接从结果集中将结果处理完放到指定的对象中. 这就大大节省了处理ResultSet的样板代码.

RowCallbackHandler

这个接口内部一行一行的处理ResultSet中的数据, 只有一个方法, 方法的参数ResultSet就是结果集, 将其想象成这个方法在处理每一行数据就可以了:

String sql = "Select * FROM sia5.course";

JdbcTemplate template = (JdbcTemplate) Util.getIOCContainer().getBean("jdbcTemplate");

//这个query方法的返回值是void
template.query(sql, new RowCallbackHandler() {
    @Override
    public void processRow(ResultSet resultSet) throws SQLException {
        System.out.println(resultSet.getInt(1) + "|" + resultSet.getString("course_name"));
    }
});

可见有了这个方法, 想怎么处理数据都可以了.

这是最基础的方法, 如果查询语句也带有参数, 可以想到和上边的update()是类似的, 回调对象总归是放在最后一个参数的位置. 所有重载等到最后再总结.

RowMapper<T>

使用RowMapper<T>的时候, query重载方法的返回值都变了, 是一个List<T>对象. RowMapper中每个方法需要返回一个泛型对象, 最后就会得到一个组装起来的List<T>对象:

看一个例子, 我们定义一个Course类:

public class Course {

    private int id;
    private String courseName;

    public Course() {

    }

    public Course(int id, String courseName) {
        this.id = id;
        this.courseName = courseName;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCourseName() {
        return courseName;
    }

    public void setCourseName(String courseName) {
        this.courseName = courseName;
    }
}

然后使用RowMapper<Course>来得到一个List<Course>结果:

String sql = "Select * FROM sia5.course";

JdbcTemplate template = (JdbcTemplate) Util.getIOCContainer().getBean("jdbcTemplate");

List<Course> courseList = template.query(sql, new RowMapper<Course>() {
    @Override
    public Course mapRow(ResultSet resultSet, int i) throws SQLException {
        Course course = new Course();
        course.setId(resultSet.getInt(1));
        course.setCourseName(resultSet.getString(2));
        return course;
    }
});

System.out.println(courseList);

这里要注意红色的泛型, 可以将RowMapper的mapRow方法看做把一行数据映射到一个对象的方法, 最后返回的结果就是装着这些对象的一个列表.

这个方法在抽象层次上比RowCallbackHandler又提升了一个层级, 可以直接根据自己想要的逻辑, 返回数据对象. 当然其本质还是对行数据的操作.

知道了原理, 剩下的重载也都是换参数而已, 这里把常用的查询重载一起列出:

    RowCallbackHandler系列:
  • void query(String sql, RowCallbackHandler rch), 不带参的查询语句, 查出来后处理每行数据, 返回为空
  • void query(String sql, Object[] params, RowCallbackHandler rch), 带参查询语句和处理.
  • void query(String sql, Object[] params, new int[]{Types}, RowCallbackHandler rch), 带参查询语句和处理, 类型安全版本.
  • void query(String sql, PreparedStatamentSetter pss, RowCallbackHandler rch), 知道了PreparedStatamentSetter对象, 就可以知道这个重载用于手工控制SQL语句中的参数绑定
  • void query(PreparedStatamentCreate psc, RowCallbackHandler rch), 使用PreparedStatamentCreate进行全部控制.
  • RowMapper系列:
  • List&ltT> query(String sql, RowMapper<T> rm), 不带参的查询.
  • List&ltT> query(String sql, Object[] params, RowMapper<T> rm), 带参的查询.
  • List&ltT> query(String sql, PreparedStatamentSetter pss, RowMapper<T> rm), 套路和上边的一样.
  • List&ltT> query(PreparedStatamentCreate psc, RowMapper<T> rm), 依然是熟悉的套路.

RowCallbackHandler和RowMapper的区别在于数量比较大的时候, RowCallbackHandler边处理边读入新数据, 类似于流式处理, 而RowMapper因为要返回一个List, 所以会读入全部数据, 这要小心会造成一些问题.

RowCallbackHandler内部记录了一个处理到多少行的变量, 因此不能多线程复用.

除了使用匿名类直接编写逻辑之外, Spring提供了RowCallbackHandler和RowMapper的几个实现类:

    RowCallbackHandler实现类:
  • RowCountCallbackHandler, 计算结果集的行数. 生成一个RowCountCallbackHandler对象然后传入update, 之后可以通过.getRowCount()获取行数.
  • SimpleRowCountCallbackHandler, 只遍历结果集, 不做任何处理的回调对象, 可以在测试的时候使用.
  • RowMapper实现类:
  • ColumnMapRowMapper, 每一行映射为一个Map对象, 键是列名, 值就是值, query方法最后返回的结果是List<Map<String, Object>>.
  • SingleColumnMapRowMapper, 将结果集中的某一列映射为一个对象.

查询单值

还有一个常用的操作是查询单值, 对应的方法是queryForObject, 直接查出来一个结果, 简单快速.

查询单值原来还有queryForInt, queryForLong,都已经被删除了, 现在查询单值就一个方法及重载: <T> T queryForObject(....).

要使用查询单值的方法, 返回的结果集中必须仅仅只有一行一列. 如果有多行, 压根不能使用这个方法. 如果是一行多列, 则必须手工使用RowMapper来构造一个对象.

看一个简单的例子, 查一下一共有几门课程:

String sql = "Select count(*) FROM sia5.course";

JdbcTemplate template = (JdbcTemplate) Util.getIOCContainer().getBean("jdbcTemplate");

int rows = template.queryForObject(sql, Integer.class);

System.out.println("一共有"+rows+"行");

知道了这个简单的例子, 后边那些泛型也就不用在一个一个罗列出来了, 本质上还是带参不带参, 以及使用RowMapper对象.

其他知识点

操作BLOB, CLOB数据, RowSet等方式, 用到的时候再看也可以.

主键在今后的项目中也要注意, 从业务层面产生主键是一个很好的想法.

Spring提供的两个JDBC模板类 NamedParameterJdbcTemplate和SimpleJdbcTemplate, 先了解, 需要的时候再使用.

LICENSED UNDER CC BY-NC-SA 4.0
Comment