Spring4(三)JdbcTemplate和事务管理

1、JdbcTemplate

为了使 JDBC 更加易于使用,Spring JDBC API 上定义了一个抽象层以此建立一个 JDBC 存取框架。

作为 Spring JDBC 框架的核心JDBC 模板的设计目的是为不同类型的 JDBC 操作提供模板方法每个模板方法都能控制整个过程并允许覆盖过程中的特定任务通过这种方式可以在尽可能保留灵活性的情况下将数据库存取的工作量降到最低。

1.1 MySql数据库环境准备:

CREATE DATABASE spring4;
USE spring4;

CREATE TABLE t_employee (
	id INT PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(50),
	email VARCHAR(50),
	dept_id INT
);

CREATE TABLE t_dept(
	id INT PRIMARY KEY AUTO_INCREMENT,
	dept_name VARCHAR(100)
);

ALTER TABLE t_employee ADD CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES t_dept(id);

INSERT INTO t_dept VALUES (1,'研发部');
INSERT INTO t_dept VALUES (2,'技术部');

INSERT INTO t_employee VALUES (1,'tom','tom@aa.com',1);
INSERT INTO t_employee VALUES (2,'bob','bob@aa.com',2);
INSERT INTO t_employee VALUES (3,'jack','jack@aa.com',1);
INSERT INTO t_employee VALUES (4,'jerry','jerry@aa.com',2);

ALTER TABLE t_employee CHANGE COLUMN NAME  last_name VARCHAR(50);

1.2 创建普通Java工程

1.2.1 创建工程,导入jar包

跟之前一样创建Java工程,需要导入的jar包:

1.2.2 配置文件

在类路径下创建conf资源文件夹,创建db.properties文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///spring4
jdbc.username=root
jdbc.password=123

jdbc.initPoolSize=5
jdbc.maxPoolSize=20

conf下面再创建spring的配置文件目录,里面创建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:aop="http://www.springframework.org/schema/aop"
       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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <!--引入外部数据源-->
    <context:property-placeholder location="classpath:db.properties"/>

    <!--配置数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
    </bean>

    <!--配置Spring的JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--他必须要配置一个数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

1.2.3 创建表对应的实体

package com.spring.jdbc;

public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    private Department department;

    //get/set以及toString方法
}
package com.spring.jdbc;

public class Department {
    private Integer id;
    private String departmentName;

    //get/set以及toString方法
}

1.2.4 JdbcTemplate的测试

package com.spring.jdbc;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class JdbcTemplateTest {

    private ApplicationContext applicationContext;
    private JdbcTemplate jdbcTemplate;

    @Before//在执行其他测试方法之前,会先执行该方法
    public void init(){
        applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
        jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
    }

    /**
     * 获取单个列的值, 或做统计查询
     * 使用 queryForObject(String sql, Class<Long> requiredType)
     */
    @Test
    public void testQueryForObject2(){
        String sql = "select count(*) from t_employee";
        Long count = jdbcTemplate.queryForObject(sql, Long.class);
        System.out.println(count);
    }

    /**
     * 查到实体类的集合
     * 注意调用的不是 queryForList 方法
     */
    @Test
    public void testQueryForList(){
        String sql = "SELECT id, last_name lastName, email FROM t_employee WHERE id < ?";
        RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
        List<Employee> employees = jdbcTemplate.query(sql, rowMapper,5);
        System.out.println(employees);
    }

    /**
     * 从数据库中获取一条记录, 实际得到对应的一个对象
     * 注意不是调用 queryForObject(String sql, Class<Employee> requiredType, Object... args) 方法!
     * 而需要调用 queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args)
     * 1. 其中的 RowMapper 指定如何去映射结果集的行, 常用的实现类为 BeanPropertyRowMapper
     * 2. 使用 SQL 中列的别名完成列名和类的属性名的映射. 例如 last_name lastName
     * 3. 不支持级联属性. JdbcTemplate 到底是一个 JDBC 的小工具, 而不是 ORM 框架
     */
    @Test
    public void testQueryForObject(){
        String sql = "select id,last_name lastName,email,dept_id deptId from t_employee where id = ?";
        RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
        Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1);
        System.out.println(employee);
    }

    /**
     * 执行批量更新: 批量的 INSERT, UPDATE, DELETE
     * 最后一个参数是 Object[] 的 List 类型: 因为修改一条记录需要一个 Object 的数组, 那么多条不就需要多个 Object 的数组吗
     */
    @Test
    public void testBatchUpdate(){
        String sql = "insert into t_employee (last_name,email,dept_id) values (?,?,?)";
        List<Object[]> list = new ArrayList<>();
        Object[] objects1 = new Object[]{"A","aa@aa.com",1};
        Object[] objects2 = new Object[]{"B","bb@aa.com",2};
        Object[] objects3 = new Object[]{"C","cc@aa.com",2};
        Object[] objects4 = new Object[]{"D","dd@aa.com",1};
        list.add(objects1);
        list.add(objects2);
        list.add(objects3);
        list.add(objects4);
        //批量新增
        int[] inserts = jdbcTemplate.batchUpdate(sql, list);
        System.out.println(inserts.length);

        //删除语句
        sql = "delete from t_employee where name = ?";
        List<Object[]> nameList = new ArrayList<>();
        nameList.add(new Object[]{"A"});
        nameList.add(new Object[]{"B"});
        nameList.add(new Object[]{"C"});
        nameList.add(new Object[]{"D"});
        //批量删除
        int[] deletes = jdbcTemplate.batchUpdate(sql, nameList);
        System.out.println(deletes.length);
    }

    // update 方法可以进行 INSERT UPDATE DELETE操作
    @Test
    public void testUpdate(){
        //单条新增
        String sql = "insert into t_employee (last_name,email,dept_id) values (?,?,?)";
        Object[] args = new Object[]{"pipi","pipi@aa.com",2};
        int insert = jdbcTemplate.update(sql, args);
        System.out.println(insert);
        //单条修改
        String sql2 = "update t_employee set last_name = ? where id = ?";
        int update = jdbcTemplate.update(sql2, "bobo", 5);
        System.out.println(update);
        //单条删除
        String sql3 = "delete from t_employee where id = ?";
        int delete = jdbcTemplate.update(sql3,  5);
        System.out.println(delete);
    }

    //测试是否可以获取到连接
    @Test
    public void testConnection() throws SQLException {
        DataSource da = applicationContext.getBean(DataSource.class);
        System.out.println(da.getConnection());
    }

}

 

1.3 实际项目中JdbcTemplate的用法

每次使用都创建一个 JdbcTemplate 的新实例 这种做法效率很低下。
JdbcTemplate 类被设计成为线程安全的 所以可以在  IOC 容器中声明它的单个实例 并将这个实例注入到所有的 DAO 实例中。
JdbcTemplate 也利用了 Java 1.5 的特定 ( 自动装箱 泛型 可变长度等 ) 来简化 开发;

Spring JDBC 框架还提供了一个 JdbcDaoSupport 类来简化 DAO 实现该类声明了 jdbcTemplate 属性它可以从 IOC 容器中注入或者自动从数据源中创建。(不推荐使用,使用起来没那么方便,需要注入数据源,但是不能通过注解注入)。

实际开发中,JdbcTemplate会在持久层(Dao)进行注入,然后配置文件配置扫描,扫描持久层类上的@Repository注解,在其他业务层,注入持久层就行了:

1.3.1 修改配置文件

配置文件中,添加包扫描:

 <!--配置包扫描,扫描@Repository @Service  @Controller-->
    <context:component-scan base-package="com.spring.jdbc"/>

1.3.2 创建Dao持久层

package com.spring.jdbc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

@Repository
public class EmployeeDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public Employee getById(Integer id){
        String sql = "select id , last_name lastName,email from t_employee where id = ?";
        RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
        Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1);
        return employee;
    }
}

1.3.3 测试

public class JdbcTemplateTest {

    private ApplicationContext applicationContext;
    private JdbcTemplate jdbcTemplate;
    private EmployeeDao employeeDao;

    @Before//在执行其他测试方法之前,会先执行该方法
    public void init(){
        applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
        jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
        employeeDao = applicationContext.getBean(EmployeeDao.class);
    }

    /**
     * 一般项目中,会有一个持久层,持久层中注入JdbcTemplate,然后业务层再调用持久层的方法
     */
    @Test
    public void getByDao(){
        Employee en = employeeDao.getById(1);
        System.out.println(en);
    }
}

2、NamedParameterJdbcTemplate

在经典的 JDBC 用法中 ,SQL 参数是用占位符 ? 表示 并且受到位置的限制 定位参数的问题在于 一旦参数的顺序发生变化 就必须改变参数绑定;
Spring JDBC 框架中 绑定 SQL 参数的另一种选择是使用 具名参数(named parameter)。
具名参数: SQL 名称(以冒号开头) 而不是按位置进行指定 具名参数更易于维护 也提升了可读性 具名参数由框架类在运行时用占位符取代;
具名参数只在 NamedParameterJdbcTemplate 中得到支持。
 
 

2.1 配置NamedParameterJdbcTemplate

<!--配置NamedParameterJdbcTemplate  该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数-->
    <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource"/>
    </bean>

 

2.2 测试

package com.spring.jdbc;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;

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

public class NamedParameterJdbcTemplateTest {
    private ApplicationContext applicationContext;
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    @Before
    public void init(){
        applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
        namedParameterJdbcTemplate = applicationContext.getBean(NamedParameterJdbcTemplate.class);
    }

    /**
     * 使用具名参数时, 可以使用 update(String sql, SqlParameterSource paramSource) 方法进行更新操作
     * 1. SQL 语句中的参数名和类的属性一致!
     * 2. 使用 SqlParameterSource 的 BeanPropertySqlParameterSource 实现类作为参数.
     */
    @Test
    public void testBean(){
        String sql = "insert into t_employee (id,last_name,email) values (:id,:lastName,:email)";
        Employee employee = new Employee();
        employee.setId(200);
        employee.setLastName("erbai");
        employee.setEmail("erbai@aa.com");
        //创建参数
        SqlParameterSource paramSource = new BeanPropertySqlParameterSource(employee);
        int row = namedParameterJdbcTemplate.update(sql, paramSource);
        System.out.println(row);
    }

    /**
     * 可以为参数起名字.
     * 1. 好处: 若有多个参数, 则不用再去对应位置, 直接对应参数名, 便于维护
     * 2. 缺点: 较为麻烦.
     */
    @Test
    public void testMap(){
        String sql = "insert into t_employee (id,last_name,email,dept_id) values (:id,:lastName,:email,:deptId)";
        Map<String,Object> paramMap = new HashMap<>();
        paramMap.put("id",100);
        paramMap.put("lastName","baige");
        paramMap.put("email","baige@aa.com");
        paramMap.put("deptId",2);
        int row = namedParameterJdbcTemplate.update(sql, paramMap);
        System.out.println(row);
    }
}

 

3、Spring事务管理

3.1 简介

事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性.。

事务就是一系列的动作,它们被当做一个单独的工作单元这些动作要么全部完成要么全部不起作用。

事务的四个关键属性(ACID):

       原子性(atomicity): 事务是一个原子操作由一系列动作组成事务的原子性确保动作要么全部完成,要么完全不起作用

       一致性(consistency):一旦所有事务动作完成事务就被提交数据和资源就处于一种满足业务规则的一致性状态中;

       隔离性(isolation):可能有许多事务会同时处理相同的数据因此每个事物都应该与其他事务隔离开来防止数据损坏

       持久性(durability):一旦事务完成无论发生什么系统错误它的结果都不应该受到影响通常情况下事务的结果被写到持久化存储器中。

 

3.2 Spring 中的事务管理器

       作为企业级应用程序框架,Spring 在不同的事务管理 API 之上定义了一个抽象层而应用程序开发人员不必了解底层的事务管理 API,就可以使用 Spring 的事务管理机制

       Spring 既支持编程式事务管理也支持声明式的事务管理

       编程式事务管理将事务管理代码嵌入到业务方法中来控制事务的提交和回滚在编程式管理事务时必须在每个事务操作中包含额外的事务管理代码

       声明式事务管理大多数情况下比编程式事务管理更好用将事务管理代码从业务方法中分离出来以声明的方式来实现事务管理事务管理作为一种横切关注点可以通过 AOP 方法模块化Spring 通过 Spring AOP 框架支持声明式事务管理

       Spring 从不同的事务管理 API 中抽象了一整套的事务机制开发人员不必了解底层的事务 API,就可以利用这些事务机制有了这些事务机制事务管理代码就能独立于特定的事务技术了;

       Spring 的核心事务管理抽象是 TransactionManager ,它为事务管理封装了一组独立于技术的方法无论使用 Spring 的哪种事务管理策略(编程式或声明式),事务管理器都是必须的;

3.2.1 Spring 中的事务管理器的不同实现

1、DataSourceTransactionManager:在应用程序中只需要处理一个数据源而且通过 JDBC 存取;

2、JtaTransactionManager:JavaEE 应用服务器上用 JTA(Java Transaction API) 进行事务管理;

3、HibernateTransactionManagerHibernate 框架存取数据库;

事务管理器以普通的 Bean 形式声明在 Spring IOC 容器中

 

3.3 需求

以网上购书为例:客户购一本书,客户的余额要减少,书的库存同时也要减少;这两个操作应该要在同一个事务中。

创建数据库表:

CREATE TABLE book (
	isbn VARCHAR(20) PRIMARY KEY ,
	book_name VARCHAR(50),
	price INT
);

CREATE TABLE account (
	username VARCHAR(20) ,
	balance INT
);

CREATE TABLE book_stock (
	isbn VARCHAR(20) PRIMARY KEY ,
	stock INT
);

ALTER TABLE book_stock ADD CONSTRAINT fk_book_bs FOREIGN KEY (isbn) REFERENCES book(isbn);

INSERT INTO book VALUES ('0001','java',100);
INSERT INTO book VALUES ('0002','python',70);

INSERT INTO account VALUES ('tom',200);

INSERT INTO book_stock VALUES ('0001',10);
INSERT INTO book_stock VALUES ('0002',10);

 

3.4 基于注解的声明式事务管理

       Spring 还允许简单地用 @Transactional 注解来标注事务方法;

       为了将方法定义为支持事务处理的可以为方法添加 @Transactional 注解根据 Spring AOP 基于代理机制 只能标注公有方法

       可以在方法或者类级别上添加 @Transactional 注解当把这个注解应用到类上时这个类中的所有公共方法都会被定义成支持事务处理的。

       在 Bean 配置文件中只需要启用 <tx:annotation-driven> 元素并为之指定事务管理器就可以了

       如果事务处理器的名称是 transactionManager就可以在<tx:annotation-driven> 元素中省略 transaction-manager 属性这个元素会自动检测该名称的事务处理器。

3.4.1 配置事务管理器

<!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

3.4.2 开启注解事务

 <!--开启事务注解,如果事务管理器是名称是transactionManager ,那么可以不用配置transaction-manager="transactionManager" ,因为是默认值-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

加上包扫描:

<!--配置包扫描,扫描@Repository @Service  @Controller-->
    <context:component-scan base-package="com.spring.jdbc,com.spring.tx"/>

3.4.3 代码编写

1、自定义两个异常

package com.spring.tx.annotation;

//自定义库存不足异常
public class BookStockException extends RuntimeException {
    public BookStockException(String message) {
        super(message);
    }
}

package com.spring.tx.annotation;

//余额不足的异常
public class UserAccountException extends RuntimeException {
    public UserAccountException(String message) {
        super(message);
    }
}

2、定义dao接口和实现类

package com.spring.tx.annotation;

public interface BookShopDao {
    //根据书号获取书的单价
    public int findBookPriceByIsbn(String isbn);

    //更新数的库存. 使书号对应的库存 - 1
    public void updateBookStock(String isbn);

    //更新用户的账户余额: 使 username 的 balance - price
    public void updateUserAccount(String username, int price);
}




package com.spring.tx.annotation;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public int findBookPriceByIsbn(String isbn) {
        String sql = "select price from book where isbn = ?";
        Integer price = jdbcTemplate.queryForObject(sql, Integer.class, isbn);
        return price;
    }

    @Override
    public void updateBookStock(String isbn) {
        //检查书的库存是否足够, 若不够, 则抛出异常
        String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?";
        int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
        if(stock == 0){
            throw new BookStockException("库存不足!");
        }
        String sql = "update book_stock set stock = stock - 1 where isbn = ?";
        jdbcTemplate.update(sql,isbn);
    }

    @Override
    public void updateUserAccount(String username, int price) {
        //验证余额是否足够, 若不足, 则抛出异常
        String sql2 = "SELECT balance FROM account WHERE username = ?";
        int balance = jdbcTemplate.queryForObject(sql2, Integer.class, username);
        if(balance < price){
            throw new UserAccountException("余额不足!");
        }

        String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
        jdbcTemplate.update(sql, price, username);
    }
}

3、定义service接口和实现类

需要事务管理的方法purchase上面一定要添加@Transactional注解

package com.spring.tx.annotation;

public interface BookShopService {
    //购买书本的方法
    public void purchase(String username, String isbn);
}



package com.spring.tx.annotation;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService{
    @Autowired
    private BookShopDao bookShopDao;

    //添加事务注解
	//1.使用 propagation 指定事务的传播行为, 即当前的事务方法被另外一个事务方法调用时
	//如何使用事务, 默认取值为 REQUIRED, 即使用调用方法的事务
	//REQUIRES_NEW: 事务自己的事务, 调用的事务方法的事务被挂起. 
	//2.使用 isolation 指定事务的隔离级别, 最常用的取值为 READ_COMMITTED
	//3.默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚. 也可以通过对应的
	//属性进行设置. 通常情况下去默认值即可. 
	//4.使用 readOnly 指定事务是否为只读. 表示这个事务只读取数据但不更新数据, 
	//这样可以帮助数据库引擎优化事务. 若真的事一个只读取数据库值的方法, 应设置 readOnly=true
	//5.使用 timeout 指定强制回滚之前事务可以占用的时间
    @Transactional
    @Override
    public void purchase(String username, String isbn) {
        //查询书本单价
        int price = bookShopDao.findBookPriceByIsbn(isbn);
        //更新库存
        bookShopDao.updateBookStock(isbn);
        //更新余额
        bookShopDao.updateUserAccount(username,price);
    }
}

4、测试

package com.spring.tx.annotation;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TxAnnotationTest {
    private ApplicationContext applicationContext;
    private BookShopService bookShopService;

    @Before
    public void init(){
        applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
        bookShopService = (BookShopService) applicationContext.getBean("bookShopService");
    }

    @Test
    public void testAnnotationTx(){
        bookShopService.purchase("tom","0001");
    }
}

3.4.4 事务的传播性

       可以参考地址:https://segmentfault.com/a/1190000013341344

       当事务方法被另一个事务方法调用时必须指定事务应该如何传播例如: 方法可能继续在现有事务中运行也可能开启一个新事务并在自己的事务中运行

       事务的传播行为可以由 propagation 传播属性指定, Spring 定义了 7  种类传播行为,他们在枚举 Propagation 定义

事务传播行为类型说明
REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。(默认值)
SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作。

下面演示一下REQUIRED和REQUIRED_NEW:

需求:买多本书,用户的结账操作,但是用户余额不足以支付所有的费用

1、REQUIRED

定义 新的 Cashier 接口和实现类:

package com.spring.tx.annotation;

import java.util.List;

public interface Cashier {
    //支付费用
    public void checkout(String username, List<String> isbns);
}


package com.spring.tx.annotation;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service("cashier")
public class CashierImpl implements Cashier {
    @Autowired
    private BookShopService bookShopService;

    @Transactional
    @Override
    public void checkout(String username, List<String> isbns) {
        for(String isbn: isbns){
            bookShopService.purchase(username, isbn);
        }
    }
}

        CashierImpl中的checkout方法上面添加了@Transactional注解,表明这是一个事务方法,同时默认的传播属性 propagation=REQUIRED;

测试方法如下:

//测试事务的传播属性:propagation = REQUIRED和propagation = REQUIRED_NEW
    @Test
    public void testPropagation(){
        cashier.checkout("tom", Arrays.asList("0001","0002"));
    }

       如上,因为我们的事务方法的传播属性都是REQUIRED,所以嵌套的事务方法会使用最外层的方法是事务,里面只要有一个地方出现了异常,就会回滚整个事务。

       当 bookService purchase() 方法被另一个事务方法 checkout() 调用时, 它默认会在现有的事务内运行这个默认的传播行为就是 REQUIRED。因此在 checkout() 方法的开始和终止边界内只有一个事务,这个事务只在 checkout() 方法结束的时候被提交, 结果用户一本书都买不了。

 

2、REQUIRES_NEW

       一种常见的传播行为是 REQUIRES_NEW,它表示该方法必须启动一个新事务并在自己的事务内运行如果有事务在运行就应该先挂起它。

修改之前的purchase方法,其他都不要改:

@Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void purchase(String username, String isbn) {
        //查询书本单价
        int price = bookShopDao.findBookPriceByIsbn(isbn);
        //更新库存
        bookShopDao.updateBookStock(isbn);
        //更新余额
        bookShopDao.updateUserAccount(username,price);
    }

请注意:@Transactional(propagation = Propagation.REQUIRES_NEW),我们把传播属性设置了,这样的话,每次调用purchase方法,会挂起外部事务,只会在purchase方法的内部事务运行。如果我们的钱足够买第一本书,那么第一本书会购买成功,库存和余额都会正确的减少,但是买第二本的时候,会报余额不足异常,购买失败。

 

3.4.5 事务的隔离级别

      脏读:对于两个事物T1, T2;T1读取了已经被 T2 更新但还没有被提交的字段之后T2回滚, T1读取的内容就是临时且无效的

      不可重复读:对于两个事物 T1, T2; T1  读取了一个字段然后 T2 更新了该字段之后,, T1再次读取同一个字段值就不同了

      虚读(幻读):对于两个事物 T1, T2;T1  从一个表中读取了一个字段然后 T2 在该表中插入了一些新的行之后如果 T1 再次读取同一个表就会多出几行;

 

Spring支持五种事务隔离级别:

隔离级别说明

DEFAULT

这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别,

read uncommited

允许事务读取未被其他事务提交的变更;脏读,不可重复读,虚读都有可能发生

read commited

只允许事务读取其他事务已经提交的变更;不可重复读,虚读有可能发生(Oracle默认事务隔离级别,开发时通常使用的隔离级别)

repeatable read

确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段更新,可以避免脏读,不可重复读,但是虚读还是有可能发生(MySQL默认事务隔离级别)

serializable

确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,可以避免脏读,不可重复读,虚读,但是性能低下。

 

 

 

 

 

 

 

 

 

在@Transactional注解上,可以设置隔离级别属性:isolation = Isolation.DEFAULT,他的可取值在枚举类Isolation中定义。

 

事务的隔离级别要得到底层数据库引擎的支持,而不是应用程序或者框架的支持。

Oracle 支持的 2 种事务隔离级别:READ_COMMITED(默认值) , SERIALIZABLE;

Mysql 支持 4 中事务隔离级别;

 

3.4.6 事务的回滚属性

        默认情况下只有未检查异常(RuntimeException和Error类型的异常)会导致事务回滚,而受检查异常不会。

        事务的回滚规则可以通过 @Transactional 注解的 rollbackFor noRollbackFor 属性来定义这两个属性被声明为 Class[] 类型的, 因此可以为这两个属性指定多个异常类;例如:rollbackFor = {UserAccountException.class,BookStockException.class}

3.4.7 超时和只读属性

        由于事务可以在行和表上获得锁,  因此长事务会占用资源并对整体性能产生影响; 如果一个事物只读取数据但不做修改,数据库引擎可以对这个事务进行优化

        超时事务属性事务在强制回滚之前可以保持多久这样可以防止长期运行的事务占用资源;通过 @Transactional 注解的timeout=3(单位是秒)

       只读事务属性表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务;通过 @Transactional 注解的readOnly = true

 

3.5 基于xml配置的声明式事务管理

       事务管理是一种横切关注点

       为了在 Spring 2.x 中启用声明式事务管理可以通过 tx Schema 中定义的 <tx:advice> 元素声明事务通知为此必须事先将这个 Schema 定义添加到 <beans> 根元素中去;

       声明了事务通知后,就需要将它与切入点关联起来由于事务通知是在 <aop:config> 元素外部声明的所以它无法直接与切入点产生关联所以必须<aop:config> 元素中声明一个增强器通知与切入点关联起来

       由于 Spring AOP 是基于代理的方法所以只能增强公共方法因此只有公有方法才能通过 Spring AOP 进行事务管理。

 

3.5.1 修改上述service类

       首先,要把之前测试注解的那些类和接口,测试类以及自定义异常代码,都复制一份到tx目录下面:

       修改CashierImpl和BookShopServiceImpl,BookShopDaoImpl,把这些个类中的@Service注解、@Autowired注解和@Transactional注解都删掉,等会全部在xml配置文件中,通过<bean>进行配置。

       凡是之前通过@Autowired进行自动注入的,都变成添加一个对应的set方法,然后在配置文件中进行注入,例如:

public class BookShopDaoImpl implements BookShopDao {

    private JdbcTemplate jdbcTemplate;
    //通过set方法注入JdbcTemplate
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
}

3.5.2 创建配置文件applicationContext-tx-xml.xml

创建一个新的配置文件,所有的IOC和DI,AOP等都在配置文件中,不使用注解了:

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <!--配置包扫描,扫描@Repository @Service  @Controller-->
    <context:component-scan base-package="com.spring.jdbc,com.spring.tx"/>

    <!--引入外部数据源-->
    <context:property-placeholder location="classpath:db.properties"/>

    <!--配置数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
    </bean>

    <!--配置Spring的JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--他必须要配置一个数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置Dao-->
    <bean id="bookShopDao" class="com.spring.tx.xml.BookShopDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>

    <!--配置bookShopService-->
    <bean id="bookShopService" class="com.spring.tx.xml.service.impl.BookShopServiceImpl">
        <property name="bookShopDao" ref="bookShopDao"/>
    </bean>

    <!--配置CashierImpl-->
    <bean id="cashier" class="com.spring.tx.xml.service.impl.CashierImpl">
        <property name="bookShopService" ref="bookShopService"/>
    </bean>

    <!--1 配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--2 配置事务属性-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- 根据方法名指定事务的属性 -->
            <tx:method name="purchase" propagation="REQUIRED"/>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="find*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- 3. 配置事务切入点, 以及把事务切入点和事务属性关联起来 -->
    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(* com.spring.tx.xml.service.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>
</beans>

其他基本上不用变,只要注意一点,我们的类中,导包要正确。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring 中,我们可以通过编程式事务和声明式事务来处理数据库事务。下面我来分别介绍一下这两种事务的实现。 ## 编程式事务 编程式事务是通过代码实现事务控制,需要手动创建和提交事务Spring 提供了一个名为 `TransactionTemplate` 的类来帮助我们实现编程式事务。 我们可以使用 `TransactionTemplate` 来控制事务的开启、提交、回滚等操作。下面是一个简单的例子: ```java @Service public class UserService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private TransactionTemplate transactionTemplate; public void transfer(int fromId, int toId, int amount) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { try { jdbcTemplate.update("update account set amount = amount - ? where id = ?", amount, fromId); jdbcTemplate.update("update account set amount = amount + ? where id = ?", amount, toId); } catch (Exception e) { status.setRollbackOnly(); throw e; } } }); } } ``` 上面的代码中,我们使用 `TransactionTemplate` 来控制转账操作的事务,如果出现异常,则会回滚事务。 ## 声明式事务 声明式事务是通过配置文件来实现事务控制,无需手动创建和提交事务Spring 提供了一个叫做 `@Transactional` 的注解来帮助我们实现声明式事务。 我们只需要在需要添加事务的方法上加上 `@Transactional` 注解,就可以实现声明式事务。下面是一个简单的例子: ```java @Service public class UserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void transfer(int fromId, int toId, int amount) { jdbcTemplate.update("update account set amount = amount - ? where id = ?", amount, fromId); jdbcTemplate.update("update account set amount = amount + ? where id = ?", amount, toId); } } ``` 上面的代码中,我们在 `transfer` 方法上添加了 `@Transactional` 注解,这样就可以实现转账操作的事务控制了。 声明式事务的好处是我们无需手动创建和提交事务,代码更加简洁,但是配置稍微复杂一些。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值