Kịch bản lỗi
Hãy tưởng tượng bạn đang xây dựng một trang hồ sơ người dùng. Bạn cố gắng lấy thông tin một người dùng cụ thể từ database bằng ID của họ, nhưng bản ghi đó không tồn tại. Nếu code của bạn sử dụng JdbcTemplate như ví dụ dưới đây, ứng dụng của bạn có khả năng vừa bị crash.
User user = jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new Object[]{userId},
userRowMapper
);
Thay vì nhận được phản hồi 404 mượt mà, bạn lại nhận được một stack trace và lỗi 500 Internal Server Error. Điều này thường xảy ra nhất với JdbcTemplate hoặc các truy vấn JPA tùy chỉnh cũ khi tìm kiếm một userId như #404 mà không trả về kết quả nào.
org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
Tại sao lỗi này xảy ra
Phương thức queryForObject rất nghiêm ngặt. Nó hoạt động dựa trên nguyên tắc "phải có đúng một kết quả". Nếu database trả về không có hàng nào, nó không trả về null mà sẽ ném ra một exception (ngoại lệ). Ngược lại, nếu nó tìm thấy hai hàng cho một truy vấn duy nhất, nó sẽ ném lỗi IncorrectResultSizeDataAccessException.
Spring được thiết kế theo cách này để ngăn chặn lỗi NullPointerExceptions ở các lớp service phía sau. Tuy nhiên, trong phát triển web hiện đại, việc thiếu một bản ghi là một tình huống nghiệp vụ phổ biến, không phải là lỗi hệ thống. Việc coi mỗi lần tìm kiếm không thấy dữ liệu là một exception sẽ tạo ra gánh nặng không cần thiết cho ứng dụng của bạn.
Cách khắc phục nhanh: Xử lý JdbcTemplate
Nếu bạn cần tiếp tục sử dụng JdbcTemplate, bạn có thể tránh bị crash bằng hai chiến lược khác nhau.
1. Sử dụng khối Try-Catch
Đây là cách tiếp cận trực tiếp nhất. Nó hoạt động tốt nếu việc thiếu bản ghi thực sự là một trạng thái hiếm gặp và đặc biệt trong logic nghiệp vụ của bạn.
try {
return jdbcTemplate.queryForObject(sql, params, mapper);
} catch (EmptyResultDataAccessException e) {
// Log sự kiện và trả về null hoặc một custom 404 exception
return null;
}
2. Cách tiếp cận bằng Stream (Khuyên dùng)
Một lựa chọn sạch sẽ hơn là sử dụng phương thức query. Vì query trả về một List, nó sẽ chỉ đơn giản trả về một danh sách trống nếu không tìm thấy gì, giúp tránh hoàn toàn exception. Sau đó, bạn có thể sử dụng Java Streams để lấy kết quả đầu tiên.
return jdbcTemplate.query(sql, params, mapper)
.stream()
.findFirst()
.orElse(null);
Cách khắc phục triệt để: Sử dụng Optional với Spring Data JPA
Nếu bạn đang sử dụng Spring Data JPA, bạn nên ngừng trả về các entity thô. Kể từ Spring Data 2.0, repository pattern đã tích hợp sẵn hỗ trợ cho Optional<T>, vốn là tiêu chuẩn công nghiệp để xử lý dữ liệu có thể bị thiếu.
Repository Pattern hiện đại
Cập nhật repository của bạn để trả về một Optional. Điều này buộc code gọi đến phải xác nhận rằng người dùng có thể không tồn tại.
public interface UserRepository extends JpaRepository<User, Long> {
// Phương thức này trả về Optional.empty() thay vì ném ra một exception
Optional<User> findByEmail(String email);
}
Bây giờ logic lớp service của bạn sẽ trở nên dễ đọc hơn nhiều:
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UserNotFoundException("Không tìm thấy người dùng với email: " + email));
Theo mặc định, repository.findById(id) đã trả về một Optional. Hãy sử dụng nó để bỏ qua hoàn toàn EmptyResultDataAccessException kiểu cũ.
Thực hành tốt nhất: Xử lý ngoại lệ tập trung (Global Exception Handling)
Đừng làm bẩn codebase của bạn bằng các khối try-catch giống hệt nhau. Nếu bạn đang làm việc trong một dự án cũ quy mô lớn, hãy xử lý exception tập trung bằng cách sử dụng @RestControllerAdvice. Điều này đảm bảo rằng mỗi khi thiếu một bản ghi, client sẽ nhận được mã trạng thái 404 sạch sẽ thay vì một kết nối bị ngắt quãng.
@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", "Không tìm thấy tài nguyên"));
}
}
Các bước xác minh
Xác minh cách khắc phục của bạn bằng một integration test. Sử dụng @DataJpaTest để kiểm tra một ID không tồn tại, ví dụ như 999L, và đảm bảo rằng Optional là trống.
@Test
void shouldReturnEmptyWhenUserIsMissing() {
Optional<User> result = userRepository.findById(999L);
assertTrue(result.isEmpty());
}
Nếu bạn đã triển khai cách khắc phục cho JdbcTemplate, hãy chạy truy vấn của bạn với một bảng trống. Phương thức bây giờ sẽ trả về null hoặc custom exception của bạn mà không có bất kỳ stack trace nào xuất hiện trong log.

