Spring

Spring Bootの@Transactionalはどこに付ける?

AIによる要約

Spring Bootの@Transactionalは、基本的にRepositoryではなく、業務処理のまとまりを表すServiceメソッドに付けることが多いです。どこからどこまでを1つの更新単位にするかを決めるためのアノテーションであり、何となく付けるものではありません。

新人SE
新人SE
@Transactionalって、Repositoryに付けておけばDB更新は安全になりますか?
ポンコツSE
ポンコツSE
Repository単体ではなく、業務処理のまとまりで考えます。複数の更新を1つの成功・失敗として扱いたいなら、Serviceに付けるのが基本です。

Spring BootでDB更新を扱うと、@Transactionalというアノテーションが出てきます。

初学者は、とりあえずRepositoryや更新メソッドに付ければよいと考えがちですが、現場では「どこからどこまでを1つのトランザクションにするか」がレビューで見られます。

この記事のポイント

  • @Transactionalは更新処理の境界を決める
  • 基本は業務処理のまとまりであるServiceに付ける
  • ControllerやRepositoryに何となく付けない
  • 例外時にどこまでロールバックしたいかを考える

@Transactionalは何をするものか

@Transactionalは、DB更新を1つのまとまりとして扱うためのアノテーションです。処理の途中で例外が起きた場合、途中までの更新をなかったことにする、という考え方につながります。

@Transactional
public void register(UserRegisterRequest request) {
    User user = userRepository.save(new User(request.getName()));
    profileRepository.save(new Profile(user.getId(), request.getBio()));
}

この例では、ユーザー登録とプロフィール登録を1つのまとまりとして扱いたいです。プロフィール登録で失敗した場合、ユーザーだけ登録されると中途半端な状態になります。

基本はServiceに付ける

現場では、@TransactionalをServiceの更新メソッドに付けることが多いです。理由は、Serviceが業務処理のまとまりを表す場所だからです。

@Service
public class UserService {
    private final UserRepository userRepository;
    private final ProfileRepository profileRepository;

    @Transactional
    public void register(UserRegisterRequest request) {
        User user = userRepository.save(new User(request.getName()));
        profileRepository.save(new Profile(user.getId(), request.getBio()));
    }
}

ControllerはHTTPの入口、RepositoryはDBアクセスです。複数のRepositoryを呼び出して1つの業務処理を完成させるのはServiceの役割です。

Repositoryに付ければよいわけではない

RepositoryはDBアクセスの部品です。1回の保存や検索だけを見ればRepositoryでもよさそうに見えますが、業務処理はRepository呼び出し1回で終わらないことがあります。

public void register(UserRegisterRequest request) {
    userRepository.save(user);
    profileRepository.save(profile);
    pointRepository.save(firstLoginPoint);
}

この3つの保存を1つの成功・失敗として扱いたいなら、個々のRepositoryではなく、この処理全体を囲むServiceに@Transactionalを付ける方が自然です。

Controllerに付けるのは避ける

Controllerに@Transactionalを付けると、HTTPリクエスト全体をトランザクションとして扱う形になります。画面/APIの入口とDB更新の境界が混ざり、責務が分かりにくくなります。

@PostMapping("/users")
@Transactional
public ResponseEntity<Void> register(@RequestBody UserRegisterRequest request) {
    userService.register(request);
    return ResponseEntity.ok().build();
}

この書き方は、Controllerがトランザクション境界まで持ってしまっています。通常はService側へ移した方がレビューで通りやすいです。

readOnlyを使う場面

検索だけの処理では、@Transactional(readOnly = true)を使う現場もあります。読み取り専用であることを明示し、更新処理との区別を付けるためです。

@Transactional(readOnly = true)
public UserDetailResponse findDetail(Long userId) {
    User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
    return UserDetailResponse.from(user);
}

ただし、すべての検索に必ず付けるかどうかはプロジェクト方針によります。既存コードの書き方に合わせましょう。

ロールバックされる例外を意識する

トランザクションでは、例外が起きたときにロールバックするかどうかも重要です。Springでは、基本的に実行時例外が起きるとロールバックされる、と理解しておくとよいです。

@Transactional
public void cancel(Long orderId) {
    Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));

    order.cancel();

    if (order.isInvalidState()) {
        throw new CannotCancelOrderException(orderId);
    }
}

新人のうちは、例外を握りつぶすコードに注意します。例外をcatchして何もしないと、失敗したのに正常終了した扱いになり、意図したロールバックにならないことがあります。

@Transactional
public void importCsv(File file) {
    try {
        importService.importFile(file);
    } catch (Exception e) {
        log.error("CSV取込に失敗しました", e);
    }
}

この例では、例外をcatchして外へ出していません。仕様として続行するなら別ですが、失敗時にロールバックしたい処理なら危険です。

同じクラス内の呼び出しに注意

少し現場寄りの注意点として、同じクラス内で@Transactional付きメソッドを呼ぶと、期待通り効かないことがあります。Springの仕組み上、プロキシ経由で呼ばれることが前提になるためです。

public void registerAll(List<UserRequest> requests) {
    for (UserRequest request : requests) {
        registerOne(request);
    }
}

@Transactional
public void registerOne(UserRequest request) {
    userRepository.save(new User(request.getName()));
}

新人のうちは深追いしすぎなくてよいですが、@Transactionalを付けたのに効かないときの典型パターンとして覚えておくと役立ちます。

現場レビューではどう見られるか

// レビューコメント例
// ユーザー登録とプロフィール登録が別々の更新になっています。
// 片方だけ成功するとデータ不整合になるため、
// Serviceのregisterメソッドに@Transactionalを付けて1つの更新単位にしてください。

レビューでは、アノテーションが付いているかだけでなく、更新単位が業務的に正しいかを見られます。

レビュー前のセルフチェック

@Transactionalの確認リスト

  • どこからどこまでを1つの更新単位にしたいか説明できるか
  • ControllerではなくServiceに付けているか
  • 複数Repository更新が途中で失敗したときの状態を考えたか
  • 例外を握りつぶしてロールバックを妨げていないか
  • 既存プロジェクトの@Transactional方針に合わせているか

この記事とあわせて読みたい

まとめ

@Transactionalは、何となくDB更新に付けるものではありません。業務処理のまとまりを考え、基本はServiceの更新メソッドに付けます。途中で失敗したときにどこまで戻したいかを説明できるようにしておきましょう。

-Spring
-, , , , , ,