上个星期博主到杭州务工了五天, 杭州不愧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<T> query(String sql, RowMapper<T> rm), 不带参的查询.
  • List<T> query(String sql, Object[] params, RowMapper<T> rm), 带参的查询.
  • List<T> query(String sql, PreparedStatamentSetter pss, RowMapper<T> rm), 套路和上边的一样.
  • List<T> 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, 先了解, 需要的时候再使用.