Spring BootのRepositoryでよくある戻り値の扱いを解説する記事のアイキャッチ。若手エンジニアがJava道場風の学習空間でOptionalやListを学ぶイラスト。

Spring

Spring BootのRepositoryでよくある戻り値の扱い

AIによる要約

Spring BootのRepositoryでは、OptionalList、単一Entityなど戻り値の扱いでレビューされることがあります。大事なのは、0件、1件、複数件を想定し、get()で雑に取り出さないことです。検索条件不足もあわせて確認します。

新人SE
新人SE
Repositoryの戻り値って、とりあえずgetで取り出せばいいんですか?
ポンコツSE
ポンコツSE
それは危ないです。0件の場合、複数件の場合、存在しないことが正常なのか異常なのかを分けて考えます。

Spring Data JPAを使うと、Repositoryのメソッドを簡単に作れます。findByIdfindByEmailのようなメソッド名だけで検索できるため便利です。

一方で、戻り値の扱いを雑にすると、NoSuchElementException、想定外の500エラー、検索条件不足による誤更新につながります。

この記事のポイント

  • Optionalは存在しない可能性を表す
  • get()で雑に取り出さない
  • Listは0件や複数件を前提に扱う
  • 1件だけのつもりなら、DB制約や検索条件も確認する

findByIdはOptionalで返る

Spring Data JPAのfindByIdは、値が存在しない可能性をOptionalで表します。IDを指定しても、削除済み、存在しないID、テストデータ不足などで0件になることがあります。

Optional<User> user = userRepository.findById(userId);

Optionalは、値があるかもしれないし、ないかもしれないという合図です。ここでいきなりget()するのは危険です。

User user = userRepository.findById(userId).get();

存在しないIDだった場合、例外になります。レビューでは、0件の場合にどうする仕様なのかを確認されます。

存在しないことが異常ならorElseThrowを使う

詳細画面や更新処理で、対象データが存在しないことが異常なら、明示的な例外に変換します。

User user = userRepository.findById(userId)
        .orElseThrow(() -> new UserNotFoundException(userId));

この書き方なら、存在しない場合に何が起きるのかが読み手に伝わります。ControllerAdviceなどで例外をHTTP 404へ変換する設計にもつなげやすくなります。

存在しないことが正常なら分岐する

検索条件に該当しないことが正常な画面もあります。たとえば、任意のメールアドレスでユーザーを検索する画面では、0件はエラーではなく「見つかりません」かもしれません。

Optional<User> user = userRepository.findByEmail(email);

if (user.isEmpty()) {
    return UserSearchResult.notFound();
}

return UserSearchResult.found(UserResponse.from(user.get()));

この場合は、存在しないことを業務上どう扱うかをServiceで表します。単に例外にするのではなく、仕様に合わせます。

Listは0件が普通にある

findByStatusfindByCustomerIdのように複数件を返す検索では、戻り値はListになることが多いです。ここでは0件も正常な結果として扱います。

List<Order> orders = orderRepository.findByCustomerId(customerId);

if (orders.isEmpty()) {
    return List.of();
}

return orders.stream()
        .map(OrderResponse::from)
        .toList();

0件を例外にするのか、空リストとして返すのかは仕様次第です。ただし、画面の一覧検索では空リストを返す方が自然なことが多いです。

1件だけのつもりなら検索条件を確認する

Repositoryメソッドで単一EntityやOptionalを返す場合、その検索条件で本当に1件に絞れるのか確認します。

Optional<User> findByEmail(String email);

メールアドレスがDB上でユニーク制約になっているなら、1件検索として自然です。しかし、DB制約がない場合、同じメールアドレスが複数件ある可能性があります。

CREATE UNIQUE INDEX uk_users_email
ON users(email);

1件だけ取れる前提で実装するなら、DBのユニーク制約、業務仕様、検索条件を確認しましょう。

LIMIT 1でごまかさない

複数件返って困るからといって、安易にLIMIT 1で1件だけ取るのは危険です。どの1件が選ばれるかで業務結果が変わる可能性があります。

SELECT *
FROM users
WHERE email = 'test@example.com'
LIMIT 1;

このSQLは、重複データがある問題を隠しているだけかもしれません。現場では、検索条件不足なのか、データ不整合なのか、仕様として複数件あり得るのかを確認します。

existsByやcountByを使う場面

データの中身が必要なく、存在チェックだけしたい場合は、existsByを使うと意図が明確になります。

boolean exists = userRepository.existsByEmail(email);

if (exists) {
    throw new DuplicateEmailException(email);
}

件数が必要な場合はcountByを使います。ただし、件数を見てから登録する処理では、同時実行の問題もあるため、最終的にはDB制約も必要です。

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

// レビューコメント例
// findByIdの戻り値に対してget()を直接呼んでいます。
// 対象データが存在しない場合の仕様が分からないため、
// orElseThrowで明示するか、0件時の扱いをServiceで分岐してください。

Repositoryの戻り値は、ただの型の話ではありません。0件、1件、複数件をどう扱うかという業務仕様の話につながります。

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

Repository戻り値の確認リスト

  • Optional.get()を直接呼んでいないか
  • 0件が正常か異常か説明できるか
  • 複数件あり得る検索なのに1件前提にしていないか
  • 1件検索ならDB制約や業務キーを確認したか
  • 存在チェックだけならexistsByで十分ではないか

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

まとめ

Repositoryの戻り値を見るときは、型だけでなく、0件、1件、複数件の業務上の意味を考えます。OptionalListを丁寧に扱うだけで、現場で起きやすい検索ミスや想定外エラーを減らせます。

-Spring
-, , , , ,