跳至主要內容

数据连接

wangdx大约 38 分钟

SpringJDBC 简介

JDBC 数据库编程

  • 任何一个商业项目之中都会保存有大量重要的商业数据,,而为了规范化数据的存储结构往往会围绕着关系型数据库进行代码编写,Java 在设计之初就确定了面向企业平台构建的目标,所以其内部提供了一套完善的 JDBC 服务标准,依靠此服务标准,所有的关系型数据库生产商都可以直接实现 Java 技术平台的接入

基于 ORM 开发应用

  • 由于 JDBC 是一套服务标准,所以在整个的 JDBC 之中全部由大量的操作接口所组成,这些接口的具体实现类则由不同的数据库厂商提供,这样一来每一位开发者就不得不按照 IDBC 的开发标准要求,编写大量重复的程序代码,不仅代码编写速度下降,同时也降低了代码的可维护性,更重要的是如果处理不当,则会带来严重的性能与安全问题。
  • 为了可以进一步规范数据库应用程序的开发,避免不良的编写习惯所带来的各种安全隐患,现代的项目往往都会基于 ORMapping 开发框架进行数据库应用的实现。ORMapping 组件可以帮助开发者实现 JDBC 代码的封装,也带来了更丰富的数据处理支持,不仅可以简化代码,同时也可以带来较好的处理性能。

DriverManagerDataSource

DriverManagerDataSource 继承结构

  • 使用 JDBC 进行数据库应用开发过程之中,需要基于 DriverManager.getConnection()方法获取数据库连接对象后才可以执行具体的 SQL 命令,而在 SpringJDBC 之中考虑到数据库连接操作的便捷,提供了一个 DriverManagerDataSource 的驱动管理类,相关的类继承结构如图所示。开发者可以将与数据库连接的相关信息配置到该类之中,并利用该类提供的 getConnection()方法实现数据库连接对象的获取,下面通过一个具体的操作配置来实现数据库连接的获取,
1、
// https://mvnrepository.com/artifact/org.springframework/spring-jdbc
implementation group: 'org.springframework', name: 'spring-jdbc', version: '6.0.0-M3'
// https://mvnrepository.com/artifact/mysql/mysql-connector-java
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.29'


2、
DROP DATABASE IF EXISTS yootk;
CREATE DATABASE yootk CHARACTER SET UTF8;
USE yootk;
CREATE TABLE book(
    bid     BIGINT  AUTO_INCREMENT comment '图书ID',
    title   VARCHAR(50) NOT NULL comment '图书名称',
    author VARCHAR(50) NOT NULL comment '图书作者',
    price DOUBLE comment '图书价格',
    CONSTRAINT pk_bid PRIMARY KEY(bid)
) engine=innodb;

3、
package com.yootk.jdbc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

@Configuration // 配置类
public class DataSourceConfig {
    @Bean("dataSource") // 配置Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(); // 驱动数据源
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); // 加载驱动程序
        dataSource.setUrl("jdbc:mysql://localhost:3306/yootk"); // 连接路径
        dataSource.setUsername("root"); // 用户名
        dataSource.setPassword("mysqladmin"); // 密码
        return dataSource;
    }
}


4、
package com.yootk.test;

import com.yootk.jdbc.config.DataSourceConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.sql.DataSource;

@ContextConfiguration(classes = {DataSourceConfig.class})
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestDataSource {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestDataSource.class);
    @Autowired
    private DataSource dataSource; // 注入DataSource接口实例
    @Test
    public void testConnection() throws Exception { // 测试数据库连接
        LOGGER.info("数据库连接对象:{}", this.dataSource.getConnection());
    }
}

HikariCP 数据库连接池

HikariCP

  • 在实际的项目应用开发过程之中,为了解决 JDBC 连接与关闭的延时以及性能问题,提供了数据库连接池的解决方案,并且针对于该方案提供了成型的 HikariCP 服务组件。HikariCp(Hikari 来自日文,“光”的含义)是由日本程序员开源的一个数据库连接 HikariCp 是池组件,该组件拥有如下特点:
    • 字节码更加的精简,这样可以在缓存中添加更多的程序代码,
    • 实现了一个无锁集合,减少了并发访问造成的资源竞争问题;
    • 使用了自定义数组类型(FastList)代替了 ArrayList,提高了 get()与 remove()的操作性能
    • 针对 CPU 的时间片算法进行了优化,尽可能在一个时间片内完成所有处理操作。。

DataSource 与连接池组件

  • 在当前的项目应用过程之中,会存在有大量的数据库连接池组件,按照使用范围的大小来讲包括有 Druid、HikariCP、BoneCP、C3P0,但是不管使用何种数据库连接池,所有的连接池组件内部一定会提供有一个 DataSource 接口实现子类,如图所示.Sorind 应用程序只关心当前容器中,是否包含有 DataSource 接口实例,而不关心具体的实现细节,下面将通过具体的步骤实现 HikariCP 数据库连接池的整合。
1、
implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.0.1'
// https://mvnrepository.com/artifact/com.zaxxer/HikariCP
implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.1.0'

2、
# 【JDBC】数据库驱动程序米可能构成
yootk.database.driverClassName=com.mysql.cj.jdbc.Driver
# 【JDBC】数据库连接地址路径
yootk.database.jdbcUrl=jdbc:mysql://localhost:3306/yootk
# 【JDBC】数据库连接的用户名
yootk.database.username=root
# 【JDBC】数据库连接密码
yootk.database.password=mysqladmin
# 【HikariCP】配置数据库连接超时的时间(单位:毫秒)
yootk.database.connectionTimeOut=3000
# 【HikariCP】配置当前的数据库是否为只读状态
yootk.database.readOnly=false
# 【HikariCP】配置一个连接最小的维持时间长度(单位:毫秒)
yootk.database.pool.idleTimeOut=3000
# 【HikariCP】配置一个连接最长的存活时间(单位:毫秒)
yootk.database.pool.maxLifetime=6000
# 【HikariCP】数据库连接池最大提供60个数据库连接对象实例
yootk.database.pool.maximumPoolSize=60
# 【HikariCP】数据库连接池在没有任何用户访问的时候,最少维持20个数据库连接
yootk.database.pool.minimumIdle=20

3、



package com.yootk.jdbc.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration // 配置类
@PropertySource("classpath:config/database.properties") // 读取资源文件
public class DataSourceConfig {
    @Value("${yootk.database.driverClassName}") // 绑定资源文件中的配置数据项
    private String driverClassName; // JDBC驱动程序
    @Value("${yootk.database.jdbcUrl}") // 绑定资源文件中的配置数据项
    private String jdbcUrl; // JDBC连接地址
    @Value("${yootk.database.username}") // 绑定资源文件中的配置数据项
    private String username; // JDBC连接用户名
    @Value("${yootk.database.password}") // 绑定资源文件中的配置数据项
    private String password; // JDBC连接密码
    @Value("${yootk.database.connectionTimeOut}") // 绑定资源文件中的配置数据项
    private long connectionTimeout; // JDBC连接超时时间
    @Value("${yootk.database.readOnly}") // 绑定资源文件中的配置数据项
    private boolean readOnly; // 是否为只读数据库
    @Value("${yootk.database.pool.idleTimeOut}") // 绑定资源文件中的配置数据项
    private long idleTimeOut; // 连接池之中的,一个连接的存活最小时间
    @Value("${yootk.database.pool.maxLifetime}") // 绑定资源文件中的配置数据项
    private long maxLifetime; // 连接池之中的,一个连接的存活最长时间
    @Value("${yootk.database.pool.maximumPoolSize}") // 绑定资源文件中的配置数据项
    private int maximumPoolSize; // 连接池最大维持的长度
    @Value("${yootk.database.pool.minimumIdle}") // 绑定资源文件中的配置数据项
    private int minimumIdle; // 连接池最小维持的长度
    @Bean("dataSource") // 配置Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(this.driverClassName);
        dataSource.setJdbcUrl(this.jdbcUrl);
        dataSource.setUsername(this.username);
        dataSource.setPassword(this.password);
        dataSource.setConnectionTimeout(this.connectionTimeout);
        dataSource.setReadOnly(this.readOnly);
        dataSource.setIdleTimeout(this.idleTimeOut);
        dataSource.setMaxLifetime(this.maxLifetime);
        dataSource.setMaximumPoolSize(this.maximumPoolSize);
        dataSource.setMinimumIdle(this.minimumIdle);
        return dataSource;
    }
}

JdbcTemplate 操作模版

JdbcTemplate 数据库操作模版

  • 在项目业务开发中,往往都需要通过数据库实现业务相关数据的存储,虽然 java 提供了原生的 JDBC 实现支持,但是传统的 JDBC 开发步骤过于繁琐。为了简化 JDBC 开发的繁琐程度,在 Spring 中对 JDBC 的使用进行了包装,提供了一个半自动化的 SpringJDBC 组件,同时在该组件中提供了 JdbcTemplate 操作模版类继承结构如图所示。开发者可以直接通过此 JdbcTemplate 模版类的对象,基于已经存在的 DataSource 实例,实现指定数据库的 SQL 命令操作。
1、
package com.yootk.jdbc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class SpringJDBCConfig {
    @Bean("jdbcTemplate")
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate template = new JdbcTemplate(); // 实例化JDBC操作模版
        template.setDataSource(dataSource); // 设置数据源
        return template;
    }
}


2、
package com.yootk.test;

import com.yootk.jdbc.config.DataSourceConfig;
import com.yootk.jdbc.config.SpringJDBCConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

// 按照道理来讲,此时应该定义一个总的配置类,而后在这个配置类上写上扫描包就可以自动注册了
@ContextConfiguration(classes = {DataSourceConfig.class, SpringJDBCConfig.class})
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestJdbcTemplate {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestJdbcTemplate.class);
    @Autowired
    private JdbcTemplate jdbcTemplate; // 注入JDBC操作模版
    @Test
    public void testInsertSQL() throws Exception { // 数据操作测试
        String sql = "INSERT INTO book(title, author, price) VALUES " +
                " ('Java从入门到项目实战', '沐言科技 - 李兴华', 99.8)"; // 传统的不能再传统的SQL语句
        LOGGER.info("SQL增加命令:{}", this.jdbcTemplate.update(sql)); // 执行SQL语句
    }
}

JdbcTemplate 数据更新操作

JdbcTemplate 预处理更新操作

  • 直接使用 SQL 命令实现数据更新,会存在有严重的安全隐患,传统 JDBC 的做法是通过 PreparedStatement 基于预处理的模式实现进行数据占位符配置,而后再设置具体的数据项,以实现最终的数据更新处理,这一机制在 JdbcTemplate 模版工具类中也都提供有相应的支持,最为简单的处理形式就是直接通过 update()方法传递更新 SQL(包与位符),并通过可变参数传递 SQL 更新数据。在 JdbcTemplate 类中整体的实现也是基于 PreparedStatement 完成的,这一点可以通过图所示结构进行观察。

1、
@Override
public int update(final String sql) throws DataAccessException {
   Assert.notNull(sql, "SQL must not be null");
   if (logger.isDebugEnabled()) {
      logger.debug("Executing SQL update [" + sql + "]");
   }
   // Callback to execute the update statement.
   // JdbcTemplate为了尽可能满足大家的一切诉求,所以这个时候都会提供一系列的Callback处理
   class UpdateStatementCallback implements StatementCallback<Integer>, SqlProvider {
      @Override
      public Integer doInStatement(Statement stmt) throws SQLException {
         int rows = stmt.executeUpdate(sql); // 执行SQL更新操作
         if (logger.isTraceEnabled()) {
            logger.trace("SQL update affected " + rows + " rows");
         }
         return rows;
      }
      @Override
      public String getSql() {
         return sql;
      }
   }
   return updateCount(execute(new UpdateStatementCallback(), true));
}


2、
public int update(String sql, @Nullable Object... args) throws DataAccessException {
   return update(sql, newArgPreparedStatementSetter(args));
}
public int update(String sql, @Nullable PreparedStatementSetter pss)
throws DataAccessException {
   return update(new SimplePreparedStatementCreator(sql), pss);
}
protected int update(final PreparedStatementCreator psc,
@Nullable final PreparedStatementSetter pss) throws DataAccessException {
   logger.debug("Executing prepared SQL update");
   return updateCount(execute(psc, ps -> {
      try {
         if (pss != null) {
            pss.setValues(ps);
         }
         int rows = ps.executeUpdate();
         if (logger.isTraceEnabled()) {
            logger.trace("SQL update affected " + rows + " rows");
         }
         return rows;
      }
      finally {
         if (pss instanceof ParameterDisposer) {
            ((ParameterDisposer) pss).cleanupParameters();
         }
      }
   }, true));
}
private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action,
 boolean closeResources) throws DataAccessException {
   Assert.notNull(psc, "PreparedStatementCreator must not be null");
   Assert.notNull(action, "Callback object must not be null");
   if (logger.isDebugEnabled()) {
      String sql = getSql(psc);
      logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
   }
   Connection con = DataSourceUtils.getConnection(obtainDataSource());
   PreparedStatement ps = null;
   try {
      ps = psc.createPreparedStatement(con);
      applyStatementSettings(ps);
      T result = action.doInPreparedStatement(ps);
      handleWarnings(ps);
      return result;
   } catch (SQLException ex) {
      // Release Connection early, to avoid potential connection pool deadlock
      // in the case when the exception translator hasn't been initialized yet.
      if (psc instanceof ParameterDisposer) {
         ((ParameterDisposer) psc).cleanupParameters();
      }
      String sql = getSql(psc);
      psc = null;
      JdbcUtils.closeStatement(ps);
      ps = null;
      DataSourceUtils.releaseConnection(con, getDataSource());
      con = null;
      throw translateException("PreparedStatementCallback", sql, ex);
   }
   finally {
      if (closeResources) {
         if (psc instanceof ParameterDisposer) {
            ((ParameterDisposer) psc).cleanupParameters();
         }
         JdbcUtils.closeStatement(ps);
         DataSourceUtils.releaseConnection(con, getDataSource());
      }
   }
}


3、
package com.yootk.test;

import com.yootk.jdbc.config.DataSourceConfig;
import com.yootk.jdbc.config.SpringJDBCConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

// 按照道理来讲,此时应该定义一个总的配置类,而后在这个配置类上写上扫描包就可以自动注册了
@ContextConfiguration(classes = {DataSourceConfig.class, SpringJDBCConfig.class})
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestJdbcTemplate {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestJdbcTemplate.class);
    @Autowired
    private JdbcTemplate jdbcTemplate; // 注入JDBC操作模版
    @Test
    public void testInsertSQL() throws Exception { // 数据操作测试
        String sql = "INSERT INTO book(title, author, price) VALUES " +
                " ('Java从入门到项目实战', '沐言科技 - 李兴华', 99.8)"; // 传统的不能再传统的SQL语句
        LOGGER.info("SQL增加命令:{}", this.jdbcTemplate.update(sql)); // 执行SQL语句
    }
    @Test
    public void testInsert() throws Exception { // 数据操作测试
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        LOGGER.info("SQL增加命令:{}", this.jdbcTemplate.update(
                sql, "Python从入门到项目实战", "爆可爱的小李", 98.8)); // 执行SQL语句
    }
    @Test
    public void testUpdate() throws Exception { // 数据操作测试
        String sql = "UPDATE book SET title=?, author=?, price=? WHERE bid=?";
        LOGGER.info("SQL更新命令:{}", this.jdbcTemplate.update(
                sql, "Java进阶开发实战", "爆可爱的小李", 59.8, 1)); // 执行SQL语句
    }
    @Test
    public void testDelete() throws Exception { // 数据操作测试
        String sql = "DELETE FROM book WHERE bid=?";
        LOGGER.info("SQL更新命令:{}", this.jdbcTemplate.update(sql, 1)); // 执行SQL语句
    }
}

KeyHolder

KeyHolder 获取自动增长 ID

  • 在数据库表结构设计过程之中,提供有自动增长列的支持,如果要想在每次数据增加完成后获取该增长列的内容,在 SpringJDBC 之中可以通过 KeyHolder 接口完成。 JdbcTemplate 中提供有一个用于接收 KeyHolder 接口实例的 update()方法该方法内部的源代码就是通过 Statement 接口中提供的 getGeneratedKeys()方法实现增长 ID 的获取
1、

    @Test
    public void testKeyHolder() throws Exception {
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        KeyHolder keyHolder = new GeneratedKeyHolder(); // 获取KEY的处理信息
        int count = this.jdbcTemplate.update(new PreparedStatementCreator() {
            @Override
            public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
                PreparedStatement pstmt = con.prepareStatement(sql,
                        Statement.RETURN_GENERATED_KEYS); // 实例化数据库操作
                pstmt.setString(1, "SpringBoot开发实战"); // 设置占位符数据
                pstmt.setString(2, "李兴华"); // 设置占位符数据
                pstmt.setDouble(3, 67.8); // 设置占位符数据
                return pstmt;
            }
        }, keyHolder); // 数据更新
        LOGGER.info("SQL更新数据行数:{}、当前数据的ID:{}", count, keyHolder.getKey());
    }

数据批处理

数据库批处理更新

  • 项目应用之中经常需要进行数据的批量增加操作,如果此时采用单条更新语句的模式则一定会受到 IO 性能的影响。而常规的做法是直接基于批处理的形式,通过一次 I0 的操作实现多条数据的增加

dbcTemplate 提供的数据批量更新方法

  • JDBCTemplate 作为一款设计结构良好的 ORM 开发组件,其内部也提供有完整的数据批处理操作的处理方法,这些方法如表所示,由于 JDBCTemplate 主要实现了 PreparedStatement 封装,所以批处理操作时都需要传入一条完整的 SQL 更新命令而后围绕着此命令进行数据的设置即可
1、
    @Test
    public void testBatch() throws Exception {  // 测试批量更新操作
        // 不可能真的去编写10W条记录,主要是说明应用的语法结构
        List<String> titles = List.of("Spring开发实战", "SSM开发实战", "SSM开发案例", "Netty开发实战", "Redis开发实战");
        List<Double> prices = List.of(69.8, 67.8, 65.3, 66.9, 57.3); // 定义价格
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        int result [] = this.jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { // 设置占位符的参数内容
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, titles.get(i));
                ps.setString(2, "沐言科技 - 李兴华");
                ps.setDouble(3, prices.get(i));
            }

            @Override
            public int getBatchSize() {
                return titles.size();
            }
        });
        LOGGER.info("SQL批处理更新:{}", Arrays.toString(result));
    }

2、
    @Test
    public void testBatchObject() throws Exception {
        List<Object[]> params = List.of(
                new Object [] {"Spring开发实战", "李兴华", 69.8},
                new Object [] {"SSM开发实战", "李兴华", 66.8},
                new Object [] {"SSM实战案例", "李兴华", 64.8},
                new Object [] {"SpringBoot开发实战", "李兴华", 62.8},
                new Object [] {"SpringCloud开发实战", "李兴华", 59.8}); // 参数定义
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        int result [] = this.jdbcTemplate.batchUpdate(sql, params); // 批量更新
        LOGGER.info("SQL批处理增加:{}", Arrays.toString(result));
    }

RowMapper

RowMapper 与结果集转换

  • 在数据库操作过程中,除了数据更新操作之外,最为繁琐的就是数据库的查询功能了由于 JdbcTemplate 设计的定位属于 ORMapping 组件,所以就需要在查询完成之后可以自动的将查询结果转为 VO 类型的实例,而为了解决该问题,在 SpringJDBC 中提供一个 RowMapper 接口,该接口提供有一个 mapRow()处理方法,可以接收查询结果每行数据的结果集,用户可以将指定列取出,并保存在目标 VO 实例之中

1、
package com.yootk.jdbc.vo;

public class Book {
    private Long bid; // 对应bid数据列
    private String title; // 对应title数据列
    private String author; // 对应author数据列
    private Double price; // 对应price数据列
    public Long getBid() {
        return bid;
    }
    public void setBid(Long bid) {
        this.bid = bid;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }
    public Double getPrice() {
        return price;
    }
    public void setPrice(Double price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "【图书信息】编号:" + this.bid + "、名称:" + this.title +
                "、作者:" + this.author + "、价格:" + this.price;
    }
}


2、
    @Test
    public void testQueryByID() throws Exception {
        String sql = "SELECT bid, title, author, price FROM book WHERE bid=?";
        Book book = this.jdbcTemplate.queryForObject(sql, new RowMapper<Book>() {
            @Override
            public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
                // 此时传入了一个ResultSet接口实例,这个接口实例可以获取查询结果
                Book book = new Book(); // 手工实例化VO对象
                book.setBid(rs.getLong(1)); // 将查询结果的内容保存在对象成员属性之中
                book.setTitle(rs.getString(2)); // 将查询结果的内容保存在对象成员属性之中
                book.setAuthor(rs.getString(3)); // 将查询结果的内容保存在对象成员属性之中
                book.setPrice(rs.getDouble(4)); // 将查询结果的内容保存在对象成员属性之中
                return book;
            }
        }, 3); // 查询指定编号的数据
        LOGGER.info("{}", book);
    }

3、
    @Test
    public void testQueryAll() throws Exception { // 查询全部
        String sql = "SELECT bid, title, author, price FROM book";
        List<Book> books = this.jdbcTemplate.query(sql, new RowMapper<Book>() {
            @Override
            public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
                // 此时传入了一个ResultSet接口实例,这个接口实例可以获取查询结果
                Book book = new Book(); // 手工实例化VO对象
                book.setBid(rs.getLong(1)); // 将查询结果的内容保存在对象成员属性之中
                book.setTitle(rs.getString(2)); // 将查询结果的内容保存在对象成员属性之中
                book.setAuthor(rs.getString(3)); // 将查询结果的内容保存在对象成员属性之中
                book.setPrice(rs.getDouble(4)); // 将查询结果的内容保存在对象成员属性之中
                return book;
            }
        } ); // 查询指定编号的数据
        for (Book book : books) {
            LOGGER.info("{}\n", book);
        }
    }

4、
【图书信息】编号:2、名称:Python从入门到项目实战、作者:爆可爱的小李、价格:98.8
【图书信息】编号:3、名称:SpringBoot开发实战、作者:李兴华、价格:67.8
【图书信息】编号:4、名称:SpringBoot开发实战、作者:李兴华、价格:67.8
【图书信息】编号:5、名称:Spring开发实战、作者:沐言科技 - 李兴华、价格:69.8
【图书信息】编号:6、名称:SSM开发实战、作者:沐言科技 - 李兴华、价格:67.8
【图书信息】编号:7、名称:SSM开发案例、作者:沐言科技 - 李兴华、价格:65.3
【图书信息】编号:8、名称:Netty开发实战、作者:沐言科技 - 李兴华、价格:66.9
【图书信息】编号:9、名称:Redis开发实战、作者:沐言科技 - 李兴华、价格:57.3
【图书信息】编号:10、名称:Spring开发实战、作者:李兴华、价格:69.8
【图书信息】编号:11、名称:SSM开发实战、作者:李兴华、价格:66.8
【图书信息】编号:12、名称:SSM实战案例、作者:李兴华、价格:64.8
【图书信息】编号:13、名称:SpringBoot开发实战、作者:李兴华、价格:62.8
【图书信息】编号:14、名称:SpringCloud开发实战、作者:李兴华、价格:59.8

5、
    @Test
    public void testQuerySplit() throws Exception { // 查询全部
        int currentPage = 1; // 当前页
        int lineSize = 5; // 每页显示数据行数
        String sql = "SELECT bid, title, author, price FROM book LIMIT ?,?";
        List<Book> books = this.jdbcTemplate.query(sql, new RowMapper<Book>() {
            @Override
            public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
                // 此时传入了一个ResultSet接口实例,这个接口实例可以获取查询结果
                Book book = new Book(); // 手工实例化VO对象
                book.setBid(rs.getLong(1)); // 将查询结果的内容保存在对象成员属性之中
                book.setTitle(rs.getString(2)); // 将查询结果的内容保存在对象成员属性之中
                book.setAuthor(rs.getString(3)); // 将查询结果的内容保存在对象成员属性之中
                book.setPrice(rs.getDouble(4)); // 将查询结果的内容保存在对象成员属性之中
                return book;
            }
        }, (currentPage - 1) * lineSize, lineSize); // 查询指定编号的数据
        for (Book book : books) {
            LOGGER.info("{}\n", book);
        }
    }

6、
【图书信息】编号:2、名称:Python从入门到项目实战、作者:爆可爱的小李、价格:98.8
【图书信息】编号:3、名称:SpringBoot开发实战、作者:李兴华、价格:67.8
【图书信息】编号:4、名称:SpringBoot开发实战、作者:李兴华、价格:67.8
【图书信息】编号:5、名称:Spring开发实战、作者:沐言科技 - 李兴华、价格:69.8
【图书信息】编号:6、名称:SSM开发实战、作者:沐言科技 - 李兴华、价格:67.8


7、
    @Test
    public void testQueryCount() throws Exception {
        String sql = "SELECT COUNT(*) FROM book WHERE title LIKE ?"; // 模糊查询
        long count = this.jdbcTemplate.queryForObject(sql, Long.class, "%开发实战%");
        LOGGER.info("数据个数统计:{}", count);
    }

JDBC 事务控制

业务层调用

  • 每一个项目都会由若干个不同处理业务所组成,而每一个业务在执行处理时又需要进行多次数据表的更新操作,所有的更新操作需要全部成功执行后才可以表示业务正确完成如果之中有一条更新操作无法正常执行,,则整体的业务应该以失败告终。为了实现这样的 ACID 处理机制,往往是在业务层之中进行数据库连接事务的管理

ACID 事务原则

  • ACID 主要指的是事务的四种特点:原子性(Atomicity)、一致性(Consistency)、隔离性或独立性(lsolation)、持久性(Durabilily)四个特征
  • 原子性(Atomicity):整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rolback)到事务开始前的状态,就像这个事务从来没有执行过一样;
  • 一致性(Consistency):一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少;
  • 隔离性(lsolation):隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统;
  • 持久性(Durability):在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中并不会被回滚。

JDBC 事务处理结构

  • 在使用原生 JDBC 技术进行项目开发时,为了保证业务的实现不受到事务的影响,就需要在项目内部引入动态代理机制。在代理业务类中,基于 JDBC 提供的 java.sql.Connection 接囗的方法来实现事务的控制,首先要通过 setAutoCommit()方法禁用自动事务提交机制,而后再根据最终业务的执行来决定当前的数据库更新操作是否成功,如果成功则通过 commit()方法提交更新事务,如果业务调用出现异常,则使用 rollback()方法进行事务的回滚处理
1、
connection.setAutoCommit(false);

2、
connection.commit();

3、
connection.rollback();

4、
https://docs.spring.io/spring-framework/docs/6.0.0-M4/reference/html/data-access.html#spring-data-tier

Spring 事务处理架构

框架开发与事务处理

  • JDBC 实现的事务管理操作,属于程序事务处理最底层的设计,不同的数据库生产商依据 JDBC 的设计结构实现事务处理功能的对接,但是这样的对接是属于 JDBC 层次上的,是最原始的一种形式,而在实际项目的开发中,由于 JDBC 操作的繁琐,所以往往会基于 ORM 开发框架进行数据层的代码编写。由于所有的 ORM 开发框架都会存在有各自的事务管理机制,最终的结果就是 Spring 无法实现 ORM 框架的事务管理整合。

PlatformTransactionManager 接口定义

Spring 事务管理接口

  • 在 PlatformTransactionManager 接口中还引用了事务状态(TransactionStatus)以及事务定义(TransactionDefinition)接口的关联,TransactionStatus 可以实现事务保存点的配置,而 TransactionDefinition 可以实现事务隔离级别以及事务传播属性的配置,这些都是 Spring 事务管理中的重要内容。同时在 Spring 5.x 版本后,为了进一步提升事务管理的定义,又提供了个 TransactionManaqer 父接口,但是该接口暂未提供具体的事务操作方法。

1、
package org.springframework.transaction;
import org.springframework.lang.Nullable;
// 2003年的时候流行的ORM开发框架有:JDO(被彻底淘汰了)、Hibernate、IBatis
public interface PlatformTransactionManager extends TransactionManager {
   TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
        throws TransactionException; // 获取事务的状态
   void commit(TransactionStatus status) throws TransactionException; // 事务提交
   void rollback(TransactionStatus status) throws TransactionException; // 事务回滚
}


2、

package org.springframework.transaction;
public interface TransactionManager {} // Spring 5.2之后提供的新接口

编程式事务控制

Spring 事务处理结构

  • PlatformTransactionManager 是事务处理的统一操作接口,在 Spring 内部提供了一个 DataSourceTransactionManager 的实现子类,继承结构如图所示,这样就可以直接通过该子类获取事务操作接口的实例,以实现最终所需要的事务处理,下面将通过一个具体的案例进行实现。
1、
package com.yootk.jdbc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration // 配置类
public class TransactionConfig {
    @Bean // 事务的注册
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        // PlatformTransactionManager是一个事务的控制标准,而后不同的数据库组件需要实现该标准。
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); // 事务实现子类
        transactionManager.setDataSource(dataSource); // 设置数据源
        return transactionManager;
    }
}


2、
package com.yootk.test;

import com.yootk.jdbc.config.DataSourceConfig;
import com.yootk.jdbc.config.SpringJDBCConfig;
import com.yootk.jdbc.config.TransactionConfig;
import com.yootk.jdbc.vo.Book;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;

import java.sql.*;
import java.util.Arrays;
import java.util.List;

@ContextConfiguration(classes = {DataSourceConfig.class, SpringJDBCConfig.class, TransactionConfig.class})
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestTransactionManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestTransactionManager.class);
    @Autowired
    private JdbcTemplate jdbcTemplate; // 注入JDBC操作模版
    @Autowired
    private PlatformTransactionManager transactionManager; // 事务控制
    @Test
    public void testInsert() throws Exception { // 数据操作测试
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        this.jdbcTemplate.update(sql, "Spring开发实战", "李兴华", 69.8);
        this.jdbcTemplate.update(sql, "Redis开发实战", null, 68.8);
        this.jdbcTemplate.update(sql, null, "李兴华", 68.8);
        this.jdbcTemplate.update(sql, "Netty开发框架", "李兴华", null);
    }
}


3、
package com.yootk.test;

import com.yootk.jdbc.config.DataSourceConfig;
import com.yootk.jdbc.config.SpringJDBCConfig;
import com.yootk.jdbc.config.TransactionConfig;
import com.yootk.jdbc.vo.Book;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import java.sql.*;
import java.util.Arrays;
import java.util.List;

@ContextConfiguration(classes = {DataSourceConfig.class, SpringJDBCConfig.class, TransactionConfig.class})
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestTransactionManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestTransactionManager.class);
    @Autowired
    private JdbcTemplate jdbcTemplate; // 注入JDBC操作模版
    @Autowired
    private PlatformTransactionManager transactionManager; // 事务控制
    @Test
    public void testInsert() throws Exception { // 数据操作测试
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        TransactionStatus status = this.transactionManager
                .getTransaction(new DefaultTransactionDefinition()); // 开启事务
        try {
            this.jdbcTemplate.update(sql, "Spring开发实战", "李兴华", 69.8);
            this.jdbcTemplate.update(sql, "Redis开发实战", null, 68.8);
            this.jdbcTemplate.update(sql, null, "李兴华", 68.8);
            this.jdbcTemplate.update(sql, "Netty开发框架", "李兴华", null);
            this.transactionManager.commit(status); // 事务提交
            LOGGER.debug("数据库事事务提交。");
        } catch (Exception e) {
            this.transactionManager.rollback(status); // 事务回滚
            LOGGER.error("数据库更新错误:{}", e.getMessage());
        }
    }
}

TransactionStatus

TransactionStatus 接口

  • 在 Spring 事务处理过程之中,会使用 PlatformTransactionManager 接口提供的 getTransaction()方法开启个新的事务,而后每一个新开启的事务都会通过 TransactionStatus 接口的实例化对象进行表示,同时开发者基于该接口提供的方法可以实现数据库事务状态的判断,以及是否允许事务提交的控制

数据库事务与 Savepoint

  • 在一次完整的数据库事务处理操作中,如果使用了 rollback()方法,则会对整体的更新操作进行回滚,如果说此时只需要其回滚到指定的位置处,那么就可以通过 TransactionStatus 中提供的 createSavepoint() 方法创建保存点,而后在每次出现异常时设置保存点并进行数据库事务提交即可
1、
    @Test
    public void testRollbackOnly() throws Exception { // 数据操作测试
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        TransactionStatus status = this.transactionManager
                .getTransaction(new DefaultTransactionDefinition()); // 开启事务
        status.setRollbackOnly(); // 只允许回滚事务
        try {
            this.jdbcTemplate.update(sql, "Spring开发实战", "李兴华", 69.8);
            // this.jdbcTemplate.update(sql, "Redis开发实战", null, 68.8);
            this.transactionManager.commit(status); // 事务提交
            LOGGER.debug("数据库事事务提交。");
        } catch (Exception e) {
            this.transactionManager.rollback(status); // 事务回滚
            LOGGER.error("数据库更新错误:{}", e.getMessage());
        }
    }

2、
    @Test
    public void testRollbackOnly() throws Exception { // 数据操作测试
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        TransactionStatus status = this.transactionManager
                .getTransaction(new DefaultTransactionDefinition()); // 开启事务
        status.setRollbackOnly(); // 只允许回滚事务
        LOGGER.info("【事务执行前】当前事务是否开启:{}、当前事务是否完成:{}",
                status.isNewTransaction(), status.isCompleted());
        try {
            this.jdbcTemplate.update(sql, "Spring开发实战", "李兴华", 69.8);
            this.jdbcTemplate.update(sql, "Redis开发实战", null, 68.8); // 产生错误
            this.transactionManager.commit(status); // 事务提交
            LOGGER.debug("数据库事事务提交。");
        } catch (Exception e) {
            this.transactionManager.rollback(status); // 事务回滚
            LOGGER.error("数据库更新错误:{}", e.getMessage());
        }
        LOGGER.info("【事务执行后】当前事务是否开启:{}、当前事务是否完成:{}",
                status.isNewTransaction(), status.isCompleted());
    }

3、
    @Test
    public void testSavePoint() throws Exception { // 数据操作测试
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        TransactionStatus status = this.transactionManager
                .getTransaction(new DefaultTransactionDefinition()); // 开启事务
        Object savePointA = null; // 记录点A
        Object savePointB = null; // 记录点B
        try {
            this.jdbcTemplate.update(sql, "Spring开发实战", "李兴华", 69.8);// 正确数据
            this.jdbcTemplate.update(sql, "SSM开发实战", "李兴华", 69.8); // 正确数据
            savePointA = status.createSavepoint(); // 创建第一个保存点
            LOGGER.info("【SAVEPOINT】{}", savePointA);
            this.jdbcTemplate.update(sql, "SSM开发案例", "李兴华", 69.8); // 正确数据
            savePointB = status.createSavepoint(); // 创建第二个保存点
            LOGGER.info("【SAVEPOINT】{}", savePointB);
            this.jdbcTemplate.update(sql, "Redis开发实战", null, 68.8);
            this.jdbcTemplate.update(sql, null, "李兴华", 68.8);
            this.jdbcTemplate.update(sql, "Netty开发框架", "李兴华", null);
            this.transactionManager.commit(status); // 事务提交
            LOGGER.debug("数据库事事务提交。");
        } catch (Exception e) {
            LOGGER.error("数据库更新错误:{}", e.getMessage());
            status.rollbackToSavepoint(savePointA); // 回滚到指定的位置
            this.transactionManager.commit(status); // 事务提交
        }
    }

事务隔离级别

数据库资源读取

  • 数据库是一个项目应用中的公共存储资源,所以在实际的项目开发过程中,很有可能会有两个不同的线程(每个线程拥有各自的数据库事务),要进行同一条数据的读取以及更新操作

脏读(Dirty reads)

  • 事务 A 在读取数据时,读取到了事务 B 未提交的数据,由于事务 B 有可能被回滚,所以该数据有可能是一个无效数据

不可重复读(Non-repeatable Reads)

  • 事务 A 对一个数据的两次读取返回了不同的数据内容,有可能在两次读取之间事务 B 对该数据进行了修改,一般此类操作出现在数据修改操作之中;

幻读(Phantom Reads)

  • 事务 A 在进行数据两次查询时产生了不一致的结果,有可能是事务 B 在事务 A 第二次查询之前增加或删除了数据内容所造成的

配置数据库隔离级别

  • Spring 之中的事务隔离级别需要在获取 TransactionStatus(事务状态)之前进行设置,可以通过 TransactionDefinition 接口子类进行定义,在 DefaultTransactionDefinition()子类中提共了 setlsolationLevel()方法,只需要根据需要传入 TransactionDefinition 接口定义的隔离级别配置常量即可。
1、
    @Test
    public void testTransactionIsolation() throws Exception { // 测试默认的隔离操作
        Integer bid = 8; // 要同时访问的ID编号
        String querySQL = "SELECT bid, title, author, price FROM book WHERE bid=?"; // 查询语句
        String updateSQL = "UPDATE book SET title=?, price=? WHERE bid=?"; // 更新语句
        RowMapper<Book> bookRowMapper = new BookRowMapper(); // 对象转换处理
        new Thread(()->{ // 每一个线程都应该拥有一个事务
            TransactionStatus statusA = this.transactionManager
                    .getTransaction(new DefaultTransactionDefinition()); // 【A】开启一个数据库事务
            Book bookA = this.jdbcTemplate.queryForObject(querySQL, bookRowMapper, bid); // 【A】查询指定ID的数据
            LOGGER.info("【{}】第一次数据查询:{}", Thread.currentThread().getName(), bookA);
            try { // 此时加入延迟的目的是为了让第二个线程可以进行数据的更新操作
                TimeUnit.SECONDS.sleep(5); // 强制性的延迟
                bookA = this.jdbcTemplate.queryForObject(querySQL, bookRowMapper, bid); // 【A】查询指定ID的数据
                LOGGER.info("【{}】第二次数据查询:{}", Thread.currentThread().getName(), bookA);
            } catch (InterruptedException e) {}
        }, "JDBC操作线程 - A").start(); // 定义并启动第一个线程

        new Thread(()->{ // 每一个线程都应该拥有一个事务
            TransactionStatus statusB = this.transactionManager
                    .getTransaction(new DefaultTransactionDefinition()); // 【B】开启一个数据库事务
            Book bookB = this.jdbcTemplate.queryForObject(querySQL, bookRowMapper, bid); // 【B】查询指定ID的数据
            LOGGER.info("【{}】第一次数据查询:{}", Thread.currentThread().getName(), bookB);
            try {
                TimeUnit.MILLISECONDS.sleep(200);
                int count = this.jdbcTemplate.update(updateSQL, "Netty开发实战", 66.66, bid); // 数据更新
                LOGGER.info("【{}】数据更新完成,影响更新的行数:{}", Thread.currentThread().getName(), count);
                this.transactionManager.commit(statusB); // 事务提交
            } catch (Exception e) {
                LOGGER.error("【{}】数据更新出错,错误信息:{}", Thread.currentThread().getName(), e.getMessage());
                this.transactionManager.rollback(statusB); // 事务回滚
            }
        }, "JDBC操作线程 - B").start(); // 定义并启动第二个线程
        TimeUnit.SECONDS.sleep(50); // 延迟处理
    }

2、
SHOW VARIABLES LIKE 'transaction_isolation';

3、
    @Test
    public void testTransactionIsolation() throws Exception { // 测试默认的隔离操作
        Integer bid = 7; // 要同时访问的ID编号
        String querySQL = "SELECT bid, title, author, price FROM book WHERE bid=?"; // 查询语句
        String updateSQL = "UPDATE book SET title=?, price=? WHERE bid=?"; // 更新语句
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); //
        RowMapper<Book> bookRowMapper = new BookRowMapper(); // 对象转换处理
        new Thread(()->{ // 每一个线程都应该拥有一个事务
            TransactionStatus statusA = this.transactionManager
                    .getTransaction(definition); // 【A】开启一个数据库事务
            Book bookA = this.jdbcTemplate.queryForObject(querySQL, bookRowMapper, bid); // 【A】查询指定ID的数据
            LOGGER.info("【{}】第一次数据查询:{}", Thread.currentThread().getName(), bookA);
            try { // 此时加入延迟的目的是为了让第二个线程可以进行数据的更新操作
                TimeUnit.SECONDS.sleep(5); // 强制性的延迟
                bookA = this.jdbcTemplate.queryForObject(querySQL, bookRowMapper, bid); // 【A】查询指定ID的数据
                LOGGER.info("【{}】第二次数据查询:{}", Thread.currentThread().getName(), bookA);
            } catch (InterruptedException e) {}
        }, "JDBC操作线程 - A").start(); // 定义并启动第一个线程

        new Thread(()->{ // 每一个线程都应该拥有一个事务
            TransactionStatus statusB = this.transactionManager
                    .getTransaction(definition); // 【B】开启一个数据库事务
            Book bookB = this.jdbcTemplate.queryForObject(querySQL, bookRowMapper, bid); // 【B】查询指定ID的数据
            LOGGER.info("【{}】第一次数据查询:{}", Thread.currentThread().getName(), bookB);
            try {
                TimeUnit.MILLISECONDS.sleep(200);
                int count = this.jdbcTemplate.update(updateSQL, "Netty开发实战", 66.66, bid); // 数据更新
                LOGGER.info("【{}】数据更新完成,影响更新的行数:{}", Thread.currentThread().getName(), count);
                this.transactionManager.commit(statusB); // 事务提交
            } catch (Exception e) {
                LOGGER.error("【{}】数据更新出错,错误信息:{}", Thread.currentThread().getName(), e.getMessage());
                this.transactionManager.rollback(statusB); // 事务回滚
            }
        }, "JDBC操作线程 - B").start(); // 定义并启动第二个线程
        TimeUnit.SECONDS.sleep(50); // 延迟处理
    }

事务传播属性

业务方法间的调用与事务处理

  • 按照业务开发的标准流程来讲,业务层需要通过数据层的实现业务逻辑的拼装,但是在一个完善的项目处理之中,也有可能会出现业务层与业务层之间的互相调用。此时不同的业务层可能都会存在有各自的事务处理支持,也有可能某些业务层不存在有事务处理在这些业务层之间整合的过程之中,为了方便的实现事务的控制,在 Spring 中提供了事务传播机制,即:父业务调用子业务时,可以通过事务的传播机制来讲当前的事务处理传递给子业务。

事务传播特点

  • TransactionDefinition.PROPAGATION_REQUIRED : 默认事务隔离级别 ,子业务直接支持当前父级事务,如果当前父业务之中没有事务,则创建一个新事务,如果父业务之中存在事务,则合并为一个完整业务;
  • TransactionDefinition.PROPAGATION_SUPPORTS : 如果当前父业务存在有事务,则加入该父级事务。如果当前不存在有父级事务,则以非事务方式送行
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED :以非事务的方式运行,如果当前存在有父级事务,则先自动挂起父级事务后运行
  • TransactionDefinition.PROPAGATION_MANDATORY :如果当前存在父级事务,则运行在父级事务之中,如果当前无事务则抛出异常(必须存在有父级事务)
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW :建立一个新的子业务事务,如果存在有父级业务则会自动将其挂起,该业务可以实现子业务事务独立提交,不受调用者的事务影响,即便父级事务异常,也可以正常提交;
  • TransactionDefinition.PROPAGATION_NEVER :以非事务的方式运行,如果当前存在有事务则抛出异常;
  • TransactionDefinition.PROPAGATION_NESTED : 如果当前存在父级事务,则当前子业务中的事务会自动成为该父级事务中的一个子事务,只有在父级事务提交后才会提交子事务。如果子事务产生异常则可以交由父级调用进行异常处理,如果父级产生回滚,则其也会回滚;
1、
interface IDeptService {
	public boolean edit();
}
interface IEmpService {
	public boolean edit(); // 需要调用IDeptService.edit()方法
}

2、
package com.yootk.test;

import com.yootk.jdbc.config.DataSourceConfig;
import com.yootk.jdbc.config.SpringJDBCConfig;
import com.yootk.jdbc.config.TransactionConfig;
import com.yootk.jdbc.vo.Book;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;

@ContextConfiguration(classes = {DataSourceConfig.class, SpringJDBCConfig.class, TransactionConfig.class})
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestTransactionPropagation {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestTransactionPropagation.class);
    @Autowired
    private JdbcTemplate jdbcTemplate; // 注入JDBC操作模版
    @Autowired
    private PlatformTransactionManager transactionManager; // 事务控制
    @Test // 业务功能的测试
    public void updateService() throws Exception { // 模拟更新处理业务
        String updateSQL = "UPDATE book SET title=?, price=? WHERE bid=?"; // 更新的SQL语句
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); // 定义事务控制
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 常见的传播机制
        TransactionStatus status = this.transactionManager.getTransaction(definition); // 事务开启
        try {
            this.insertService(); // 业务的嵌套调用
            this.jdbcTemplate.update(updateSQL, null, 67.8, 7); // 产生错误,作者的信息不允许为空
            this.transactionManager.commit(status); // 事务提交
        } catch (Exception e) {
            LOGGER.error("更新操作出现错误,{}", e.getMessage());
            this.transactionManager.rollback(status);
        }
    }
    @Test
    public void insertService() throws Exception { // 模拟增加处理业务
        String insertSQL = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)"; // 增加的SQL语句
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); // 定义事务控制
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); // 嵌套传播机制
        TransactionStatus status = this.transactionManager.getTransaction(definition); // 开启事务
        this.jdbcTemplate.update(insertSQL, "Redis开发实战", "李兴华", 67.6); // 更新数据表
        this.transactionManager.commit(status); // 事务提交
    }
}


3、
package com.yootk.test;

import com.yootk.jdbc.config.DataSourceConfig;
import com.yootk.jdbc.config.SpringJDBCConfig;
import com.yootk.jdbc.config.TransactionConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@ContextConfiguration(classes = {DataSourceConfig.class, SpringJDBCConfig.class, TransactionConfig.class})
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestTransactionPropagationC {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestTransactionPropagationC.class);
    @Autowired
    private JdbcTemplate jdbcTemplate; // 注入JDBC操作模版
    @Autowired
    private PlatformTransactionManager transactionManager; // 事务控制
    @Test // 业务功能的测试
    public void updateService() throws Exception { // 模拟更新处理业务
        String updateSQL = "UPDATE book SET title=?, price=? WHERE bid=?"; // 更新的SQL语句
        insertService(); // 此时没有父级事务
        this.jdbcTemplate.update(updateSQL, null, 67.8, 7); // 产生错误,作者的信息不允许为空
    }
    @Test
    public void insertService() throws Exception { // 模拟增加处理业务
        String insertSQL = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)"; // 增加的SQL语句
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); // 定义事务控制
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY); // 嵌套传播机制
        TransactionStatus status = this.transactionManager.getTransaction(definition); // 开启事务
        this.jdbcTemplate.update(insertSQL, "Redis开发实战", "李兴华", 67.6); // 更新数据表
        this.transactionManager.commit(status); // 事务提交
    }
}

只读事务控制

只读事务

  • 虽然业务开发都是围绕着数据层展开的,但并不是说所有的业务都需要进行数据库的处理尤其是在对外提供服务的业务方法更是要警惕数据更新所带来的问题,所以此时最为常见的做法就是将一些的处理定义为只读事务,即:只要执行更新操作则会自动抛出异常。
1、
    @Test
    public void testReadOnly() {
        String updateSQL = "UPDATE book SET title=? WHERE bid=?"; // 更新SQL
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setReadOnly(true);// 不允许进行更新操作
        TransactionStatus status = this.transactionManager.getTransaction(definition); // 获取事务控制
        this.jdbcTemplate.update(update, "Gradle开发实战", 7); // 更新数据
        this.transactionManager.commit(status);// 事务提交
    }

2、
    @Test
    public void testReadOnly2() {
        String selectSQL = "SELECT bid, title, price, author WHERE bid=?"; // 更新SQL
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setReadOnly(true);// 不允许进行更新操作
        TransactionStatus status = this.transactionManager.getTransaction(definition); // 获取事务控制
        Book book = this.jdbcTemplate.queryForObject(selectSQL, new BookRowMapper(), 7); // 数据查询
        LOGGER.info("【数据查询】{}", book);
        this.transactionManager.commit(status);// 事务提交
    }

@Transactional 注解

@Transactional 注解

  • 为了简化 Spring 事务定义繁琐度在 Spring 中提供了一个“@Transactional”注解,该注并且可以直接进行事务隔离级别与传播机制的配置解可以直接应用于业务方法之中,

1、
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    <context:annotation-config/>
    <context:component-scan base-package="com.yootk.jdbc"/>
</beans>

2、
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd">
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

3、
package com.yootk.jdbc.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service // 直接注册
public class BookService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Transactional(propagation = Propagation.REQUIRED) // 事务的配置
    public void remove() {
        String sql = "DELETE FROM book WHERE bid=?";
        this.jdbcTemplate.update(sql, 7); // 数据的删除
    }
}


4、
package com.yootk.jdbc.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service // 直接注册
public class BookService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Transactional(propagation = Propagation.REQUIRED) // 事务的配置
    public void remove() {
        String deleteSql = "DELETE FROM book WHERE bid=?";
        this.jdbcTemplate.update(deleteSql, 7); // 数据的删除
        String insertSql = "INSERT INTO book (title, author, price) VALUES (null, null, null)"; // 错误的数据
        this.jdbcTemplate.update(insertSql); // 数据增加
    }
}


5、
package com.yootk.test;

import com.yootk.jdbc.service.BookService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

// 按照道理来讲,此时应该定义一个总的配置类,而后在这个配置类上写上扫描包就可以自动注册了
@ContextConfiguration(locations = "classpath:spring/spring-*.xml")
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestBookService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBookService.class);
    @Autowired
    private BookService bookService;
    @Test
    public void testRemove() throws Exception { // 测试数据删除
        this.bookService.remove(); // 代码测试
    }
}

AOP 切面事务管理

AOP 声明式事务

  • 使用“@Transactiona”注解虽然可以简化事务代码的配置操作,但是随之而来的问题在于该注解定义频繁,因为一个完整的项目应用中会存在有大量的业务处理方法,而且大部分的业务处理中所采用的事务形式也都是类似的,所以此时最佳的做法是借助于 AOP 切面管理的方式实现事务控制。这样开发的代码不仅结构清,并且在进行业务代码开发与维护时也会更加的容易
1、
package com.yootk.jdbc.service;

import com.yootk.jdbc.vo.Book;

public interface IBookService {
    public boolean add(Book book);
    public boolean edit(Book book);

    public boolean editAll();
}



2、
package com.yootk.jdbc.service.impl;

import com.yootk.jdbc.service.IBookService;
import com.yootk.jdbc.vo.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service // 直接注册
public class BookServiceImpl implements IBookService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Override
    public boolean add(Book book) {
        String sql = "INSERT INTO book(title, author, price) VALUES (?, ?, ?)";
        return this.jdbcTemplate.update(sql, book.getTitle(), book.getAuthor(), book.getPrice()) > 0;
    }

    @Override
    public boolean edit(Book book) {
        String sql = "UPDATE book SET title=?, author=?, price=? WHERE bid=?";
        return this.jdbcTemplate.update(sql, book.getTitle(), book.getAuthor(),
                book.getPrice(), book.getBid()) > 0;
    }

    @Override
    public boolean editAll() {
        Book bookA = new Book();
        bookA.setTitle("Java进阶开发实战");
        bookA.setAuthor("沐言科技 - 可爱李");
        bookA.setPrice(45.6);
        bookA.setBid(8L);
        Book bookB = new Book();
        bookB.setTitle("Java程序设计开发实战");
        bookB.setAuthor(null); // 错误的数据
        bookB.setPrice(55.6);
        return this.edit(bookA) && this.add(bookB);
    }
}


3、
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/aop
            https://www.springframework.org/schema/aop/spring-aop.xsd">
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- 定义方法的名称匹配,以及该业务方法对应的事务处理方案 -->
            <tx:method name="add*" propagation="REQUIRED"/>
            <tx:method name="edit*" propagation="REQUIRED"/>
            <tx:method name="delete*" propagation="REQUIRED"/>
            <tx:method name="insert*" propagation="REQUIRED"/>
            <tx:method name="update*" propagation="REQUIRED"/>
            <tx:method name="remove*" propagation="REQUIRED"/>
            <tx:method name="change*" propagation="REQUIRED"/>
            <tx:method name="get*" propagation="REQUIRED" read-only="true"/>
        </tx:attributes>
    </tx:advice>
    <aop:config>
        <aop:pointcut id="transactionPointcut"
                      expression="execution(public * com.yootk..service..*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcut"/>
    </aop:config>
</beans>

4、
package com.yootk.jdbc.service;
import com.yootk.jdbc.vo.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PubService {
    @Autowired
    private IBookService bookService;
    public boolean editAll() {
        Book bookA = new Book();
        bookA.setTitle("Java进阶开发实战");
        bookA.setAuthor("沐言科技 - 可爱李");
        bookA.setPrice(45.6);
        bookA.setBid(8L);
        Book bookB = new Book();
        bookB.setTitle("Java程序设计开发实战");
        bookB.setAuthor(null); // 错误的数据
        bookB.setPrice(55.6);
        return bookService.edit(bookA) && bookService.add(bookB);
    }
}


5、
package com.yootk.test;
import com.yootk.jdbc.service.PubService;
import com.yootk.jdbc.service.impl.BookServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ContextConfiguration(locations = "classpath:spring/spring-*.xml")
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestBookServiceImpl {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBookServiceImpl.class);
    @Autowired
    private PubService pubService;
    @Test
    public void testEdit() throws Exception { // 测试数据删除
        this.pubService.editAll(); // 代码测试
    }
}

Bean 事务切面配置

AOP 事务定义

  • 基于 XML 配置文件实现的 AOP 切面事务管理,属于事务开发与实现的早期方式,如果开发者现在不希望使用配置文件的方式来进行事务配置,则也可以使用随后通过配置 AnnotationConfigApplicationcontext 的方式实现注解上下文的启动 Bean 的方式来进行事务定义,这样就需要将 tx 命名空间之中的自动配置
1、
package com.yootk.jdbc.config;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;

import java.util.HashMap;
import java.util.Map;

@Configuration
@Aspect // AOP代理配置
public class TransactionAdviceConfig { // 事务切面配置
    @Bean("txAdvice")
    public TransactionInterceptor transactionInterceptor(
            TransactionManager transactionManager) {
        RuleBasedTransactionAttribute readAttribute = new RuleBasedTransactionAttribute();
        readAttribute.setReadOnly(true); // 只读事务
        readAttribute.setPropagationBehavior(
                TransactionDefinition.PROPAGATION_NOT_SUPPORTED); // 非事务运行
        RuleBasedTransactionAttribute requiredAttribute = new RuleBasedTransactionAttribute();
        requiredAttribute.setPropagationBehavior(
                TransactionDefinition.PROPAGATION_REQUIRED); // 开启事务
        Map<String, TransactionAttribute> attributeMap = new HashMap<>(); // 保存方法映射
        attributeMap.put("add*", requiredAttribute); // 方法映射配置
        attributeMap.put("edit*", requiredAttribute); // 方法映射配置
        attributeMap.put("delete*", requiredAttribute); // 方法映射配置
        attributeMap.put("get*", readAttribute); // 方法映射配置
        NameMatchTransactionAttributeSource source =
                new NameMatchTransactionAttributeSource(); // 命名匹配事务
        source.setNameMap(attributeMap); // 方法名称的匹配操作的事务控制生效
        TransactionInterceptor interceptor =
                new TransactionInterceptor(transactionManager, source); // 事务拦截器配置
        return interceptor;
    }
    @Bean
    public Advisor transactionAdviceAdvisor(TransactionInterceptor interceptor) {
        String express = "execution (* com.yootk..service.*.*(..))"; // 切面表达式
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); // 切入表达式配置
        pointcut.setExpression(express); // 定义切面
        return new DefaultPointcutAdvisor(pointcut, interceptor);
    }
}


2、
package com.yootk.jdbc;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.yootk.jdbc") // 配置扫描包
public class StartSpringApplication {}


3、
package com.yootk.test;

import com.yootk.jdbc.StartSpringApplication;
import com.yootk.jdbc.service.PubService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ContextConfiguration(classes = StartSpringApplication.class) // 启动类
@ExtendWith(SpringExtension.class) // JUnit5测试
public class TestBookServiceImpl {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBookServiceImpl.class);
    @Autowired
    private PubService pubService;
    @Test
    public void testEdit() throws Exception { // 测试数据删除
        this.pubService.editAll(); // 代码测试
    }
}

demo


上次编辑于: