AdSense

網頁

2020/10/22

Spring @Transactional Propagation.REQUIRES_NEW

Spring @Transactional屬性propagation設為Propagation.REQUIRES_NEW的效果如下。

範例環境:

  • Spring Boot 2.3.2.RELEASE
  • Spring Data JPA
  • H2 Database
  • Lombok

Spring @Transactionalpropagation如未指定則預設為Propagation.REQUIRED,作用為如果目前已經處在一個交易,則使用原本的交易,沒有的話則建立新的交易。

若把propagation設為Propagation.REQUIRES_NEW,則效果為如果已經有交易存在則先暫停並另開新的交易處理,不使用原本的交易。

例如下面Employee是交易存取的entity。

Employee

package com.abc.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Employee implements Serializable {
    private static final Long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

}

EmployeeRepository為Spring Data JPA的repository,用來存取Employee

EmployeeRepository

package com.abc.demo.repository;

import com.abc.demo.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}

DemoDao呼叫EmployeeRepository新增及修改。

注意update2()@Transactional屬性propagationPropagation.REQUIRES_NEW,且故意拋出RuntimeException模擬例外發生的情況。

DemoDao

package com.abc.demo.dao;

import com.abc.demo.entity.Employee;
import com.abc.demo.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class DemoDao {

    @Autowired
    private EmployeeRepository employeeRepository;

    /** 新增 */
    @Transactional
    public void insert(String name) {
        Employee employee = Employee.builder()
                .name(name)
                .build();
        employeeRepository.save(employee);
    }

    /** 修改1 */
    @Transactional
    public void update1(long id, String name) {
        employeeRepository.findById(id).ifPresent(e -> {
            e.setName(name);
            employeeRepository.save(e);
        });
    }

    /** 修改2 */
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 另外產生新的交易,所以丟出例外時僅此方法的異動回滾
    public void update2(long id, String name) {
        try {
            employeeRepository.findById(id).ifPresent(e -> {
                e.setName(name);
                employeeRepository.save(e);
            });
            throw new RuntimeException("Database error."); // 模擬交易中發生錯誤
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

DemoServicemodify()中呼叫了DemoDao的三個方法,順序為:

  1. 新增一個Employee名為Ivy為第一筆資料,id1
  2. 修改Employee id為1的名稱為John
  3. 修改Employee id為1的名稱為Kenny

DemoService

package com.abc.demo.service;

import com.abc.demo.dao.DemoDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class DemoService {

    @Autowired
    private DemoDao demoDao;

    @Transactional
    public void modify() {
        demoDao.insert("Ivy");        // { "id"=1, "name"="Ivy" }
        demoDao.update1(1L, "John");  // Ivy -> John
        demoDao.update2(1L, "Kenny"); // John -> Kenny
    }

}

則執行DemoService.modify()時,在update2()發生錯誤造成回滾,由於update2()設定了Propagation.REQUIRES_NEW,也就是新建一個交易,所以回滾範圍限於update2()內,前面已執行insert()update1()的異動沒有回滾,最終Employee的名稱為John


DemoServiceTests測試。

DemoServiceTests

package com.abc.demo.service;

import com.abc.demo.entity.Employee;
import com.abc.demo.repository.EmployeeRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoServiceTests {

    @Autowired
    private DemoService demoService;
    @Autowired
    private EmployeeRepository employeeRepository;

    @Test
    void modify_nameIsJohn() {
        
        demoService.modify();
        
        String name = employeeRepository.findById(1L)
                .map(Employee::getName).orElse("");

        Assertions.assertEquals("John", name); // pass
    }

}

參考github


1 則留言:

卓慕 提到...

@Service
public class DemoService {

@Autowired
private DemoDao demoDao;

@Transactional
public void modify() {
demoDao.insert("Ivy"); // { "id"=1, "name"="Ivy" }
demoDao.update1(1L, "John"); // Ivy -> John
demoDao.update2(1L, "Kenny"); // John -> Kenny
}

}
你這樣寫會有問題
方便解釋我把@Transactional分成以下四個
主 @Transactional
insert @Transactional
update1 @Transactional
update2 @Transactional

因為 insert、update1 @Transactional沒有寫propagation ,所以默認使用REQUIRED,全部加入到主 @Transactional 裡


1.第一個有問題地方是update2 裡面加了try catch,這會導致異常被捕獲,@Transactional失效,回滾失敗

2.第二個問題是,應該要在 Service 那邊加上try catch,不然update2 的異常會出去影響到主 @Transactional,導致加入主 @Transactional的insert、update1 @Transactional也一起觸發回滾,
簡單講就是,會全部回滾

AdSense