21 トランザクション管理
21.1 トランザクションの概要
トランザクションは操作の集合であり、分割できない作業単位です。トランザクションはすべての操作を1つの整体として、データベースにコミットするか、操作リクエストをロールバックします。つまり、これらの操作は同時に成功するか、同時に失敗します。
トランザクションの操作は以下の3ステップです:
- トランザクション開始(一連の操作開始前に、トランザクションを開始): start transaction / begin
- トランザクションコミット(すべての操作が成功した場合、コミット): commit
- トランザクションロールバック(途中でいずれかの操作で異常が発生した場合、ロールバック): rollback
21.2 Springトランザクション管理
21.2.1 ケーススタディ
次にトランザクション管理の案例を確認します:部署の解散(部署の削除)
要件:部署が解散された場合、部署情報を削除するだけでなく、その部署に所属する従業員データも削除する必要があります。
手順:
- IDに基づいて部署データを削除
- 部署IDに基づいて該部署の従業員を削除
DepartmentServiceImpl
@Slf4j
@Service
public class DepartmentServiceImpl implements DepartmentService {
@Autowired
private DepartmentMapper departmentMapper;
@Autowired
private EmployeeMapper employeeMapper;
@Override
public void remove(Integer deptId) {
//部署IDに基づいて部署情報を削除
departmentMapper.deleteById(deptId);
//部署下の全従業員情報を削除
employeeMapper.deleteByDepartmentId(deptId);
}
}
DepartmentMapper
@Mapper
public interface DepartmentMapper {
@Delete("delete from department where id = #{id}")
void deleteById(Integer id);
}
EmployeeMapper
@Mapper
public interface EmployeeMapper {
@Delete("delete from employee where dept_id = #{deptId}")
int deleteByDepartmentId(Integer deptId);
}
SpringBootサービスを再起動し、postmanで部署削除をテストします。
コードが正常な場合、department表とemployee表のデータが削除されます。
DepartmentServiceImplクラスのコードを変更し、異常が発生する可能性のあるコードを追加します:
@Slf4j
@Service
public class DepartmentServiceImpl implements DepartmentService {
@Autowired
private DepartmentMapper departmentMapper;
@Autowired
private EmployeeMapper employeeMapper;
@Override
public void remove(Integer deptId) {
//部署IDに基づいて部署情報を削除
departmentMapper.deleteById(deptId);
//シミュレーション:異常発生
int result = 10 / 0;
//部署下の全従業員情報を削除
employeeMapper.deleteByDepartmentId(deptId);
}
}
SpringBootサービスを再起動し、postmanで部署削除をテストします。
データベーステーブルを確認:
- 2号部署が削除された
- 2号部署下の従業員データが削除されていない
上記プログラムの問題点:プログラムが例外をスローしても、部署は削除されるが、部署下の従業員は削除されず、データの一貫性が保たれない。
21.2.2 原因分析
原因:
- 最初にIDに基づいて部署を削除する操作を実行し、このステップが完了すると、データベースのdepartmentテーブルのデータがすでに削除されている
- 10 / 0操作を実行し、例外をスロー
- 例外をスローする前までに、下のすべてのコードは実行されなくなり、部署IDに基づいて該部署の従業員を削除するという操作も実行されない
この问题时、部署は削除されたが、部署下の従業員は依然として存在し、業務操作前後のデータが一致しない。操作前後のデータの一貫性を保証するには、解散部署に含まれる2つの業務操作を、すべて成功するか、すべて失敗させる必要がある。这时,就可以實現トランザクションを通じて。なぜなら、トランザクション内の複数の業務操作は、すべて成功するか、すべて失敗するからである。
delete削除業務機能にトランザクションを追加:
- メソッド実行前にトランザクションを開始し、メソッドが正常に実行되면トランザクションをコミットし、メソッド実行過程で例外が発生したらトランザクションをロールバックする
Springフレームワークでは、トランザクション制御のコードがすでに封装られており、手動で実装する必要はなく、@Transactionalだけで解决できる。
21.2.3 Transactionalアノテーション
作用:現在のメソッドの実行開始前にトランザクションを開始し、メソッド実行完了後にトランザクションをコミットする。メソッド実行過程で例外が発生した場合は、トランザクションのロールバック操作を行う。
一般的には業務層でトランザクションを制御する。因为、業務層では、1つの業務機能が複数のデータアクセス操作含まれる可能性がある。業務層でトランザクションを制御すれば、複数のデータアクセス操作を1つのトランザクション範囲内で制御できる。
記述位置:
- メソッド:現在のメソッドをSpringのトランザクション管理に委ねる
- クラス:現在のクラスのすべてのメソッドをSpringのトランザクション管理に委ねる
- インターフェース:インターフェース配下のすべての実装クラスのすべてのメソッドをSpringのトランザクション管理に委ねる
業務メソッドdeleteに@Transactionalを追加してトランザクションを制御:
@Slf4j
@Service
public class DepartmentServiceImpl implements DepartmentService {
@Autowired
private DepartmentMapper departmentMapper;
@Autowired
private EmployeeMapper employeeMapper;
@Override
@Transactional
public void remove(Integer deptId) {
//部署IDに基づいて部署情報を削除
departmentMapper.deleteById(deptId);
//シミュレーション:異常発生
int result = 10 / 0;
//部署下の全従業員情報を削除
employeeMapper.deleteByDepartmentId(deptId);
}
}
業務機能に@Transactionalアノテーションを追加してトランザクション管理を行った後、SpringBootサービスを再起動し、postmanでテストする。
Springトランザクション管理を追加後、サーバーサイドプログラムが例外を引发したため、トランザクションがロールバックされる。
説明:application.yml設定ファイルでトランザクション管理ログを有効にすることで、トランザクション関連のログをコンソールで確認できる。
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
21.3 rollbackFor
以前に作成した業務メソッドに@Transactionalアノテーションを追加して、トランザクション管理を実現する。
@Transactional
public void remove(Integer deptId) {
//部署IDに基づいて部署情報を削除
departmentMapper.deleteById(deptId);
//シミュレーション:異常発生
int result = deptId / 0;
//部署下の全従業員情報を削除
employeeMapper.deleteByDepartmentId(deptId);
}
上記の業務機能remove()メソッドが実行時に、除算の算術演算例外(実行時例外)を引发し、例外が発生した後、メソッドに@Transactionalアノテーションが附加されているためトランザクション管理が行われ、例外発生時にロールバック操作が実行され、トランザクション操作前後のデータが一致することが保证される。
テスト:業務機能コードを変更し、シミュレーション異常の位置で直接Exception例外(コンパイル時例外)をスローする。
@Transactional
public void remove(Integer deptId) throws Exception {
//部署IDに基づいて部署情報を削除
departmentMapper.deleteById(deptId);
//シミュレーション:異常発生
if (true) {
throw new Exception("異常が発生しました~~~");
}
//部署下の全従業員情報を削除
employeeMapper.deleteByDepartmentId(deptId);
}
説明:serviceでExceptionコンパイル時例外を上にスローした後、controllerがserviceを呼び出すため、controllerに例外処理コードが必要である。この场合、controllerで例外を上にスローし続ける选择する。
@DeleteMapping("/departments/{id}")
public ResponseResult delete(@PathVariable Integer id) throws Exception {
log.info("IDに基づいて部署を削除");
departmentService.remove(id);
return ResponseResult.success();
}
サービスを再起動してテスト:例外をスローした後、トランザクションはロールバックされるか?
- 現在のテーブルデータ:
- postmanでテストし、5号部署を削除:
- Exception例外が発生したが、トランザクションはまだコミットされた
- departmentテーブルデータ:
上記のテストから得出できる結論:デフォルト情况では、RuntimeException(実行時例外)のみが发生时のみトランザクションがロールバックされる。
すべての例外をロールバックしたい場合は、@TransactionalアノテーションのrollbackFor属性を設定する必要がある。この属性を通じて、どの异常タイプが発生した時にトランザクションをロールバックするかを指定できる。
@Slf4j
@Service
public class DepartmentServiceImpl implements DepartmentService {
@Autowired
private DepartmentMapper departmentMapper;
@Autowired
private EmployeeMapper employeeMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void remove(Integer deptId) {
//部署IDに基づいて部署情報を削除
departmentMapper.deleteById(deptId);
//シミュレーション:異常発生
int num = deptId / 0;
//部署下の全従業員情報を削除
employeeMapper.deleteByDepartmentId(deptId);
}
}
サービスを再起動し、部署削除操作をテストする。
コンソールログ:3号部署の削除操作を実行し、例外のためトランザクションがロールバックされた。
データテーブル:3号部署は削除されていない。
結論:
- Springのトランザクション管理では、デフォルトでは実行時異常RuntimeExceptionのみがロールバックされる
- 特定の异常タイプもロールバックする必要がある場合は、rollbackFor属性で指定できる
21.4 propagation
21.4.1 紹介
propagation属性:トランザクションの伝播動作を設定するために使用される
トランザクションの伝播動作:1つのトランザクションメソッドが別のトランザクションメソッドから呼び出される时、当該トランザクションメソッドはいかにしてトランザクション制御を行うべきか
例:2つのトランザクションメソッド、AメソッドとBメソッド。この2つのメソッドに 모두@Transactionalアノテーションが附加されており、この2つのメソッドがトランザクションを持つことを意味する。そして、Aメソッドの中でBメソッドを呼び出す。
所谓トランザクションの伝播動作:
- Aメソッドが実行される時、まずトランザクションを開始し、Aメソッドの中でBメソッドを呼び出す
- Bメソッド自身もトランザクションを持つ
- では、Bメソッドが実行される時、Aメソッドのトランザクションに参加するのか、それともBメソッドが新しいトランザクションを作成するのか?这就是涉及到了トランザクションの伝播動作
トランザクションの伝播動作を制御するには、@Transactionalアノテーションの後にpropagation属性を指定し、propagation属性を通じて伝播動作を指定する。以下は一般的なトランザクションの伝播動作である:
| 属性値 | 意味 |
|---|---|
| REQUIRED | 【デフォルト値】トランザクション必要、あれば参加、なければ新規作成 |
| REQUIRES_NEW | 新規トランザクション必要、常に新規トランザクション作成 |
| SUPPORTS | トランザクションをサポート、あれば参加、なければトランザクションなしで実行 |
| NOT_SUPPORTED | トランザクション非対応、トランザクションなしで実行、現在トランザクションが存在する場合は一時停止 |
| MANDATORY | トランザクション必須,否则例外スロー |
| NEVER | トランザクション禁止,否则例外スロー |
| ... |
重点的に关注するのは以下の2つのみ:
- REQUIRED(デフォルト値)
- 大部分の場合、この伝播動作を使用
- REQUIRES_NEW
- トランザクション間での相互影響を望まない場合に使用可能。例えば:注文前にログを記録する必要がある、注文の保存成功与否に関係なく、ログ記録が成功することを保证する必要がある。
21.4.2 ケーススタディ
要件:
- 部署解散時に操作ログを記録する必要がある
- 部署解散は非常に重要で危険な操作であるため、业务において部署解散操作を実行每一次都需要痕迹を残す
- 执行成功しても执行失敗しても、痕迹を残す必要がある
手順:
- 部署解散業務を実行:先に部署を削除し、先部署下の従業員を削除(前面で実装済み)
- 部署解散のログを記録(未実装)
準備作業:
データベーステーブルdepartment_logを作成:
create table department_log (
id int auto_increment primary key,
create_time datetime null,
description varchar(300) null
) comment '部署操作ログテーブル';
提供されたエンティティクラス導入:DepartmentLog
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DepartmentLog {
private Integer id;
private LocalDateTime createTime;
private String description;
}
提供されたMapperインターフェース導入:DepartmentLogMapper
@Mapper
public interface DepartmentLogMapper {
@Insert("insert into department_log(create_time, description) values(#{createTime}, #{description})")
void insert(DepartmentLog log);
}
提供された業務インターフェース導入:DepartmentLogService
public interface DepartmentLogService {
void insert(DepartmentLog log);
}
提供された業務実装クラス導入:DepartmentLogServiceImpl
@Service
public class DepartmentLogServiceImpl implements DepartmentLogService {
@Autowired
private DepartmentLogMapper logMapper;
@Transactional
@Override
public void insert(DepartmentLog log) {
logMapper.insert(log);
}
}
コード実装:
業務実装クラス:DepartmentServiceImpl
@Slf4j
@Service
public class DepartmentServiceImpl implements DepartmentService {
@Autowired
private DepartmentMapper departmentMapper;
@Autowired
private EmployeeMapper employeeMapper;
@Autowired
private DepartmentLogService logService;
@Override
@Log
@Transactional(rollbackFor = Exception.class)
public void remove(Integer deptId) throws Exception {
try {
//部署IDに基づいて部署情報を削除
departmentMapper.deleteById(deptId);
//シミュレーション:異常
if (true) {
throw new Exception("異常が発生しました~~~");
}
//部署下の全従業員情報を削除
employeeMapper.deleteByDepartmentId(deptId);
} finally {
//異常是否有りに関わらす、最終的に実行するコード:ログ記録
DepartmentLog log = new DepartmentLog();
log.setCreateTime(LocalDateTime.now());
log.setDescription("部署解散操作を実行しました,此时解散的是" + deptId + "号部署");
//他の業務クラスのメソッドを呼び出す
logService.insert(log);
}
}
}
テスト:SpringBootサービスを再起動し、3号部署を削除后将会发生什么をテストする。
- 3号部署削除操作を実行
- 部署ログ挿入操作を実行
- プログラムがException例外を発生
- トランザクションロールバックを実行(削除、挿入操作が同一トランザクション範囲内にあるため、両方の操作がロールバックされる)
- そしてdepartment_logテーブルにはログデータが記録されていない
原因分析:
- delete操作を実行する时トランザクションを開始した
- insert操作を実行する时、insertで设定されたトランザクション伝播動作はデフォルト値のREQUIREDであり、トランザクションがあれば参加、なければ新規作成を表す
- この場合:deleteとinsert操作は同じトランザクションを使用し、同じトランザクション内の複数の操作はすべて成功するか、すべて失敗するため、例外が発生した时トランザクションをロールバックすると、deleteとinsert操作の両方がロールバックされる
解決策:
DepartmentLogServiceImplクラスのinsertメソッドに@Transactional(propagation = Propagation.REQUIRES_NEW)を追加する。
Propagation.REQUIRES_NEW:トランザクション是否有りに関わらす、常に新規トランザクションを作成し、独立したトランザクションで実行する。
@Service
public class DepartmentLogServiceImpl implements DepartmentLogService {
@Autowired
private DepartmentLogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DepartmentLog log) {
logMapper.insert(log);
}
}
SpringBootサービスを再起動し、もう一度3号部署を削除をテストする。
この场合、DepartmentServiceImplのremoveメソッドが実行される时、トランザクションを開始する。logService.insert(log)を调用する时も新しいトランザクションを作成する。insertメソッドが実行完了した後、トランザクションはすでにコミットされている。外部のトランザクションが例外を発生しても、内部でコミットされたトランザクションはロールバックされない,因為是两个独立的事务。