Spring 學習筆記③:JDBC與事務管理


收藏這三篇筆記,完整回顧Spring常見問題及使用方式速查:

  1. Spring 學習筆記①:IoC容器、Bean與注入
  2. Spring 學習筆記②:動態代理及面向切面編程
  3. Spring 學習筆記③:JDBC與事務管理(即本篇)

0. 基本概念

  • Spring 框架提供的JDBC支持主要由四個包組成,分別是 core(核心包)、object(對象包)、dataSource(數據源包)和 support(支持包)。
<!-- JDBC模板 -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>
<!-- 事務控制 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>
<!-- mysql依賴 -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.20</version>
</dependency>

1. JDBCTemplate使用示例

1.1 數據源及配置

數據源的基本類即爲 org.springframework.jdbc.dataSource.DriverManagerDataSource ,主要功能是獲取數據庫連接,還可以引入緩衝池、分佈式事務等,需要進行以下配置:

<!-- 配置數據源 --> 
<bean id="dataSource" class="org.springframework.jdbc.dataSource.DriverManagerDataSource">
  <!-- 數據庫驅動-->
  <!-- 注意:com.mysql.jdbc.Driver 在`8.0.x`的mysql驅動中已廢棄 -->
  <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> 
  <!-- 連接數據庫的url,需要同步時區-->
  <property name= "url" value="jdbc:mysql://localhost:3306/spring?serverTimezone=Asia/Shanghai" />
  <!-- 連接數據庫的用戶名 -->
  <property name="username" value="$root" />
  <!-- 連接數據庫的密碼 -->
  <property name="password" value="$password" />
  <!-- 連接池的配置 -->
  <property name="initialPoolSize" value="3"></property>
  <property name="maxPoolSize" value="10"></property>
  <property name="maxStatements" value="100"></property>
  <property name="acquireIncrement" value="2"></property>

</bean>

核心類是 org.springframework.jdbc.core.JDBCTemplate ,需要載入數據源進行實例化:

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.jdbcTemplate">
  <!--默認必須使用數據源-->
  <property name="dataSource" ref="dataSource"/>
</bean>

也可以使用配置文件類完成以上配置:

@Bean
public DriverManagerDataSource dataSource(){
    String url = "jdbc:mysql://localhost:3306/spring?serverTimezone=Asia/Shanghai";
    String username = "root";
    String password = "$password";
    DriverManagerDataSource d = new DriverManagerDataSource(url, username, password);
    d.setDriverClassName("com.mysql.cj.jdbc.Driver");
    return d;
}

@Bean
public JdbcTemplate jdbcTemplate(){
    return new JdbcTemplate(dataSource());
}

1.2 數據庫配置

假設數據庫有表 student

CREATE TABLE student (
    id BIGINT(20) NOT NULL COMMENT '主鍵ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年齡',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '郵箱',
    PRIMARY KEY (id)
);

INSERT INTO student (id, name, age, email) VALUES
  (1, 'Jack',  28, '[email protected]'),
  (2, 'Louis', 20, '[email protected]'),
  (3, 'Tom',   24, '[email protected]'),
  (4, 'Sandy', 12, '[email protected]'),
  (5, 'Lily',  85, '[email protected]');

以及對應的Model:

@Data
@AllArgsConstructor
public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String email;
}

1.3 查詢語句示例

JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
jdbcTemplate.query(
    "SELECT * FROM student;",
    (resultSet, i) -> new Student(resultSet.getInt(1), resultSet.getString(2), resultSet.getInt(3), resultSet.getString(4))
).forEach(System.out::println);

2. Spring事務控制

  • 事務控制一般在Service層,分爲“編程式事務”和“聲明式事務”;Sring提供的是後者,基於AOP實現。
  • Spring基於JDBC的事務管理器爲 DataSourceTransactionManager

2.1 配置文件

由於Spring的事務控制是基於AOP實現的,因此也需要引入AOP的命名空間。

<beans xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>

2.2 事務接口

Spring-tx (即Spring Transaction)中的三個核心接口爲:

  • PlatformTransactionManager :用於管理事務。
  • TransactionDefinition :用於定義事務的的相關信息。
  • TransactionStatus :描述事務的狀態。

2.2.1 事務管理:PlatformTransactionManager

  • TransactionStatus getTransaction(TransactionDefinition definition) :用於獲取事務狀態信息。
  • void commit(TransactionStatus status) :用於提交事務。
  • void rollback(TransactionStatus status) :用於回滾事務。

2.2.2 事務信息:TransactionDefinition

  • String getName() :獲取事務對象名稱。
  • int getIsolationLevel() :獲取事務的隔離級別。
  • int getPropagationBehavior() :獲取事務的傳播行爲。
  • int getTimeout() :獲取事務的超時時間。
  • boolean isReadOnly() :獲取事務是否只讀。

其中,事務的傳播行爲被定義爲:

屬性名稱 描述 的值
PROPAGATION_REQUIRED 如果當前有事務環境就加入當前正在執行的事務環境,否則就新建一個事務。【默認】 REQUIRED
PROPAGATION_SUPPORTS 指定當前方法加入當前事務環境,如果當前沒有事務,就以非事務方式執行。 SUPPORTS
PROPAGATION_MANDATORY 指定當前方法必須加入當前的事務環境,如果當前沒有事務,則拋出異常。 MANDATORY
PROPAGATION_REQUIRES_NEW 將創建新的事務,如果當前方法已經在事務中,則將當前新建的事務掛起等待執行。 REQUIRES_NEW
PROPAGATION_NOT_SUPPORTED 不支持當前事務,以非事務狀態執行。如果當前存在事務環境,則將其掛起等待當前方法先執行。 NOT_SUPPORTED
PROPAGATION_NEVER 不支持當前事務,如果當前方法在事務中,則拋出異常。 NEVER
PROPAGATION_NESTED 指定當前方法執行時,如果已經有一個事務存在,則運行在這個嵌套的事務中。如果當前環境沒有運行的事務,就新建並與父事務相互獨立的新事務,這個事務擁有多個可以回滾的保存點——內部事務回滾不會對外部事務造成影響(只對 DataSourceTransactionManager 事務管理器起效)。 NESTED

2.2.3 事務狀態:TransactionStatus

  • void flush() :刷新事務。
  • boolean hasSavepoint() :獲取是否存在保存點。
  • boolean isCompleted() :獲取事務是否完成。
  • boolean isNewTransaction() :獲取是否是新事務。
  • boolean isRollbackOnly() :獲取是否回滾。
  • void setRollbackOnly() :設置事務回滾。

2.3 Spring的聲明式事務管理

構建一個轉賬的數據庫事務場景,則數據庫表如下:

CREATE TABLE account (
    id INT (11) PRIMARY KEY AUTO_INCREMENT COMMENT '主鍵ID',
    username VARCHAR(20) NOT NULL COMMENT '姓名',
    money INT DEFAULT NULL COMMENT '賬戶餘額'
);
/* 初始數據 */
INSERT INTO account VALUES (1, '張三', 2000);
INSERT INTO account VALUES (2, '李四', 1000);

對應的Model類:

@Data
@AllArgsConstructor
public class Account {
    private Integer id;
    private String username;
    private Integer money;
}

出具持久層:

@Repository(value = "accountDao")
public class AccountDaoImpl implements AccountDaoInterface{
    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void send(int money, Account sbPaid) {
        val sql = "UPDATE account SET money=money-? WHERE username=?;";
        this.jdbcTemplate.update(sql, money, sbPaid.getUsername());
    }

    @Override
    public void receive(int money, Account sbReceived) {
        val sql = "UPDATE account SET money=money+? WHERE username=?;";
        this.jdbcTemplate.update(sql, money, sbReceived.getUsername());
    }
    
    @Override
    public Account query(Account account){
        val sql = "SELECT * FROM account WHERE username=?";
        return this.jdbcTemplate.query(sql,
                (res, index) -> new Account(res.getInt(1),
                        res.getString(2),
                        res.getInt(3)
                ),
            account.getUsername()).get(0);
    }
}

業務邏輯爲:

@Service(value = "accountService")
public class AccountServiceImpl implements AccountServiceInterface {
    private AccountDaoInterface accountDao;

    @Autowired
    public void setAccountDao(AccountDaoInterface accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public void transfer(Account from, Account to, int money) throws Throwable {
        this.accountDao.send(money, from);
        if(money >= 1000) throw new Exception("轉賬超過限額");
        this.accountDao.receive(money, to);
    }

    @Override
    public Account query(Account account){
        return this.accountDao.query(account);
    }
}

測試類:

AccountServiceInterface accountService = (AccountServiceInterface)context.getBean("accountService");
System.out.println(accountService.query(new Account(1, "張三", -1)));
System.out.println(accountService.query(new Account(2, "李四", -1)));
accountService.transfer(
        new Account(1, "張三", -1),
        new Account(2, "李四", -1), 500); // 改成1000之後將會中斷轉賬 
System.out.println(accountService.query(new Account(1, "張三", -1)));
System.out.println(accountService.query(new Account(2, "李四", -1)));

2.3.1 示例①:基於XML的聲明式事務管理

<!-- 編寫通知:對事務進行增強(通知),需要編寫切入點和具體執行事務的細節 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
  <tx:attributes>
    <!-- 給切入點方法添加事務詳情:
                name                    :表示方法名稱(*表示任意方法名稱)
        propagation     :用於設置傳播行爲
        isolation           :表示隔離級別
        rollback-for    :需要回滾的異常類 -->
    <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.Exception"/>
  </tx:attributes>
</tx:advice>

<!-- aop編寫,讓Spring自動對目標生成代理,需要使用AspectJ的表達式 -->
<aop:config>
  <!-- 切入點 -->
  <aop:pointcut expression="execution(* MVC.Service.AccountServiceImpl.transfer(..))" id="txPointCut" />
  <!-- 通知器:將切入點與通知整合 -->
  <aop:advisor pointcut-ref="txPointCut" advice-ref="txAdvice" />
</aop:config>


2.3.2 tx:method的屬性詳解

屬性 類型 默認值 說明
propagation propagation枚舉 REQUIRED 事務傳播屬性(詳見2.2.2)
isolation isolation枚舉 DEFAULT(所用數據庫默認級別) 事務隔離級別
read-only boolean false 是否才用優化的只讀事務
timeout int -1 超時(秒)
rollbackFor Class[] {} 需要回滾的異常類
rollbackForClassName String[] {} 需要回滾的異常類名
noRollbackFor Class[] {} 不需要回滾的異常類
noRollbackForClassName String[] {} 不需要回滾的異常類名
  • read-only 的值爲 true ,則爲只讀事務,即連接點內無 INSERT 等寫操作。
  • 隔離等級共有五級:
  1. DEFAULT :【默認】採用數據庫默認隔離級別。
  2. SERIALIZABLE :最嚴格的級別,事務串行執行,資源消耗最大。
  3. REPEATABLE_READ :保證了一個事務不會修改已經由另一個事務讀取但未提交的數據。避免了“髒讀取”和“不可重複讀取”的情況,但是帶來了更多的性能損失。
  4. READ_COMMITTED :大多數主流數據庫的默認事務等級,保證了一個事務不會讀到另一個並行事務已修改但未提交的數據,避免了“髒讀取”。該級別適用於大多數系統。
  5. READ_UNCOMMITTED :保證了讀取過程中不會讀取到非法數據。隔離級別在於處理多事務的併發問題。

2.3.3 示例②:基於註解的聲明式事務管理

在配置文件中開啓事務註解的驅動及註冊事務管理器:

<!-- 配置事務管理器 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>
<!-- 註冊事務管理驅動 -->
<tx:annotation-driven transaction-manager="txManager"/>

修改業務層:

@Service(value = "accountService")
// 添加此註釋,參數與 2.3.1 & 2.3.2 小節中一致
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, rollbackFor = {Exception.class})
public class AccountServiceImpl implements AccountServiceInterface {
    private AccountDaoInterface accountDao;

    @Autowired
    public void setAccountDao(AccountDaoInterface accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public void transfer(Account from, Account to, int money) throws Throwable {
        this.accountDao.send(money, from);
        if(money >= 1000) throw new Exception("轉賬超過限額");
        this.accountDao.receive(money, to);
    }

    @Override
    public Account query(Account account){
        return this.accountDao.query(account);
    }
}

附錄 學習筆記①~③代碼及工程文件

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