エラーの発生シナリオ
ユーザープロフィールページを作成している場面を想像してください。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 メソッドを使用することです。query は List を返すため、結果が見つからない場合は単に空のリストを返し、例外は発生しません。その後、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 またはカスタム例外を返すようになっているはずです。

