Scalar DBでの例外処理とは

# TL;DR
Scalar DB と Spring Boot を使った API 開発を行う際の例外処理の設計およびその処理方法をまとめてみました。

この記事は、以下の記事の続きになります。Scalar DBとSpring Bootを使ったAPIの開発方法について知りたい方は以下の記事をご覧ください。

[Spring BootとScalar DBを用いたAPIの作り方①](https://qiita.com/yoshi-kinoshita/items/5b2c5c713c9cc2fa2915)
[Spring BootとScalar DBを用いたAPIの作り方②](https://qiita.com/yoshi-kinoshita/items/44363a4aeacf83efcff3)
[Spring BootとScalar DBを用いたAPIの作り方③](https://qiita.com/yoshi-kinoshita/items/1cb6c0b11be5ab087ed6)

# 目次
1. [例外処理の設計](#例外処理の設計)
    1. [Scalar DBでスローされ例外の種類](#scalar-dbでスローされる例外の種類)
    1. [Spring Boot のレイヤーごとに処理するべき例外](#spring-boot-のレイヤーごとに処理するべき例外)
        1. [Repositoryクラスで処理すべき例外](#repositoryクラスで処理すべき例外)
        1. [Serviceクラスで処理すべき例外](#serviceクラスで処理すべき例外)
1. [まとめ](#まとめ)

# 例外処理の設計
## Scalar DBでスローされる例外の種類
Scalar DB は、次の種類の例外をスローします。

- トランザクションの失敗
- トランザクションが失敗したか成功したか不明
- コミットの失敗
- コミットのコンフリクト
- トランザクションの中止処理の失敗
- CRUD処理の失敗
- CRUD処理のコンフリクト

## Spring Boot のレイヤーごとに処理するべき例外
Scalar DB は Bussinessレイヤーでトランザクション処理を、PersistenceレイヤーでCRUD処理を実装します。

このため、CRUD処理に関する例外はPersistenceレイヤーの実装クラスであるRepositoryクラスで、
トランザクションに関する例外はBussinessレイヤーの実装クラスであるServiceクラスで、例外処理を記述します。

### Repositoryクラスで処理すべき例外
Repositoryクラスでは以下の２つに関する例外をキャッチします。

| 例外 | 説明　|
| --- | --- |
| CrudException　|CRUD処理が失敗した場合にスローされる例外|
| CrudConflictException　|CRUD処理のコンフリクトが発生した場合にスローされる例外|

#### Repostoryクラスでの例外処理の実装例
以下の例では、ReposiotryConflictEception と RepositoryCrudException は、RuntimeException をラップしており、例外チェーンを構築しています。これにより、ラップされた例外として処理できます。

``` java:UserRepository.java
public String createUser(DistributedTransaction tx, CreateUserDto createUserDto, String userId) {
  try {
    Key pk = createPk(userId);
    getAndThrowsIfAlreadyExist(tx, createGet(pk));
    Put put =
        new Put(pk)
            .forNamespace(NAMESPACE)
            .forTable(TABLE_NAME)
            .withValue(User.EMAIL, createUserDto.getEmail())
            .withValue(User.COMMON_KEY, COMMON_KEY);
    tx.put(put);
    return userId;
  } catch (CrudConflictException e) {
    throw new RepositoryConflictException(e.getMessage(), e);
  } catch (CrudException e) {
    throw new RepositoryCrudException("Adding User failed", e);
  }
}
```

### Serviceクラスで処理すべき例外
Serviceクラスではキャッチした例外の種類によって、処理を分けていきます。

以下の３つの例外をキャッチした場合は、トランザクションの再実行を行い、一定回数以上失敗する場合はトランザクションを中止させます。

| 例外 | 説明　|
| --- | --- |
| UnKnownTrasactionException |トランザクションのコミットに成功したか失敗したか不明な状態のときにスローされる例外|
| CrudConflictException |CRUD処理のコンフリクトが発生した場合にスローされる例外|
| CommitConflictException |コミットにコンフリクトが発生した場合にスローされる例外|

以下の３つの例外をキャッチした場合は、直ちにトランザクションを中止させます。

| 例外 | 説明　|
| --- | --- |
| TransactionException |トランザクションが失敗したときにスローされる例外|
| CrudException |CRUD処理が失敗した場合にスローされる例外|
| CommitException |コミットに失敗した場合にスローされる例外|

以下の例外をキャッチした場合はログに書き込みを行います。

| 例外 | 説明　|
| --- | --- |
| AbortException |トランザクションの中止処理に失敗した場合にスローされる例外|

#### Serviceクラスでの例外処理の実装例
以下の例では、変数`retryCount`にトランザクションの実行回数を格納し、３回トランザクション処理に失敗した場合に処理を中止しています。

また、トランザクションの再実行に100ミリ秒の間隔をあけています。

``` java:UserService.java
@Service
@Slf4j
public class UserService {
  private final UserRepository userRepository;
  private final DistributedTransactionManager manager;
  private DistributedTransaction tx;

  int retryCount = 0; // トランザクションの実行回数

  @Autowired
  public UserService(
      UserRepository userRepository,
      DistributedTransactionManager manager) {
    this.userRepository = userRepository;
    this.manager = manager;
  }

  public String createUser(CreateUserDto createUserDto) throws InterruptedException {
    while (true) {
      if (retryCount > 0) {
        if (retryCount == 3) { // ３回トランザクションの実行に３回失敗した場合、処理を中止し、RuntimeExceptionをラップした例外に情報を付加してスロー
          throw new ServiceException("An error occurred when adding a user");
        }
        TimeUnit.MILLISECONDS.sleep(100);
      }

      try {
        tx = manager.start();
      } catch (TransactionException e) { // トランザクションのスタートに失敗した場合は、処理を中止し、RuntimeExceptionをラップした例外に情報を付加してスロー
        try {
          tx.abort();
        } catch (AbortException ex) { // トランザクションの中止に失敗した場合は、ログに書き込む
          log.error(ex.getMessage(), ex);
        }
        throw new ServiceException("An error occurred when adding a group", e);
      }

      try {
        String userId = UUID.randomUUID().toString();
        userRepository.createUser(tx, createUserDto, userId);
        tx.commit();
        return userId;
      } catch (CommitConflictException | RepositoryConflictException| UnknownTransactionStatusException e) {
        try { // これらの例外をキャッチした場合は、トランザクションを中止し、retryCountを増やし、トランザクションを再実行させる
          tx.abort();
        } catch (AbortException ex) {
          log.error(ex.getMessage(), ex);
        }
        retryCount++;
      } catch (CommitException | RepositoryCrudException e) {
        try { // これらの例外をキャッチした場合は、直ちにトランザクションを中止させ、RuntimeExceptionをラップした例外に情報を付加してスロー
          tx.abort();
        } catch (AbortException ex) {
          log.error(ex.getMessage(), ex);
        }
        throw new ServiceException("An error occurred when adding a user", e);
      }
    }
  }
}
```
# まとめ
以上が、Scalar DB と Spring Boot を使った API 開発を行う際の例外処理の設計およびその処理方法になります。

ご覧いただいた方のご参考になればと思います。