Spring BootにおけるEmptyResultDataAccessExceptionの解決方法

intermediate Java2026-05-28| Java 8+, Spring Boot 2.x/3.x, Spring Data JPA, JdbcTemplate

Error Message

org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
#spring-boot#java#jpa#error-handling

エラーの発生シナリオ

ユーザープロフィールページを作成している場面を想像してください。IDを指定してデータベースから特定のユーザーを取得しようとしたとき、そのレコードが存在しないことがあります。もし、以下の例のように JdbcTemplate を使用している場合、アプリケーションはおそらくクラッシュしてしまいます。

User user = jdbcTemplate.queryForObject(
    "SELECT * FROM users WHERE id = ?", 
    new Object[]{userId}, 
    userRowMapper
);

スムーズな404レスポンスを返す代わりに、スタックトレースと共に「500 Internal Server Error」が発生します。これは、JdbcTemplate やレガシーなカスタムJPAクエリを使用して、ID「#404」のような存在しないレコードを検索した際によく発生します。

org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0

発生原因

queryForObject メソッドの仕様は厳格です。これは「結果が必ず1つであること」という契約に基づいています。データベースが0行を返した場合、null を返すのではなく、例外をスローします。逆に、一意のクエリに対して2行見つかった場合は、IncorrectResultSizeDataAccessException がスローされます。

Springがこのような設計にしているのは、サービスレイヤーで NullPointerException が発生するのを防ぐためです。しかし、現代のウェブ開発において、レコードが存在しないことは一般的なビジネスケースであり、システムの障害ではありません。すべての検索失敗を例外として扱うことは、アプリケーションのスタックに不要なオーバーヘッドを加えることになります。

クイックフィックス: JdbcTemplateでの対処法

JdbcTemplate を使い続ける必要がある場合は、以下の2つの戦略でクラッシュを回避できます。

1. Try-Catchブロック

これが最も直接的な方法です。レコードの欠落がビジネスロジック上、非常に稀で例外的な状態である場合に適しています。

try {
    return jdbcTemplate.queryForObject(sql, params, mapper);
} catch (EmptyResultDataAccessException e) {
    // イベントをログに記録し、nullまたはカスタムの404例外を返す
    return null; 
}

2. Stream APIによるアプローチ(推奨)

より洗練された代替案は、代わりに query メソッドを使用することです。queryList を返すため、結果が見つからない場合は単に空のリストを返し、例外は発生しません。その後、JavaのStream APIを使用して最初の結果を取得します。

return jdbcTemplate.query(sql, params, mapper)
    .stream()
    .findFirst()
    .orElse(null);

恒久的な対策: Spring Data JPAでOptionalを使用する

Spring Data JPA を使用している場合は、エンティティを直接返すのはやめるべきです。Spring Data 2.0以降、リポジトリパターンは Optional<T> を標準でサポートしており、これはデータの欠落を処理するための業界標準となっています。

モダンなリポジトリパターン

リポジトリが Optional を返すように更新します。これにより、呼び出し側のコードでユーザーが存在しない可能性を考慮することを強制できます。

public interface UserRepository extends JpaRepository<User, Long> {
    // このメソッドは例外をスローする代わりに Optional.empty() を返す
    Optional<User> findByEmail(String email);
}

これにより、サービスレイヤーのロジックが格段に読みやすくなります。

User user = userRepository.findByEmail(email)
    .orElseThrow(() -> new UserNotFoundException("指定されたメールアドレスのユーザーが見つかりません: " + email));

デフォルトで、repository.findById(id) はすでに Optional を返すようになっています。これを利用して、レガシーな EmptyResultDataAccessException を完全に回避しましょう。

ベストプラクティス: グローバル例外ハンドリング

コードベースのあちこちに同じような try-catch ブロックを散布させないでください。大規模なレガシープロジェクトに取り組んでいる場合は、@RestControllerAdvice を使用して例外をグローバルに処理します。これにより、レコードが見つからないたびに、クライアントは接続断ではなく、クリーンな 404 ステータスコードを受け取ることができます。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EmptyResultDataAccessException.class)
    public ResponseEntity<Map<String, String>> handleEmptyResult(EmptyResultDataAccessException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                             .body(Map.of("error", "リソースが見つかりません"));
    }
}

検証ステップ

統合テストで修正を検証します。@DataJpaTest を使用して 999L のような存在しないIDをチェックし、Optional が空であることを確認します。

@Test
void shouldReturnEmptyWhenUserIsMissing() {
    Optional<User> result = userRepository.findById(999L);
    assertTrue(result.isEmpty());
}

JdbcTemplate の修正を適用した場合は、空のテーブルに対してクエリを実行してください。ログにスタックトレースが表示されることなく、メソッドが null またはカスタム例外を返すようになっているはずです。

Related Error Notes