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','[email protected]',1);
INSERT INTO t_employee VALUES (2,'bob','[email protected]',2);
INSERT INTO t_employee VALUES (3,'jack','[email protected]',1);
INSERT INTO t_employee VALUES (4,'jerry','[email protected]',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","[email protected]",1};
        Object[] objects2 = new Object[]{"B","[email protected]",2};
        Object[] objects3 = new Object[]{"C","[email protected]",2};
        Object[] objects4 = new Object[]{"D","[email protected]",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","[email protected]",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("[email protected]");
        //創建參數
        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","[email protected]");
        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>

其他基本上不用變,只要注意一點,我們的類中,導包要正確。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章