Specification và JPA Auditing: Truy Vấn Động và Nhật Ký
Xây dựng dynamic filter với Specification Criteria API và tự động ghi nhận thời gian createdAt, updatedAt qua cơ chế JPA Auditing.
TL;DR: Specification<T> bọc quanh JPA Criteria API cho phép kết hợp các điều kiện lọc động (and/or) tại runtime mà không làm bùng nổ số lượng method repository. Trong khi đó, JPA Auditing tự động ghi nhận createdAt, updatedAt, createdBy bằng cách can thiệp vào lifecycle callbacks (@PrePersist, @PreUpdate) thông qua AuditingEntityListener.
Vì sao bộ lọc động không thể viết bằng Derived Query?
Giả sử màn hình quản trị cung cấp 5 tiêu chí lọc tùy chọn (optional): status, createdAfter, createdBefore, minPriority, keyword. Người dùng có thể nhập bất kỳ trường nào hoặc bỏ trống:
- Nếu dùng Derived Query, số lượng method cần viết theo tổ hợp là $2^5 = 32$ method — hoàn toàn bất khả thi.
- Nếu dùng
@QueryJPQL tĩnh, chuỗi truy vấn không thể linh hoạt tự gọt bớt các mệnh đềAND field = :paramkhi tham số truyền vào bằngnull.
Giải pháp: Sử dụng Specification<T> để dựng cây điều kiện tại thời điểm runtime.
Interface Specification hoạt động như thế nào?
Specification<T> là một functional interface chuẩn của Spring Data JPA:
@FunctionalInterface
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
Ba tham số nòng cốt:
Root<T> root: Đại diện cho Entity gốc, dùng để trỏ tới các thuộc tính (root.get("status")).CriteriaQuery<?> query: Đại diện cho toàn bộ câu lệnh (hỗ trợDISTINCThoặc subquery).CriteriaBuilder cb: Nhà máy sản xuất điều kiệnPredicate(cb.equal(),cb.like(),cb.greaterThan()).
Khi tham số đầu vào bằng null, hàm trả về null. Phương thức and() nội bộ của Spring Data sẽ tự động bỏ qua các nhánh null này:

Triển khai Specification trong thực tế ra sao?
Bước 1: Mở rộng JpaSpecificationExecutor trong Repository
public interface ProjectRepository extends JpaRepository<Project, Long>,
JpaSpecificationExecutor<Project> {
// Ke thua findAll(Specification, Pageable)
}
Bước 2: Tạo lớp Factory chứa các Specification tái sử dụng
public class ProjectSpecs {
public static Specification<Project> hasStatus(ProjectStatus status) {
return status == null ? null : (root, q, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<Project> createdAfter(Instant from) {
return from == null ? null : (root, q, cb) -> cb.greaterThan(root.get("createdAt"), from);
}
public static Specification<Project> nameContains(String fragment) {
return fragment == null ? null : (root, q, cb) ->
cb.like(cb.lower(root.get("name")), "%" + fragment.toLowerCase() + "%");
}
}
Bước 3: Kết hợp các điều kiện tại tầng Service
@Service
@RequiredArgsConstructor
public class ProjectService {
private final ProjectRepository repo;
public Page<Project> search(ProjectFilter filter, Pageable pageable) {
Specification<Project> spec = Specification.where(ProjectSpecs.hasStatus(filter.status()))
.and(ProjectSpecs.createdAfter(filter.createdAfter()))
.and(ProjectSpecs.nameContains(filter.nameContains()));
return repo.findAll(spec, pageable);
}
}
Câu lệnh SQL phát sinh sẽ tự động co giãn: chỉ những trường có giá trị mới xuất hiện trong mệnh đề WHERE.
Cơ chế JPA Auditing tự động lưu vết thời gian như thế nào?
Mọi hệ thống chuyên nghiệp đều yêu cầu hai mốc thời gian: createdAt (thời điểm tạo, bất biến) và updatedAt (thời điểm cập nhật mới nhất). Thay vì viết setter thủ công rải rác khắp các service, ta sử dụng JPA Auditing.
Khi kích hoạt @EnableJpaAuditing, Spring Data đăng ký AuditingEntityListener hook trực tiếp vào các JPA Lifecycle Callbacks:

- Khi lưu Entity mới (
@PrePersist): Tự động điền cảcreatedAtvàupdatedAt. - Khi cập nhật Entity (
@PreUpdate): Chỉ cập nhật lại trườngupdatedAt.
Cấu hình Base Entity chuẩn mực
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseAuditableEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
}
@Configuration
@EnableJpaAuditing
public class JpaConfig {
// Kich hoat bo lang nghe Auditing cua Spring Data JPA
}
Thuộc tính updatable = false trên @Column(createdAt) ngăn chặn triệt để Hibernate sinh lệnh UPDATE ... SET created_at = ?, bảo đảm mốc thời gian khởi tạo không bao giờ bị ghi đè do nhầm lẫn mã nguồn.
Những cạm bẫy phổ biến cần tránh là gì?
- Quên kế thừa JpaSpecificationExecutor: Khiến Repository không có các phương thức nạp
Specification, gây lỗi biên dịch. - Tìm kiếm LIKE không tận dụng được Index: Biểu thức
cb.like(..., "%" + keyword + "%")chứa ký tự đại diện ở đầu sẽ gây full-table scan trên database. Với dữ liệu lớn, cần cân nhắc Full-Text Search. - Quên @EntityListeners trên Base Class: Làm cho
createdAtvàupdatedAtluôn bịnullsau khi gọi lệnh lưu dữ liệu.
Tự kiểm tra kiến thức
- Q1Tại sao Specification lại giải quyết hiệu quả bài toán lọc dữ liệu với nhiều trường tùy chọn (optional filters)?
- Q2Interface nào bắt buộc phải được khai báo thêm trong Repository để sử dụng được Specification với phân trang?
- Q3Cơ chế nào bên dưới giúp JPA Auditing tự động điền giá trị cho các trường @CreatedDate và @LastModifiedDate?
- Q4Tại sao nên thêm thuộc tính 'updatable = false' vào annotation @Column của trường createdAt?
- Q5Nếu ứng dụng có @EnableJpaAuditing nhưng trường createdAt vẫn bị null sau khi lưu bản ghi, nguyên nhân phổ biến nhất là gì?
Bài tiếp theo: Pageable, Sort & Cú Pháp SQL Phân Trang
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
@Query, @Modifying và Projection: Tùy Biến Truy Vấn Dữ Liệu
Bài tiếpPageable và Sort: Phân Trang và Sắp Xếp Dữ Liệu Chuẩn Mực
Lộ trình Junior Java Backend Developer
Chặng 16/21 · Spring Data JPA — bài 11/20 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này