介绍SpringJDBC

说明:

(1) 本篇博客是介绍Spring JDBC的基本内容,不涉及具体的编码实现;

(2) JDBC是java程序访问数据库的标准。那么,Spring生态体系,自然也对JDBC进行了支持和封装。

(3) 开始了解【Spring JDBC与事务管理】之前,可以快速浏览下【[MySQL基础】和【JDBC入门】中的内容;


零:【Spring JDBC与事务管理】部分,内容介绍


一:Spring JDBC简介

(1) 原生的JDBC操作起来比较麻烦,比如必须使用繁琐的语句完成查询,必须要时刻留意是否关闭了数据库连接等;Spring公司,就在其Spring框架中,添加了【Spring JDBC】模块,来解决这个问题;

(2.1) 【Spring JDBC】的作用,有点像【Apache Commons DBUtils】,只是一个是由Spring机构开发的,一个是由Apache机构开发的;

(2.2) 由于Spring框架底层是基于IoC容器的,所以【Spring JDBC】基于IoC容器,还可以实现更多的扩展行为;这是【Apache Commons DBUtils】所不具备的;

(3) JdbcTemplate对象,提供了数据增删改查的API方法;

(4) 学习【Spring JDBC】,主要就是学习【如何配置Spring JDBC】和【JdbcTemplate对象中的方法】;


二:已经有【Mybatis】了,为什么还需要【Spring JDBC】?

(0) ORM(object/relation mapping 对象关系映射)目前数据库是关系型数据库 ORM 主要是把数据库中的关系数据映射称为程序中的对象。

(1) 这两者面向的对象不一样;【Mybatis】作为一个ORM框架,其封装程度较高,适合于中小企业进行软件的敏捷开发,让程序员可以快速完成数据库交互工作;但是,【Mybatis】需要进行一系列的配置,也有很多操作细节需要注意,即【Mybatis】的封装程度还是比较高的;封装程度高,就意味着执行效率相对较低;

(2) 对于大的公司,无论是数据量还是并发量都是很高的,此时如果使用【Mybatis】,可能由于微小的性能上的差距,就会导致整体应用变慢;所以,对于很多大公司来说,其很少使用市面上成熟的框架,更多时候是使用像【Spring JDBC】这样的轻量级框架,在这个基础上结合自己企业的特点进行二次封装;

(4) 【Spring JDBC】只是对原始JDBC进行简单封装;和【Mybatis】相比,【Spring JDBC】的执行效率更高;

(5) 同时,由于Spring中【Spring IoC】容器的存在,和使用【原生JDBC】相比,使用【Spring JDBC】程序也变得更容易管理;

(6) 【Spring JDBC】是介于【ORM框架,比如Mybatis和Hibernate】和【原生JDBC】之间的一个折中方案


三:Spring JDBC使用步骤

(1) 【 DataSource数据源】以前介绍过,其作用是指明【到底要连接哪种数据库,哪台服务器,数据库用户名和密码是什么】;

(2) 配置好之后,需要在Dao中初始化【JdbcTemplate对象】,并通过【JdbcTemplate对象】实现增删改查操作;

SpringJDBC配置过程

说明: (1) Spring JDBC配置过程主要为:

● 在pom.xml中引入【Spring Context模块】,【Spring JDBC模块】,【Mysql的JDBC驱动依赖】;

●在applicationContext.xml中配置dataSource数据源对象,jdbcTemplate对象;(这样一来,IoC容器初始化时,就能实例化JdbcTemplate)

●在需要增删改查的Dao类中,去持有JdbcTemplate属性,并为其设置get和set方法(之所以设置get和set方法,是因为后面我们在依赖注入的时候是采用【使用XML方式实现Spring】中的策略来做的);然后,在Dao类的具体增删改查业务代码中,调用JdbcTemplate相应的方法,完成相应的增删改查操作;

● 在ApplicationContext.xml中配置Dao类对象,并完成JdbcTemplate属性注入的工作;

● 在初始化IoC容器后,在具体的业务代码中,从IoC容器中获取Dao类对象,调用对应的业务方法就能完成相应的增删改查操作了;


1.准备数据库数据

(1)创建一个逻辑空间:springjdbctest;

(2)导入数据

2.创建一maven工程

3.pom文件中引入依赖: 【Spring Context模块】,【Spring JDBC模块】,【Mysql的JDBC驱动】;

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
 <project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
     <groupId>com.imooc.spring</groupId>
     <artifactId>jdbc</artifactId>
     <version>1.0-SNAPSHOT</version>
 
     <repositories>
         <repository>
             <id>aliyun</id>
             <name>aliyun</name>
             <url>https://maven.aliyun.com/repository/public</url>
         </repository>
     </repositories>
     <dependencies>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
             <version>5.3.9</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-jdbc</artifactId>
             <version>5.3.9</version>
         </dependency>
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>8.0.23</version>
         </dependency>
     </dependencies>
 
 </project>

说明:

(1)主要引入了三个依赖: 【Spring Context模块】,【Spring JDBC模块】,【JDBC】;

(2)【JDBC】依赖的几点说明:

● java程序想操作数据库,就需要引入JDBC驱动,我们在前面没有学习框架和maven的时候,如果需要操作数据库的话,都在工程中引入了jdbc的依赖;

● 自然,【Mybatis】,【Spring JDBC】这些操作数据库的框架都是基于jdbc的封装;所以,这儿也需要在pom中引入jdbc依赖;

● 【jdbc驱动的依赖】的版本最好和【数据库版本】保持一致;

4.创建applicationContext.xml配置文件:配置dataSource对象;jdbcTemplate对象;

applicationContext.xml:

<?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
         https://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         https://www.springframework.org/schema/context/spring-
context.xsd">
 
     <bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
         <property name="driverClassName"
value="com.mysql.cj.jdbc.Driver"/>
         <property name="url"
 
value="jdbc:mysql://localhost:3306/springjdbctest?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
         <property name="username" value="root"/>
         <property name="password" value="12345"/>
     </bean>
     <bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
         <property name="dataSource" ref="dataSource"/>
     </bean>
 </beans>

说明:

(1) 这儿只是引入了【默认命名空间】、【cointext命名空间】和相关的schema约束;

(2) 在IoC容器中,配置【DataSource对象】:设置数据源;

(3) 在IoC容器中,配置【JdbcTemplate对象】;

5.创建与数据库表对应的实体类:Employee类,作为承载存储数据库查询结果的实体类;

Employee类:

package com.imooc.spring.jdbc.entity;
 
 import java.util.Date;
 
 public class Employee {
     private Integer eno;
     private String ename;
     private Float salary;
     private String dname;
     private Date hiredate;
 
     public Integer getEno() {
         return eno;
     }
 
     public void setEno(Integer eno) {
         this.eno = eno;
     }
 
     public String getEname() {
         return ename;
     }
 
     public void setEname(String ename) {
         this.ename = ename;
     }
 
     public Float getSalary() {
         return salary;
     }
 
     public void setSalary(Float salary) {
         this.salary = salary;
     }
 
     public String getDname() {
         return dname;
     }
 
     public void setDname(String dname) {
         this.dname = dname;
     }
 
     public Date getHiredate() {
         return hiredate;
     }
 
     public void setHiredate(Date hiredate) {
         this.hiredate = hiredate;
     }
 
     @Override
     public String toString() {
         return "Employee{" +
                 "eno=" + eno +
                 ", ename='" + ename + '\'' +
                 ", salary=" + salary +
                 ", dname='" + dname + '\'' +
                 ", hiredate=" + hiredate +
                 '}';
     }
 }

说明:

(1) 实体类的字段名需要采用驼峰命名方式,与Employee表的字段名保持一致;

(2) Employee类的hiredate类型是java.util包下的Date;以前介绍过,【java.sql下的Date】是【java.util下的Date】的子类;具体可以参考【JDBC中的Date日期的处理方式】;

(3) 添加了set和get方法;重写了toString()方法;

6.创建EmployeeDao类:这个类需要持有JdbcTemplate对象属性;编写业务方法;在IoC容器中配置EmployeeDao对象,并在EmployeeDao对象中注入JdbcTemplate对象;

EmployeeDao类:

package com.imooc.spring.jdbc.dao;
 
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 public class EmployeeDao {
     private JdbcTemplate jdbcTemplate;
 
 
     public Employee findById(Integer eno) {
         String sql = "select * from employee where eno = ?";
         Employee employee = jdbcTemplate.queryForObject(sql, new Object[]{eno}, new BeanPropertyRowMapper<Employee(Employee.class));
return employee;
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
     }
 }

说明:

(1) 因为我们这儿使用【Spring JDBC】完成数据库的增删改查,所以EmployeeDao在数据操作是需要依赖【JdbcTemplate】对象;所以,EmployeeDao中需要持有JdbcTemplate;同时,需要给JdbcTemplate属性添加get和set方法;

(2) 在EmployeeDao类中,增加了一个查询方法:findById()方法,中间调用了JdbcTemplate对象的queryForObject()方法,完成相应的增删改查操作;

● 只要我们在写Employee实体类时,实体类的属性名严格按照驼峰命名规则,与数据表的字段名保持一致了;那么queryForObject()方法的第三个参数就可以使用【new BeanPropertyRowMapper<Employee(Employee.class)】来完成【从数据库记录到实体类对象的转换】;

● 因为,queryForObject()方法,是查询只有一个返回值的SQL语句,即其最多只会返回一条记录;所以,这儿使用Employee对象去接受方法返回结果就行了;

(3) 又因为我们这个项目是基于Spring的,即需要使用IoC管理对象;所以,我们接下来需要在IoC容器中配置EmployeeDao对象,并在EmployeeDao对象中注入JdbcTemplate对象;

7.创建SpringApplication类:测试;

SpringApplication类:

package com.imooc.spring.jdbc;
 
 import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringApplication {
     public static void main(String[] args) {
         ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
EmployeeDao employeeDao = (EmployeeDao)
context.getBean("employeeDao", EmployeeDao.class);
Employee employee = employeeDao.findById(3308);
System.out.println(employee);
}
}

说明:

(1) 代码分析:再啰嗦一下~~

(2) 运行结果:

JDBCTemplate的数据查询方法

说明:

(1) 在【Spring JDBC与事务管理:Spring JDBC配置过程】;本篇博客主要内容是Spring JDBC的核心类→JdbcTemplate类的的查询方法; (2) 本篇博客内容说明:

● 如果查询结果只有一条记录:使用queryForObject()方法;

● 如果查询结果有多条记录:使用query()方法;

● 如果查询结果无法用一个实体类对象去承载:使用queryForList()方法;(这个在实际开发中还是比较常用的)


0.准备:在pom.xml中引入【junit单元测试】,【spring test测试模块】;

说明:

(1) 这儿引入【junit单元测试】,【spring test测试模块】的目的是,可以更灵活的来及时测试JdbcTemplate各个方法的效果;

(2)【junit单元测试】可以快速参考【单元测试与Junit4】;【spring test测试模块】可以快速参考【Spring与JUnit4整合】;

1.JdbcTemplate的【queryForObject()】:查询单条数据;

JdbcTemplateTestor:

import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
 import javax.annotation.Resource;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(locations = {"classpath:applicationContext.xml"})
 public class JdbcTemplateTestor {
     @Resource
     private EmployeeDao employeeDao;
 
     @Test
     public void testFindById() {
         Employee employee = employeeDao.findById(3308);
         System.out.println(employee);
     }
 
 }

说明:

(1) @RunWith()和@ContextConfiguration()注解,作用是整合【spring test】和【junit】;可以参考【Spring Test测试模块】;

(2) 把IoC容器中的EmployeeDao对象,注入到当前测试类对象中;

(3) 当查询结果只有一条记录时,使用JdbcTemplate的queryForObject()方法;

(4) 运行结果;

2.JdbcTemplate的【query()】:查询多条(复合)数据;

首先,在EmployeeDao类中,增加一个findByDname()方法,该方法会调用JdbcTemplate的query()方法;

package com.imooc.spring.jdbc.dao;
 
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 import java.util.List;
 
 public class EmployeeDao {
     private JdbcTemplate jdbcTemplate;
 
     public List<Employee findByDname(String dname) {
         String sql = "select * from employee where dname = ?";
         List<Employee list = jdbcTemplate.query(sql,
                 new Object[]{dname},
                 new BeanPropertyRowMapper<Employee(Employee.class));
         return list;
     }
     public JdbcTemplate getJdbcTemplate() {
         return jdbcTemplate;
     }
 
     public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
     }
 }

说明:

(1) 方法说明:查询结果有多条记录,使用JdbcTemplate的query()方法;


然后,在JdbcTemplateTestor类中创建testFindByDname()方法去测试;

import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
 import javax.annotation.Resource;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(locations = {"classpath:applicationContext.xml"})
 public class JdbcTemplateTestor {
     @Resource
     private EmployeeDao employeeDao;
 
     @Test
     public void testFindByDname() {
         System.out.println(employeeDao.findByDname("研发部"));
     }
 
 }

说明:

(1) 运行结果;

**3.JdbcTemplate的【queryForList()】

当没有的实体类对象能够去承载查询结果时,使用 List<Map(String,Object)去承载查询结果

首先,在EmployeeDao类中,增加一个findMapByDname()方法,该方法会调用JdbcTemplate的queryForList()方法;

package com.imooc.spring.jdbc.dao;
 
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 import java.util.List;
 import java.util.Map;
 
 public class EmployeeDao {
     private JdbcTemplate jdbcTemplate;
 
 
     public List<Map<String, Object  findMapByDname(String dname) {
         String sql = "select eno as empno,salary as s from employee  where dname = ?";
List<Map<String, Object maps = jdbcTemplate.queryForList(sql,
new Object[]{dname});
return maps;
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
     }
 }

说明:

(1)情况说明: 没有【实体类对象】能够去承载【查询结果】;(即,无法完成实体类和数据表的映射)

(2) 没有【实体类对象】能够去承载【查询结果】时,使用queryForList()方法,使用List<Map()去承载查询结果;


然后,在JdbcTemplateTestor类中创建testFindMapByDname()方法去测试;

import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
 import javax.annotation.Resource;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(locations = {"classpath:applicationContext.xml"})
 public class JdbcTemplateTestor {
     @Resource
     private EmployeeDao employeeDao;
 
     @Test
     public void testFindMapByDname() {
         System.out.println(employeeDao.findMapByDname("研发部"));
     }
 
 }

说明:

(1) 运行结果;

(2)分析:通过queryForList()方法,即使没有合适的实体类对象可以去承载查询结果,也可以通过一个List <Map(String,Object)去承载查询结果;这在实际开发中,是比较常见的。

JDBCTemplate数据写入操作

说明:

(1) 本篇博客主要内容是介绍JdbcTemplate的update()方法;在JdbcTemplate中,新增、修改、删除统称为写入操作,都使用update()方法;


1. update()方法:新增;

首先,在EmployeeDao中增加insert()方法;

package com.imooc.spring.jdbc.dao;
 
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 import java.util.List;
 import java.util.Map;
 
 public class EmployeeDao {
     private JdbcTemplate jdbcTemplate;
 
     public void insert(Employee employee) {
         String sql = "insert into employee(eno,ename,salary,dname,hiredate) values(?,?,?,?,?)";
jdbcTemplate.update(sql, new Object[]{employee.getEno(),
employee.getEname(), employee.getSalary(), employee.getDname(),
employee.getHiredate()});
}
public JdbcTemplate getJdbcTemplate() {
         return jdbcTemplate;
     }
 
     public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
     }
 }

说明:

(1) 新增的时候,update()方法只有两个参数,第一个是SQL语句,第二个就是SQL语句中的参数;


然后,在JdbcTemplateTestor中,创建测试方法testInsert();

import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
 import javax.annotation.Resource;
 import java.util.Date;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(locations = {"classpath:applicationContext.xml"})
 public class JdbcTemplateTestor {
     @Resource
     private EmployeeDao employeeDao;
 
     @Test
     public void testInsert() {
         Employee employee = new Employee();
         employee.setEno(4100);
         employee.setEname("小刘");
         employee.setSalary(7895f);
         employee.setDname("研发部");
         employee.setHiredate(new Date());
         employeeDao.insert(employee);
     }
 
 }

说明:

(1) 运行结果;

(2) 没什么好说的,很简单~~

2. update()方法:更新;

首先,在EmployeeDao中增加update()方法;

package com.imooc.spring.jdbc.dao;
 
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 import java.util.List;
 import java.util.Map;
 
 public class EmployeeDao {
     private JdbcTemplate jdbcTemplate;
 
 
     public int update(Employee employee) {
         String sql = "update employee set ename = ?,salary = ?,dname =?,hiredate = ? where eno = ?";
int count = jdbcTemplate.update(sql, new
Object[]{employee.getEname(), employee.getSalary(), employee.getDname(),
employee.getHiredate(), employee.getEno()});
return count;
}
 
public JdbcTemplate getJdbcTemplate() {
         return jdbcTemplate;
     }
 
     public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
     }
 }

说明:

(1) 更新的时候,update()方法只有两个参数,第一个是SQL语句,第二个就是SQL语句中的参数;

(2) 更新的时候,update()方法会返回一个int类型的返回值,代表本次更新的记录数;


然后,在JdbcTemplateTestor中,创建测试方法testUpdate();

import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
 import javax.annotation.Resource;
 import java.util.Date;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(locations = {"classpath:applicationContext.xml"})
 public class JdbcTemplateTestor {
     @Resource
     private EmployeeDao employeeDao;
 
     @Test
     public void testUpdate() {
         Employee employee = employeeDao.findById(4100);
         employee.setSalary(employee.getSalary() + 1000);
         int count = employeeDao.update(employee);
         System.out.println(count);
     }
 
 }

说明:

(1) 代码分析

(2) 运行结果;

3. update()方法:删除;

首先,在EmployeeDao中创建delete()方法;

package com.imooc.spring.jdbc.dao;
 
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 import java.util.List;
 import java.util.Map;
 
 public class EmployeeDao {
     private JdbcTemplate jdbcTemplate;
 
     public int delete(Integer eno) {
         String sql = "delete from employee where eno = ?";
         int count = jdbcTemplate.update(sql, new Object[]{eno});
         return count;
     }
 
     public JdbcTemplate getJdbcTemplate() {
         return jdbcTemplate;
     }
 
     public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
     }
 }

说明:

(1) 新增的时候,update()方法只有两个参数,第一个是SQL语句,第二个就是SQL语句中的参数;

(2) update()方法删除时候,返回的是成功删除的记录数;


然后,在JdbcTemplateTestor类中,编写测试方法testDelete();

import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
 import javax.annotation.Resource;
 import java.util.Date;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(locations = {"classpath:applicationContext.xml"})
 public class JdbcTemplateTestor {
     @Resource
     private EmployeeDao employeeDao;
 
     @Test
     public void testDelete() {
         int count = employeeDao.delete(4100);
         System.out.println(count);
     }
 
 }

说明:

(1) 运行结果;

(2) 第一次执行delete()方法后,eno=4100就已经被删除了;如果再次执行delete()方法:

一旦涉及到写操作(新增、更新、删除), 就必须要考虑到实用性,即必须要考虑事务控制;

Spring编程式事务

说明:

(1) 有关事务内容可以快速参考【事务的基本介绍】及附近文章;【JDBC实现事务】;

(2) 【Spring JDBC模块】中也有事务控制;其中就包括编程式事务和声明式事务;本篇博客介绍编程式事务;


一:编程式事务简介

(1) 【Spring JDBC模块】通过引入TransactionManager对象(事务管理器),来控制事务;

(2) TransactionManager对象提供了两个核心方法:commit()和rollback()方法;(PS:在【[JDBC实现事务】中Connection对象也有自己的commit()和rollback()方法来控制事务;都是相通的)

二:代码演示:没有使用【编程式事务控制】的情况

需求: 公司入职10名新员工,需要把这10名新员工批量导入Employee表中;要求是,要么一次性全部导入,要么一个也不导入;

首先,创建EmployeeService类:

EmployeeService:

package com.imooc.spring.jdbc.service;
 
 import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 
 import java.util.Date;
 
 public class EmployeeService {
     private EmployeeDao employeeDao;
     public void batchImport() {
         for (int i = 1; i <= 10; i++) {
             Employee employee = new Employee();
             employee.setEno(8000 + i);
             employee.setEname("员工"+i);
             employee.setSalary(4000f);
             employee.setDname("研发部");
             employee.setHiredate(new Date());
             employeeDao.insert(employee);
         }
     }
 
     public EmployeeDao getEmployeeDao() {
         return employeeDao;
     }
 
     public void setEmployeeDao(EmployeeDao employeeDao) {
         this.employeeDao = employeeDao;
     }
 }

然后,在applicationContext.xml中配置EmployeeService对象,并注入EmployeeDao:

然后,在JdbcTemplateTestor测试类中编写测试方法:


三:问题分析

虽然,上面写完了,而且导入成功了;但是上面写入操作并没有进行事务控制;

我们要求是,这10名员工,要么一次性全部导入,要么一个也不导入;而上面10次新增操作并不是在一个事务中完成的;可以打印一下该过程的日志来观察一下;

引入logback日志依赖:

说明:

(1) 在【MyBatis日志管理】中介绍过logback这个日志组件;

(2) 当添加logback这个日志依赖后,Spring默认就集成了;即Spring发现当前类路径下引入了logback后,Spring框架就可以默认使用logback将日志打印输出;

通过日志信息,分析【未使用控制事务】时的执行过程:

为了重新测试,将刚才添加的十条数据删除:

然后,重新运行JdbcTemplateTestor测试类中的testBatchImport()方法:观察日志信息

即,可知,在插入10条数据的过程中,一旦在过程中程序出现了中断,肯定会出现【一部分数据插入成功,而另一部分数据没有插入的情况】,而这是和我们的要求相违背的;


四:使用【编程式事务】去控制(核心!)

首先,在applicationContext.xml中配置transactionManager事务管理器对象:

然后,在需要事务控制的类(EmployeeService类)中,添加并注入transactionManager:

这儿,是在EmployeeService类中需要事务控制;

设置好了之后,如何使用?

然后,就是【编程式事务】的实际使用了:

EmployeeService完整代码如下:

package com.imooc.spring.jdbc.service;
 
     import com.imooc.spring.jdbc.dao.EmployeeDao;
     import com.imooc.spring.jdbc.entity.Employee;
     import org.springframework.jdbc.datasource.DataSourceTransactionManager;
     import org.springframework.transaction.TransactionDefinition;
     import org.springframework.transaction.TransactionStatus;
     import
 org.springframework.transaction.support.DefaultTransactionDefinition;
 
     import java.util.Date;
 
     public class EmployeeService {
         private EmployeeDao employeeDao;
         private DataSourceTransactionManager transactionManager;
         public void batchImport() {
             //定义了事务默认的标准配置
             TransactionDefinition definition = new
 DefaultTransactionDefinition();
             //开始一个事务,返回值是事务状态,事务状态说明当前事务的执行阶段
             TransactionStatus transactionStatus =
 transactionManager.getTransaction(definition);
             try {
                 for (int i = 1; i <= 10; i++) {
                     Employee employee = new Employee();
                     employee.setEno(8000 + i);
                     employee.setEname("员工" + i);
                     employee.setSalary(4000f);
                     employee.setDname("研发部");
                     employee.setHiredate(new Date());
                     employeeDao.insert(employee);
                 }
                 //如果一切正常,就提交事务
                 transactionManager.commit(transactionStatus);
             } catch (RuntimeException e) {
                 //如果运行出错,回滚事务
                 transactionManager.rollback(transactionStatus);
 
 e.printStackTrace();//可以将异常进行打印,这也意味着异常不会向上抛出,在EmployeeService内部就被消化了;
     //            throw e;//如果,想让异常在外侧(即调用方)处理的话,可以把异常抛出去;
             }
         }
 
         public EmployeeDao getEmployeeDao() {
             return employeeDao;
         }
 
         public void setEmployeeDao(EmployeeDao employeeDao) {
             this.employeeDao = employeeDao;
         }
 
         public DataSourceTransactionManager getTransactionManager() {
             return transactionManager;
         }
 
         public void setTransactionManager(DataSourceTransactionManager
 transactionManager) {
             this.transactionManager = transactionManager;
         }
     }

说明:

(1) 代码分析

测试1:【执行过程中出现异常】的案例:

为了重新测试,将刚才添加的十条数据删除:

日志分析:

可以看到,当使用了编程式事务控制以后,所有的数据操作,都是在一个数据库连接中完成的;然后,已经成功写入的数据,不是直接写到数据库的表中,而是先被放入到了事务区中;

测试2:【执行过程中没有出现异常】的案例:

日志分析:


五:【编程式事务】有缺点,引出【声明式事务】

编程式事务:有优点,有缺点:

优点: 编程式事务的控制,是直接写在代码中的, 人眼阅读比较友好;

缺点: 编程式事务需要我们在代码中去书写事务控制的代码,因为不同的人对Spring掌握的程度不同,或者有的人可能在写代码的时候忘记写如下的代码:

这就可能导致数据的不完整的情况,从而给软件埋下巨大的风险。在一些金融、军事、科学运算等对数据要求很高的系统中,出现不完整的数据是非常严重的错误。所以,编程式事务控制可能存在人为的风险;为此,将介绍【Spring JDBC】提供的一种更加高级的做法:声明式事务。

声明式事务

说明:

(1) 已知,编程式事务虽然易于理解,但是由于程序员水平问题或疏忽问题,可能会出现忘记编写事务控制代码的情况;为此,声明式事务应运而生;

(2) 这一点要明确:声明式事务不是新技术,而是【Spring AOP面向切面编程】一个典型的应用场景而已;

(3) 【声明式事务】把事务控制的工作,转移到了applicationContext.xml配置文件中;这样以后,在编写业务代码的时候,程序员就不需要再设置事务了;这种策略,既安全又高效;


一:声明式事务简介

(1) 声明式事务不是新技术;而是,【Spring AOP面向切面编程】一个典型的应用场景而已;

(2) 利用【Spring AOP】中的环绕通知,可以轻松解决这个需求。即,【声明式事务】就是通过【Spring AOP的环绕通知】,在不修改源代码的情况下,完成程序的扩展,实现事务的控制;

(3) 关于【Spring AOP 环绕通知】可以快速参考下【AOP通知之:Around Advice 环绕通知】 的内容;


二:声明式事务配置过程

因为,【声明式事务】是通过【Spring AOP的环绕通知】来实现的;所以,【声明式事务】的整个配置过程,都是在applicationContext.xml配置文件中完成的,不需要修改源代码;

(1) 首先,需要在applicationContext.xml中配置TransactionManager对象。

前面已经知道,TransactionManager对象的作用是提交或回滚事务;【编程式事务】和【声明式事务】都需要TransactionManager事务管理器对象;

(2) 然后,给不同的方法,配置事务通知和事务属性;

在实际开发中,有的方法(比如新增、修改、删除方法)需要事务控制,有的方法(比如查询方法)不需要事务控制。需要根据不同的情况,进行不同的配置;

(3) 最后,为事务绑定【划定范围的切点】;

这儿需要利用【AOP】的切点表达式来划定一个范围;只有在这个范围内的,且满足(2)中的条件的方法,才会开启事务控制;


三:初始代码:创建项目s02;(这儿不是本篇博客的核心,快速浏览就行。)

s02的内容和前面几篇博客的代码基本一致,只是去除了编程式事务的有关代码而已;

(1)pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
 <project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
     <groupId>com.imooc.spring</groupId>
     <artifactId>jdbc</artifactId>
     <version>1.0-SNAPSHOT</version>
     <dependencies>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
             <version>5.2.6.RELEASE</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-jdbc</artifactId>
             <version>5.2.6.RELEASE</version>
         </dependency>
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>8.0.16</version>
         </dependency>
 
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.12</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-test</artifactId>
             <version>5.2.6.RELEASE</version>
         </dependency>
 
         <!--logback日志组件,Spring框架默认集成-->
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</artifactId>
             <version>1.2.3</version>
         </dependency>
 
     </dependencies>
 
 </project>

说明:

(1) 没什么好说的,目前在pom.xml中,引入了【Spring Context模块】,【Spring JDBC模块】,【MySQL JDBC驱动】,【junit测试依赖】,【Spring Test模块】,【logback日志依赖】这几个;

(2)Employee实体类:

package com.imooc.spring.jdbc.entity;
 
 import java.util.Date;
 
 public class Employee {
     private Integer eno;
     private String ename;
     private Float salary;
     private String dname;
     private Date hiredate;
 
     public Integer getEno() {
         return eno;
     }
 
     public void setEno(Integer eno) {
         this.eno = eno;
     }
 
     public String getEname() {
         return ename;
     }
 
     public void setEname(String ename) {
         this.ename = ename;
     }
 
     public Float getSalary() {
         return salary;
     }
 
     public void setSalary(Float salary) {
         this.salary = salary;
     }
 
     public String getDname() {
         return dname;
     }
 
     public void setDname(String dname) {
         this.dname = dname;
     }
 
     public Date getHiredate() {
         return hiredate;
     }
 
     public void setHiredate(Date hiredate) {
         this.hiredate = hiredate;
     }
 
     @Override
     public String toString() {
         return "Employee{" +
                 "eno=" + eno +
                 ", ename='" + ename + '\'' +
                 ", salary=" + salary +
                 ", dname='" + dname + '\'' +
                 ", hiredate=" + hiredate +
                 '}';
     }
 }

说明:

(0) Employee实体类很简单,没什么好说的;而且,在前面几篇博客中已经介绍过了;

(1) 因为Employee实体类的作用,就是用来承载和存储数据库employee表的查询内容的;所以Employee类的属性,最好严格按照【驼峰命名规则】比照着employee表的字段名去写;

(2) 然后,按常规写get和set方法;然后,为了方便观察Employee对象的内容,又重写了toString()方法;

(3)applicationContext.xml配置文件:

<?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
         https://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         https://www.springframework.org/schema/context/spring-
context.xsd">
     <!-- 数据源 -->
     <bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
         <property name="driverClassName"
value="com.mysql.cj.jdbc.Driver"/>
         <property name="url"
 
value="jdbc:mysql://localhost:3306/springjdbctest?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
         <property name="username" value="root"/>
         <property name="password" value="12345"/>
     </bean>
     <!--JdbcTemplate提供数据CRUD的API-->
     <bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
         <property name="dataSource" ref="dataSource"/>
     </bean>
 
     <bean id="employeeDao"
class="com.imooc.spring.jdbc.dao.EmployeeDao">
         <!--为Dao注入JdbcTemplate对象-->
         <property name="jdbcTemplate" ref="jdbcTemplate"/>
     </bean>
 
     <bean id="employeeService"
class="com.imooc.spring.jdbc.service.EmployeeService">
         <property name="employeeDao" ref="employeeDao"/>
     </bean>
 
 </beans>

说明:

(1) 目前只是引入了【默认命名空间】,【context命名空间】;

(2) 这个applicationContext.xml很简单,没什么好说的,就是基本的【Spring JDBC】配置;

(4)EmployeeDao类:

package com.imooc.spring.jdbc.dao;
 
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 import java.util.List;
 import java.util.Map;
 
 public class EmployeeDao {
     private JdbcTemplate jdbcTemplate;
 
     public Employee findById(Integer eno){
         String sql = "select * from employee where eno = ?";
         //查询单条数据
         Employee employee = jdbcTemplate.queryForObject(sql, new
Object[]{eno}, new BeanPropertyRowMapper<Employee>(Employee.class));
         return employee;
     }
 
     public List<Employee> findByDname(String dname){
         String sql = "select * from employee where dname = ?";
         //查询复合数据
         List<Employee> list = jdbcTemplate.query(sql, new
Object[]{dname}, new BeanPropertyRowMapper<Employee>(Employee.class));
         return list;
     }
 
     public List<Map<String, Object>> findMapByDname(String dname){
         String sql = "select eno as empno , salary as s from employee
where dname = ?";
         //将查询结果作为Map进行封装
         List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql,
new Object[]{dname});
         return maps;
     }
 
     public void insert(Employee employee){
         String sql = "insert into
employee(eno,ename,salary,dname,hiredate) values(?,?,?,?,?)";
         //利用update方法实现数据写入操作
         jdbcTemplate.update(sql,new Object[]{
            employee.getEno() ,
employee.getEname(),employee.getSalary(),employee.getDname() ,
employee.getHiredate()
         });
     }
 
     public int update(Employee employee){
         String sql = "UPDATE employee SET ename = ?, salary = ?, dname =
?, hiredate = ? WHERE eno = ?";
         int count = jdbcTemplate.update(sql, new
Object[]{employee.getEname(), employee.getSalary(), employee.getDname(),
employee.getHiredate(), employee.getEno()});
         return count;
     }
 
     public int delete(Integer eno){
         String sql = "delete from employee where eno = ?";
         return jdbcTemplate.update(sql, new Object[]{eno});
     }
 
 
     public JdbcTemplate getJdbcTemplate() {
         return jdbcTemplate;
     }
 
     public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
     }
 }

说明:

(1) 因为EmployeeDao这个Dao类需要直接操作数据库,所以增加了JdbcTemplate属性;自然再applicationContext.xml中也在EmployeeDao对象中注入了JdbcTemplate对象;

(2) EmployeeDao中都是定义了一些查询、新增、删除、修改方法;而且,在前面几篇博客中已经介绍过了;

(5)EmployeeService类:

package com.imooc.spring.jdbc.service;
 
 import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import java.util.Date;
 
 public class EmployeeService {
     private EmployeeDao employeeDao;
 
     public void batchImport() {
         for (int i = 1; i <= 10; i++) {
             Employee employee = new Employee();
             employee.setEno(8000 + i);
             employee.setEname("员工" + i);
             employee.setSalary(4000f);
             employee.setDname("市场部");
             employee.setHiredate(new Date());
             employeeDao.insert(employee);
         }
     }
 
     public EmployeeDao getEmployeeDao() {
         return employeeDao;
     }
 
     public void setEmployeeDao(EmployeeDao employeeDao) {
         this.employeeDao = employeeDao;
     }
 
 }

说明:

(1) 啰嗦一下吧:因为Service类需要调用Dao类中的方法;所以EmployeeService类中增加了EmployeeDao属性,并且在IoC容器中完成了对象注入;

(2) 在EmployeeService中也增加了batchImport()这个业务方法; 然后,我们没有使用【编程式事务控制中的,那些事务控制代码;去控制事务】;


四:需求阐述

已知: EmployeeService中的batchImport()业务方法,是批量更新的操作;在实际开发中,这显然是需要进行事务控制的。

要求:在不修改原始代码的情况下,对系统重要的Service类,配置【声明式事务】;从而,让程序员在编写代码的过程中,不用再考虑事务的问题;(因为,我们已经通过【声明式事务】完成了事务控制的工作;所以,程序员在编写业务代码的时候,就不用再考虑编写事务控制的代码;)


五:【声明式事务】:配置过程;(核心!)

1.在pom.xml中引入aspectj;

2.在applicationContext.xml中配置TransactionManager事务管理器对象;(第一步)

说明:

(1) 【编程式事务】和【声明式事务】都需要TransactionManager事务管理器对象;

(2) 在【Spring编程式事务】中已经介绍过配置TransactionManager事务管理器对象了;

3.在applicationContext.xml中:引入【tx】命名空间和约束;【aop】命名空间和约束;

说明:

(1) 这些命名空间的内容其实很简单,不需要死记硬背;而且,在Spring官网也都有的;

4.在applicationContext.xml中:事务通知配置:决定哪些方法使用事务,哪些方法不使用事务;(核心!)(第二步;第三步)

(1)第二步是: 通过<tx:advice>,配置哪些开启事务,以及事务的属性是什么;

(2)第三步是: 通过<aop:config>,来划定一个范围;只有在这个范围内的,且满足第二步条件的方法,才会开启事务;

5.实际测试;

(1)首先,有异常的情况:


(2)然后,没有异常的情况:


六:【声明式事务】总结;扩展分析;现存问题分析;

1.【声明式事务】summary;

(1) 【声明式事务】就是在不修改源代码的情况下,实现事务的控制;

(2) 【声明式事务】控制的规则是:如果方法执行成功,则提交事务;如果遇到运行时异常,则回滚事务;

(3) 【声明式事务】是【Spring AOP】的一种典型应用;

2.扩展分析;

(1)<tx:method>:通配符映射:匹配多个方法


(2)如果某类方法不需要事务控制,如何设置;


(3)【其他选项 *】:设置【未被匹配到的方法】;


(4)【声明式事务】:在某种程度上也有助于程序的规范;

【声明式事务】让程序员从繁琐的事务控制中摆脱出来,即我们不用再在代码中编写事务控制代码,而是由程序根据自己的执行情况,自己决定提交事务还是回滚事务。

3.现存问题分析:事务传播方式还未了解;

现存问题:【propagation=“NOT_SUPPORTED”】,事务传播行为是什么,然后【NOT_SUPPORTED】和【REQUIRED】区别又是什么。

事务传播方式

说明:

(1) 我们知道propagation属性设置为“REQURIED”就是开启声明式事务;但是,对于propagation属性的详细内容并不了解;所以,本篇博客就来详细介绍下事务传播行为的内容;

(2) 在实际开发中,我们很少去设置事务传播方式(行为);只是,在面试的时候可能会被问到;所以,本篇博客的内容就作为一个扩展知识吧;


一:事务传播行为简介

(1) 事务传播行为:可以使用XML的方式配置;

(2) 事务传播行为:也可以使用注解的方式配置;【使用注解方式,开发声明式事务】在下篇博客中会介绍;

(3)这儿不懂【事务传播行为】没事,接下来的案例会更好的说明;


二:为演示【事务传播行为】,而准备的案例;

(1)创建BatchService类:该类里,添加了两个新增的方法;

package com.imooc.spring.jdbc.service;
 
 import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 
 import java.util.Date;
 
 public class BatchService {
     private EmployeeDao employeeDao;
     public void importJob1() {
         for (int i = 1; i <= 10; i++) {
             Employee employee = new Employee();
             employee.setEno(8000 + i);
             employee.setEname("研发部员工" + i);
             employee.setSalary(4000f);
             employee.setDname("研发部");
             employee.setHiredate(new Date());
             employeeDao.insert(employee);
         }
     }
     public void importJob2() {
         for (int i = 1; i <= 10; i++) {
             Employee employee = new Employee();
             employee.setEno(9000 + i);
             employee.setEname("市场部员工" + i);
             employee.setSalary(4500f);
             employee.setDname("市场部");
             employee.setHiredate(new Date());
             employeeDao.insert(employee);
         }
     }
 
     public EmployeeDao getEmployeeDao() {
         return employeeDao;
     }
 
     public void setEmployeeDao(EmployeeDao employeeDao) {
         this.employeeDao = employeeDao;
     }
 }

说明:

(1) 在实际项目中,业务肯定会很复杂,自然会有很多业务方法;而自然,也会有很多新增、修改删除的业务方法。而这类新增操作的业务方法,都是需要进行事务控制的;

(2) BatchService类的主要的功能就是批量导入;

(3) 比如,BatchService类中的,inportJob1()和importJob2()这两个新增的方法,按理说都是需要进行业务控制的;

(2)修改EmployeeService类:增加BatchService属性,并调用BatchService中的方法;

package com.imooc.spring.jdbc.service;
 
 import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import java.util.Date;
 
 public class EmployeeService {
     private EmployeeDao employeeDao;
     private BatchService batchService;
 
     public void batchImport() {
         for (int i = 1; i <= 10; i++) {
 //            if (i == 3) {
 //                throw new RuntimeException("一个意料之外的异常。");
 //            }
             Employee employee = new Employee();
             employee.setEno(8000 + i);
             employee.setEname("员工" + i);
             employee.setSalary(4000f);
             employee.setDname("市场部");
             employee.setHiredate(new Date());
             employeeDao.insert(employee);
         }
     }
 
     /**
      * 调用BatchService这个【批量导入类】的批量导入方法;
      */
     public void startImportJob() {
         batchService.importJob1();
         batchService.importJob2();
     }
 
     public EmployeeDao getEmployeeDao() {
         return employeeDao;
     }
 
     public void setEmployeeDao(EmployeeDao employeeDao) {
         this.employeeDao = employeeDao;
     }
 
     public BatchService getBatchService() {
         return batchService;
     }
 
     public void setBatchService(BatchService batchService) {
         this.batchService = batchService;
     }
 }

说明:

(1) 在EmployeeService类中,新增了startImportJob()方法,该方法中,调用了BatchService类中的方法;

(2) 为了能实现(1)中的需求,在EmployeeService类中增加了BatchService属性,并添加对应的get和set方法;

(3)在IoC容器中配置BatchService对象;在EmployeeService对象中注入BatchService对象;设置BatchService两个方法的事务传播方式;


三:初始演示一:【事务传播行为】初体验;

(1)情况说明:

(2)测试、运行

(3)分析

可以发现,上面涉及到了方法的嵌套调用;如果代码正常运行、没有出现问题,其结果看着是OK的;但是,在实际开发中上面这种配置是有问题的;

因为,importJob1()和importJob2(),只需要各自保证自己就行了;这两个方法之间应该互不影响;如下:


即可以发现,importJob1()、importJob2(),startImportJob()三个方法是嵌套调用的;然后,三个方法的事务传播方式均设置成了“REQURIED”;此时,通过运行结果可以发现,importJob1()、importJob2()是一荣俱荣、一损俱损的,二者并没有做到互不影响;


四:【事务传播行为】的七种类型;

1.七种【事务传播行为】;

(1) 【REQURIED】:事务传播行为的默认值:如果我们不设置事务传播方式,那么就默认是【REQURIED】;

● 当【设置了事务的方法】进行嵌套时,如果【外侧方法】有事务,那么【内侧被调用的方法】就会加入到外侧方法的事务中;如下图所示:

● 【REQURIED】详细分析;

(2) 【REQURIES_NEW】:方法会创建新的事务;

● 【REQURIES_NEW】:即便【外侧方法】有事务,【内侧被调用的方法】依旧会给自己创建一个新事务;如下案例演示;

然后,重新运行;

● 【REQURIES_NEW】:详细分析;

(3)【NOT_SUPPORTED】:如果【外侧方法】设置了事务,则把当前事务挂起,以非事务方式执行【内侧被调用的方法】;这种策略无法保证数据的完整性,不太安全;

(4)【SUPPORTS】:如果【外侧方法】设置了事务,【内侧被调用的方法】就用这个事务;如果没有事务,就不使用事务;因为,【新增、修改、删除】不使用事务会存在风险,所以这种无法保证数据的完整性,不太安全;

(5) 【MANDATORY】:如果【外侧方法】设置了事务,【内侧被调用的方法】就用这个事务;如果【外侧方法】没有设置事务,就报错;

PS:mandatory:n:受托者;adj:强制性的; 强制的; 法定的; 义务的;

(6) 【NEVER】:如果【外侧方法】设置了事务,就报错;如果【外侧方法】没有设置事务,【内侧被调用的方法】就以非事务方式执行;这个几乎不用;

(7)【NESTED】:如果【外侧方法】设置了事务,【内侧被调用的方法】就用这个事务;如果【外侧方法】没有设置事务,【内侧被调用的方法】就采用【REQURIED】的方式;


2.summary

● 根据不同的业务需求,设置不同的事务传播行为;在实际开发中,【REQURIED】、【REQURIES_NEW】、【NOT_SUPPORTED】使用较多;

● 日常开发中,大多数我们使用【REQURIED】,因为我们一般要保证一个Service方法中的内容要么全部OK,要么什么也不做;

● 少部分情况下,使用【REQURIES_NEW】;

●【NOT_SUPPORTED】多用于查询方法上;仔细一想,因为【新增、修改、删除】存在出错的风险,一旦出错其事务就会回滚;如果,【新增、修改删除】和【查询】混合在一起时,【NOT_SUPPORTED】可以把【查询】从【整体的潜在的回滚风险】中,拎出来;

注解形式声明式事务

说明:

(1) 同理,Spring这个机构,也提供了【使用注解方式,配置声明式事务】的策略;

(2) 本篇博客的IoC容器也采用了注解方式;

(3) 【注解形式】配置声明式事务很简单,主要包括两部分:

● 在applicationContext.xml中使用 【 <tx:annotation-driven transaction-manager=“transactionManager”/】 :启用注解形式声明事务;

● 然后,在需要事务控制的类、方法上使用【@Transactional注解】;

(5)在实际开发中,声明事务的常用配置和开发习惯,这个需要慢慢积累;


一:【注解形式】配置声明式事务;

(1)pom.xml:引入所需依赖;

<?xml version="1.0" encoding="UTF-8"?>
 <project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
     <groupId>com.imooc.spring</groupId>
     <artifactId>jdbc</artifactId>
     <version>1.0-SNAPSHOT</version>
     <dependencies>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
             <version>5.2.6.RELEASE</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-jdbc</artifactId>
             <version>5.2.6.RELEASE</version>
         </dependency>
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>8.0.16</version>
         </dependency>
 
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.12</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-test</artifactId>
             <version>5.2.6.RELEASE</version>
         </dependency>
 
         <!--logback日志组件,Spring框架默认集成-->
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</artifactId>
             <version>1.2.3</version>
         </dependency>
         <dependency>
             <groupId>org.aspectj</groupId>
             <artifactId>aspectjweaver</artifactId>
             <version>1.9.5</version>
         </dependency>
     </dependencies>
 
 </project>

说明:

(1) 没什么好说的,目前在pom.xml中,引入了【Spring Context模块】,【Spring JDBC模块】,【MySQL JDBC驱动】,【junit测试依赖】,【Spring Test模块】,【logback日志依赖】,【Spring AOP底层依赖的aspectjweaver】这几个;

(2)applicationContext.xml:配置公用类对象;启用注解形式声明事务;

<?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"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
         https://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         https://www.springframework.org/schema/context/spring-
context.xsd
         http://www.springframework.org/schema/tx
         https://www.springframework.org/schema/tx/spring-tx.xsd
         http://www.springframework.org/schema/aop
         https://www.springframework.org/schema/aop/spring-aop.xsd">
     <context:component-scan base-package="com.imooc"/>
     <!--数据源-->
     <bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
         <property name="driverClassName"
value="com.mysql.cj.jdbc.Driver"/>
         <property name="url"
 
value="jdbc:mysql://localhost:3306/springjdbctest?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
         <property name="username" value="root"/>
         <property name="password" value="12345"/>
     </bean>
     <!--JdbcTemplate-->
     <bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
         <property name="dataSource" ref="dataSource"/>
     </bean>
     <!--事务管理器-->
     <bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
         <property name="dataSource" ref="dataSource"/>
     </bean>
     <!-- 启用注解形式声明式事务 -->
     <tx:annotation-driven transaction-manager="transactionManager"/>
 </beans>

说明:

(1) 【 <context:component-scan base-package=“com.imooc”/】意思是组件扫描,其作用是:确定扫描的包的范围是什么;(这个在【使用注解方式实现Spring IoC】中第一次接触过);

(2) 即使我们这儿是使用注解方式配置声明式事务;但是,DriverManagerDataSource数据源类,JdbcTemplate核心类,DataSourceTransactionManager事务管理器类这三个系统底层的公用类,还是需要采用XML方式进行配置;

(3) 【<tx:annotation-driven transaction -manager=“transactionManager”/】:启用注解形式声明事务;写了这句话后,就能完成【注解形式声明式事务的底层设置】了;(为了能更好的理解这句话,可以快速参考【基于注解开发Spring AOP】)

(3)EmployeeDao类:一个简单的Dao类;

package com.imooc.spring.jdbc.dao;
 
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Repository;
 
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
 @Repository
 public class EmployeeDao {
     @Resource
     private JdbcTemplate jdbcTemplate;
 
     public Employee findById(Integer eno){
         String sql = "select * from employee where eno = ?";
         //查询单条数据
         Employee employee = jdbcTemplate.queryForObject(sql, new
Object[]{eno}, new BeanPropertyRowMapper<Employee>(Employee.class));
         return employee;
     }
 
     public List<Employee> findByDname(String dname){
         String sql = "select * from employee where dname = ?";
         //查询复合数据
         List<Employee> list = jdbcTemplate.query(sql, new
Object[]{dname}, new BeanPropertyRowMapper<Employee>(Employee.class));
         return list;
     }
 
     public List<Map<String, Object>> findMapByDname(String dname){
         String sql = "select eno as empno , salary as s from employee
where dname = ?";
         //将查询结果作为Map进行封装
         List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql,
new Object[]{dname});
         return maps;
     }
 
     public void insert(Employee employee){
         String sql = "insert into
employee(eno,ename,salary,dname,hiredate) values(?,?,?,?,?)";
         //利用update方法实现数据写入操作
         jdbcTemplate.update(sql,new Object[]{
            employee.getEno() ,
employee.getEname(),employee.getSalary(),employee.getDname() ,
employee.getHiredate()
         });
     }
 
     public int update(Employee employee){
         String sql = "UPDATE employee SET ename = ?, salary = ?, dname =
?, hiredate = ? WHERE eno = ?";
         int count = jdbcTemplate.update(sql, new
Object[]{employee.getEname(), employee.getSalary(), employee.getDname(),
employee.getHiredate(), employee.getEno()});
         return count;
     }
 
     public int delete(Integer eno){
         String sql = "delete from employee where eno = ?";
         return jdbcTemplate.update(sql, new Object[]{eno});
     }
 
 
     public JdbcTemplate getJdbcTemplate() {
         return jdbcTemplate;
     }
 
     public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
         this.jdbcTemplate = jdbcTemplate;
     }
 }

说明:

(1) 这儿我们也使用注解方式配置IoC容器了;PS:为了少点麻烦,要注意如下这种的名字的对应:

(2) EmployeeDao没什么好说的,就是使用注解形式配置IoC,然后使用注解完成对象的注入;

(4)EmployeeService:一个简单的Service类;使用注解配置了声明式事务;

package com.imooc.spring.jdbc.service;
 
 import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.Date;
 @Service
 //声明式事务核心注解
 //放在类上,将声明式事务配置应用于当前类所有方法,默认事务传播为 REQUIRED
 @Transactional
 public class EmployeeService {
     @Resource
     private EmployeeDao employeeDao;
     @Resource
     private BatchService batchService;
 
     @Transactional(propagation = Propagation.NOT_SUPPORTED , readOnly =
true)
     public Employee findById(Integer eno){
         return employeeDao.findById(eno);
     }
 
     public void batchImport() {
         for (int i = 1; i <= 10; i++) {
             if(i==3){
                 throw new RuntimeException("意料之外的异常");
             }
             Employee employee = new Employee();
             employee.setEno(8000 + i);
             employee.setEname("员工" + i);
             employee.setSalary(4000f);
             employee.setDname("市场部");
             employee.setHiredate(new Date());
             employeeDao.insert(employee);
         }
     }
 
     public void startImportJob(){
         batchService.importJob1();
         if(1==1){
             throw new RuntimeException("意料之外的异常");
         }
         batchService.importJob2();
         System.out.println("批量导入成功");
     }
 
     public EmployeeDao getEmployeeDao() {
         return employeeDao;
     }
 
     public void setEmployeeDao(EmployeeDao employeeDao) {
         this.employeeDao = employeeDao;
     }
 
     public BatchService getBatchService() {
         return batchService;
     }
 
     public void setBatchService(BatchService batchService) {
         this.batchService = batchService;
     }
 }
 

说明:

(1) 对于EmployeeService,我们也使用注解方式配置其IoC;

(2) 在类上使用【@Transactional注解】,传播方式默认为REQURIED;那么这个在类上的配置,会应用到本类的所有方法上;

(3) 在方法上使用【@Transactional注解】,则该方法优先使用在本方法上配置的事务传播方式;

(4) 即【@Transactional注解】可以用在类上,也可以用在方法上;

● 写在类上的【@Transactional注解】,会作用在本类的所有方法上;

● 写在方法上的【@Transactional注解】, 只对当前方法生效;

● 在加载是时候,写在方法上的【@Transactional注解】比写在类上的【@Transactional注解】优先级高;

(5)BatchService:一个简单的Service类;使用注解配置了声明式事务;

package com.imooc.spring.jdbc.service;
 
 import com.imooc.spring.jdbc.dao.EmployeeDao;
 import com.imooc.spring.jdbc.entity.Employee;
 import com.sun.xml.internal.ws.developer.Serialization;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.Date;
 @Service
 @Transactional(propagation = Propagation.NOT_SUPPORTED,readOnly = true)
 public class BatchService {
     @Resource
     private EmployeeDao employeeDao;
     @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void importJob1(){
         for (int i = 1; i <= 10; i++) {
             Employee employee = new Employee();
             employee.setEno(8000 + i);
             employee.setEname("研发部员工" + i);
             employee.setSalary(4000f);
             employee.setDname("研发部");
             employee.setHiredate(new Date());
             employeeDao.insert(employee);
         }
     }
 
     @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void importJob2(){
         for (int i = 1; i <= 10; i++) {
             Employee employee = new Employee();
             employee.setEno(9000 + i);
             employee.setEname("市场部员工" + i);
             employee.setSalary(4500f);
             employee.setDname("市场部");
             employee.setHiredate(new Date());
             employeeDao.insert(employee);
         }
     }
 
     public EmployeeDao getEmployeeDao() {
         return employeeDao;
     }
 
     public void setEmployeeDao(EmployeeDao employeeDao) {
         this.employeeDao = employeeDao;
     }
 }

说明:

(1) 对于BatchService,我们也使用注解方式配置其IoC;

(2) 在类上使用【@Transactional注解】;因为按照业务需求,BatchService中的方法都需要在独立的事务中运行,所以在BatchService的类上使用如下配置:

(6)测试;