18.声明式事务的介绍及其注解实现和相关属性

一:编程式事务

事务功能的相关操作全部通过自己编写代码来实现:

Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}

编程式的实现方式存在缺陷:

  • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
  • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复 用。

二:声明式事务

既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。

封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。

  • 好处1:提高开发效率
  • 好处2:消除了冗余的代码
  • 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性 能等各个方面的优化

所以,我们可以总结下面两个概念:

  • 编程式:自己写代码实现功能
  • 声明式:通过配置让框架实现功能

三:基于注解的声明式事务

1)准备工作

①加入依赖

    
        
        
            org.springframework
            spring-context
            5.3.23
        
        
        
        
        
            org.springframework
            spring-orm
            5.3.23
        
        
        
            org.springframework
            spring-test
            5.3.23
        
        
        
            junit
            junit
            4.12
            test
        
        
        
            mysql
            mysql-connector-java
            8.0.16
        
        
        
            com.alibaba
            druid
            1.0.31
        
    

②创建jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
jdbc.username=root
jdbc.password=Ht1971350878

③配置Spring的配置文件

    
    

    
    

    
        
        
        
        
    

    
        
    

④创建数据表

CREATE TABLE `t_book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
`price` int(11) DEFAULT NULL COMMENT '价格',
`stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍
穹',80,100),(2,'斗罗大陆',50,100);
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);

⑤创建组件

1.创建控制层

BookController类:

@Controller
public class BookController {

}

2.创建业务层

接口BookService:

public interface BookService {
}

创建实现类BookServiceImpl:

@Service
public class BookServiceImpl implements BookService {

}

3.创建持久层

接口BookDao:

public interface BookDao {
}

创建实现类BookDaoImpl:

@Repository
public class BookDaoImpl implements BookDao {
}

但是我们要通过注解来管理组件的话,我们除了注解,还得扫描。所以我们得去spring配置文件里面加上扫描组件加上

    
    

 Controller需要用到BookService对象来进行自动装配

@Controller
public class BookController {
    @Autowired
    private BookService bookService;

    public void buyBook(Integer userId,Integer bookId) {
        bookService.buyBook(userId,bookId);
    }
}

 但这里BookService类中并没有这个方法,所以我们就需要来创建这个方法

public interface BookService {
    /**
     * 买书
     * @param userId
     * @param bookId
     */
    void buyBook(Integer userId, Integer bookId);
}

 在我们的Service层就需要来访问数据库了,所以我们就创建了持久层的对象

@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer userId, Integer bookId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId,price);
    }
}

把操作数据库的方法封装起来,以便于代码的复用

public interface BookDao {
    /**
     * 根据图书的id查询图书的价格
     * @param bookId
     * @return
     */
    Integer getPriceByBookId(Integer bookId);

    /**
     * 更新图书的库存
     * @param bookId
     */
    void updateStock(Integer bookId);

    /**
     * 更新用户的余额
     * @param userId
     * @param price
     */
    void updateBalance(Integer userId, Integer price);
}
@Repository
public class BookDaoImpl implements BookDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sql = "select price from t_book where book_id = ?";
        return jdbcTemplate.queryForObject(sql,Integer.class,bookId);
    }

    @Override
    public void updateStock(Integer bookId) {
        String sql = "update t_book set stock = stock - 1 where book_id = ?";
        jdbcTemplate.update(sql,bookId);
    }

    @Override
    public void updateBalance(Integer userId, Integer price) {
        String sql = "update t_user set balance = balance - ? where user_id = ?";
        jdbcTemplate.update(sql,price,userId);
    }
}

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:ex-annotation.xml")
public class TxByAnnotationTest {
    @Autowired
    private BookController bookController;

    @Test
    public void testBuyBook() {
        bookController.buyBook(1,1);
    }
}

但我们测试后会发现报错了,这是因为我们的余额不够钱买书。但前两个方法执行成功了,在数据库中我们的库存也会减少

四:基于注解的声明式事务之实现事务功能

声明式事务中我们不需要手动去创建事务的切面,也不需要去写通知,因为在我们当前的Spring中它为我们提供了有事务管理的切面和通知

声明式事务的配置步骤:

1.在Spring的配置文件中配置事务管理器

2.开启事务的注解驱动

在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理

@Transactional注解标识的位置:

1.标识在方法上

2.标识在类上,则类中所有的方法都会被事务管理

①在配置文件中配置事务管理器

    
    
        
    

②开启事务的注解驱动

我们开启之后,在idea中这个标签的左边会出现一个环绕通知的图标,我们写了这样一个标签它就是环绕通知了吗,其实并非如此,我们配置的事务管理器才是切面,这个里面才是通知。开启事务的注解驱动,只是将我们的事务管理器,把这个切面里面的通知作用到我们当前的连接点上

    
    

五:声明式事务的属性之只读,超时,回滚策略

①事务属性:只读

1.介绍

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这 样数据库就能够针对查询操作来进行优化。

2.使用方式

    @Transactional(
            readOnly = true
    )
    public void buyBook(Integer userId, Integer bookId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId,price);
    }

3.注意

对增删改操作设置只读会抛出下面异常:

Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

②事务属性:超时

1.介绍:

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间 占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常 程序可以执行。

概括来说就是一句话:超时回滚,释放资源。

2.使用方式

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    @Override
    @Transactional(
//            readOnly = true
            timeout = 3
    )
    public void buyBook(Integer userId, Integer bookId) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId,price);
    }
}

3.观察结果

执行过程中抛出异常:

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022

③事务属性:回滚策略

1.介绍

声明式事务默认只针对运行时异常回滚,编译时异常不回滚。

可以通过@Transactional中相关属性设置回滚策略

  • rollbackFor属性:需要设置一个Class类型的对象
  • rollbackForClassName属性:需要设置一个字符串类型的全类名
  • noRollbackFor属性:需要设置一个Class类型的对象
  • rollbackFor属性:需要设置一个字符串类型的全类名

2.使用方式

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    @Override
    @Transactional(
//            readOnly = true
//            timeout = 3
//            noRollbackFor = ArithmeticException.class
            noRollbackForClassName = "java.lang.ArithmeticException"
    )
    public void buyBook(Integer userId, Integer bookId) {
//            try {
//                TimeUnit.SECONDS.sleep(5);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId,price);
        System.out.println(1/0);
    }
}

3.观察结果

虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行

你可能感兴趣的:(Spring框架,数据库,java,mysql)