Repository abstraction — JpaRepository, derived queries, @Query
Spring Data JPA sinh implementation từ interface. Bài này bóc 3 tier method: built-in (JpaRepository), derived query (method name parsing), @Query (JPQL/native), dynamic query qua Specification, projection DTO, modifying queries, và Pageable.
Bài 02 đã chỉ ra entity mapping. Bài này bóc tầng tiếp theo: làm sao truy vấn entity? Spring Data JPA cung cấp 3 tier method abstraction — chọn đúng theo độ phức tạp query.
1. JpaRepository — interface hierarchy
flowchart TB
Repository["Repository<T, ID><br/>marker"]
CrudRepository["CrudRepository<T, ID><br/>save, find, delete"]
PagingAndSortingRepository["PagingAndSortingRepository<T, ID><br/>+ Pageable, Sort"]
JpaRepository["JpaRepository<T, ID><br/>+ flush, batch, EntityManager"]
Repository --> CrudRepository
CrudRepository --> PagingAndSortingRepository
PagingAndSortingRepository --> JpaRepository
style JpaRepository fill:#fef3c7| Interface | Methods |
|---|---|
Repository | Marker — empty |
CrudRepository | save, saveAll, findById, findAll, count, existsById, delete, deleteById, deleteAll |
PagingAndSortingRepository | + findAll(Pageable), findAll(Sort) |
JpaRepository | + flush, saveAndFlush, deleteInBatch, getReferenceById, findAll(Example) |
99% use case dùng JpaRepository — superset của tất cả.
public interface ProjectRepository extends JpaRepository<Project, Long> {
// Inherit ~20 method built-in
}
@Service
public class ProjectService {
private final ProjectRepository repo;
public Project create(Project p) {
return repo.save(p); // INSERT
}
public Optional<Project> findById(Long id) {
return repo.findById(id); // SELECT
}
public List<Project> findAll() {
return repo.findAll(); // SELECT (no LIMIT — careful)
}
public Page<Project> page(Pageable page) {
return repo.findAll(page); // SELECT with LIMIT/OFFSET
}
public boolean exists(Long id) {
return repo.existsById(id); // SELECT 1
}
public void delete(Long id) {
repo.deleteById(id); // DELETE
}
public long count() {
return repo.count(); // SELECT COUNT(*)
}
}
2. Derived queries — method name parsing
Khi built-in không đủ, define method theo grammar:
public interface ProjectRepository extends JpaRepository<Project, Long> {
// Single field
Optional<Project> findByName(String name);
List<Project> findByStatus(ProjectStatus status);
long countByStatus(ProjectStatus status);
boolean existsByName(String name);
// Multiple field
List<Project> findByStatusAndCreatedAtAfter(ProjectStatus status, Instant since);
Optional<Project> findByNameOrSlug(String name, String slug);
// Comparison
List<Project> findByCreatedAtBefore(Instant before);
List<Project> findByCreatedAtBetween(Instant start, Instant end);
List<Project> findByPriorityGreaterThanEqual(int min);
// String matching
List<Project> findByNameContaining(String fragment); // LIKE %?%
List<Project> findByNameStartingWith(String prefix); // LIKE ?%
List<Project> findByNameEndingWith(String suffix); // LIKE %?
List<Project> findByNameContainingIgnoreCase(String fragment);
// Collection
List<Project> findByStatusIn(Collection<ProjectStatus> statuses);
List<Project> findByIdIn(List<Long> ids);
// Null check
List<Project> findByDescriptionIsNull();
List<Project> findByDescriptionIsNotNull();
// Sort + Limit
List<Project> findTop10ByOrderByCreatedAtDesc();
List<Project> findFirst5ByStatusOrderByName(ProjectStatus status);
// Distinct
List<Project> findDistinctByStatus(ProjectStatus status);
// Modify (DELETE, UPDATE) — require @Modifying + @Transactional
@Modifying
@Transactional
int deleteByStatus(ProjectStatus status);
}
Spring parse method name lúc startup, build JPQL. Verify bằng cách bật log:
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
2.1 Grammar đầy đủ
[verb] [Distinct]? [Top|First N]? [subject] By [property] [keyword] ... [OrderBy ...]
| Keyword | SQL |
|---|---|
Equals, no keyword | = |
Not | != |
LessThan, LessThanEqual | <, <= |
GreaterThan, GreaterThanEqual | >, >= |
Between | BETWEEN ? AND ? |
In, NotIn | IN (...), NOT IN (...) |
Like, NotLike | LIKE, NOT LIKE |
Containing, StartingWith, EndingWith | LIKE %?%, LIKE ?%, LIKE %? |
IgnoreCase | LOWER(field) = LOWER(?) |
True, False | = true, = false |
IsNull, IsNotNull | IS NULL, IS NOT NULL |
OrderBy | ORDER BY ? |
2.2 Khi derived query không đủ
3 case fail:
- Method name quá dài (>5 condition) — không readable.
- Aggregate function (SUM, AVG): không support.
- JOIN multiple table với projection: phức tạp.
→ Switch sang @Query.
3. @Query annotation — JPQL/native
public interface ProjectRepository extends JpaRepository<Project, Long> {
// JPQL
@Query("SELECT p FROM Project p WHERE p.status = :status AND p.createdAt > :since")
List<Project> findRecent(@Param("status") ProjectStatus status,
@Param("since") Instant since);
// Native SQL
@Query(value = "SELECT * FROM projects WHERE status = :status",
nativeQuery = true)
List<Project> findByStatusNative(@Param("status") String status);
// Aggregate
@Query("SELECT p.status, COUNT(p) FROM Project p GROUP BY p.status")
List<Object[]> countByStatus();
// With JOIN
@Query("SELECT p FROM Project p JOIN p.tasks t WHERE t.priority = :priority")
List<Project> findWithHighPriorityTasks(@Param("priority") TaskPriority priority);
// SpEL
@Query("SELECT p FROM Project p WHERE p.createdAt > ?#{#since} AND p.id < ?1")
List<Project> findRecentBefore(Long beforeId, @Param("since") Instant since);
}
3.1 Named parameters vs positional
// Positional — ?1, ?2, ?3
@Query("SELECT p FROM Project p WHERE p.status = ?1 AND p.name = ?2")
List<Project> find1(ProjectStatus status, String name);
// Named — :name (recommended)
@Query("SELECT p FROM Project p WHERE p.status = :status AND p.name = :name")
List<Project> find2(@Param("status") ProjectStatus status,
@Param("name") String name);
Recommend named:
- Refactor-friendly (rename field không impact).
- Self-documenting.
- Java 8+ với
-parametersflag không cần@Param.
3.2 Native query
@Query(value = """
SELECT p.* FROM projects p
WHERE p.created_at > :since
AND p.status = :status
AND EXISTS (SELECT 1 FROM tasks t WHERE t.project_id = p.id AND t.priority = 'HIGH')
ORDER BY p.created_at DESC
LIMIT 10
""",
nativeQuery = true)
List<Project> findComplexNative(@Param("since") Instant since,
@Param("status") String status);
Khi nào dùng native:
- DB-specific feature: Postgres array, JSONB query, full-text search, window function.
- Performance critical: hand-tuned SQL.
- Migration legacy: SQL có sẵn từ DBA.
Trade-off:
- Lock vào DB vendor.
- Result mapping: trả
Object[]hoặc dùng@SqlResultSetMapping. - Không type-safe (string SQL).
4. Modifying queries
public interface ProjectRepository extends JpaRepository<Project, Long> {
@Modifying
@Transactional
@Query("UPDATE Project p SET p.status = :newStatus WHERE p.status = :oldStatus")
int bulkUpdateStatus(@Param("oldStatus") ProjectStatus oldStatus,
@Param("newStatus") ProjectStatus newStatus);
@Modifying
@Transactional
@Query("DELETE FROM Project p WHERE p.archivedAt < :cutoff")
int deleteOldArchived(@Param("cutoff") Instant cutoff);
}
@Modifying cho UPDATE/DELETE. @Transactional cần thiết — modify yêu cầu tx.
Cảnh báo: modifying query không tương tác persistence context — Hibernate không update entity managed. Sau modifying:
@Transactional
public void example() {
Project p = repo.findById(42L).orElseThrow();
System.out.println(p.getStatus()); // ACTIVE
repo.bulkUpdateStatus(ACTIVE, ARCHIVED);
System.out.println(p.getStatus()); // VAN ACTIVE — entity stale
repo.flush();
em.refresh(p); // force reload
System.out.println(p.getStatus()); // ARCHIVED
}
@Modifying(clearAutomatically = true) clear persistence context sau modify. Hoặc avoid mix entity load + bulk modify trong cùng tx.
5. Projection — return shape khác entity
Khi cần subset field hoặc DTO khác entity, 4 pattern projection:
5.1 Interface projection (closed)
public interface ProjectSummary {
Long getId();
String getName();
ProjectStatus getStatus();
}
public interface ProjectRepository extends JpaRepository<Project, Long> {
List<ProjectSummary> findByStatus(ProjectStatus status);
}
Spring Data sinh proxy implement interface, populate qua entity.
Pros: declarative, type-safe. Cons: Hibernate vẫn load full entity rồi map → no SQL optimization.
5.2 DTO projection (constructor expression)
public record ProjectSummary(Long id, String name, ProjectStatus status, long taskCount) {}
@Query("""
SELECT new com.olhub.dto.ProjectSummary(
p.id, p.name, p.status, COUNT(t.id)
)
FROM Project p LEFT JOIN p.tasks t
WHERE p.status = :status
GROUP BY p.id, p.name, p.status
""")
List<ProjectSummary> findSummaryByStatus(@Param("status") ProjectStatus status);
Map row trực tiếp → DTO. Performance tốt nhất — chỉ select fields cần.
Pros: SQL select chỉ field cần, no entity overhead, no LazyInit risk. Cons: verbose, full path class trong JPQL.
5.3 Class-based projection
public class ProjectSummaryClass {
private final Long id;
private final String name;
public ProjectSummaryClass(Long id, String name) {
this.id = id;
this.name = name;
}
// getters
}
public interface ProjectRepository extends JpaRepository<Project, Long> {
List<ProjectSummaryClass> findByStatus(ProjectStatus status);
}
Auto-detect bởi Spring Data — match field name. Less idiomatic.
5.4 Open projection với SpEL
public interface ProjectVerbose {
Long getId();
String getName();
@Value("#{target.name + ' (' + target.status + ')'}")
String getDisplayName();
}
SpEL access target entity field, compute. Phức tạp — hiếm dùng.
5.5 Recommendation
| Case | Approach |
|---|---|
| Subset field từ entity | Interface projection (closed) |
| Aggregate (count, sum) | DTO projection với JPQL |
| Full DTO transform | DTO projection với JPQL |
| Computed field | Open projection với SpEL |
Default 2026: DTO projection cho list/page (performance + clarity). Entity cho single get + modify.
6. Pageable + Sort
Module 03 đã giới thiệu Pageable. Module 04 đào sâu DB integration:
public interface ProjectRepository extends JpaRepository<Project, Long> {
// Default page method
Page<Project> findAll(Pageable pageable);
// Custom với filter
Page<Project> findByStatus(ProjectStatus status, Pageable pageable);
// With @Query
@Query("SELECT p FROM Project p WHERE p.createdAt > :since")
Page<Project> findRecent(@Param("since") Instant since, Pageable pageable);
}
@Service
public class ProjectService {
public Page<ProjectDto> list(ProjectStatus status, Pageable pageable) {
Page<Project> page = repo.findByStatus(status, pageable);
return page.map(ProjectDto::from);
}
}
@RestController
public class ProjectController {
@GetMapping
public Page<ProjectDto> list(
@RequestParam(required = false) ProjectStatus status,
Pageable pageable // Spring auto-bind từ ?page=&size=&sort=
) {
return service.list(status, pageable);
}
}
Request: GET /projects?page=0&size=20&sort=createdAt,desc.
Pageable resolve qua PageableHandlerMethodArgumentResolver (Spring Data Web).
6.1 SQL generated
Pageable translate sang LIMIT/OFFSET (Postgres):
SELECT p.* FROM projects p WHERE p.status = 'ACTIVE'
ORDER BY p.created_at DESC
LIMIT 20 OFFSET 0;
-- Spring Data also runs:
SELECT COUNT(*) FROM projects p WHERE p.status = 'ACTIVE';
2 SQL: 1 SELECT + 1 COUNT (cho Page.totalElements).
6.2 Slice — bỏ COUNT cho performance
Slice<Project> findByStatus(ProjectStatus status, Pageable pageable);
Slice không count total — chỉ check có "next" page hay không. Performance tốt hơn cho infinite scroll UI.
6.3 Cursor pagination — KeysetScrollPosition
Spring Data 3.1+ thêm cursor-based pagination:
Window<Project> findFirst20By(ScrollPosition position);
Use case: pagination với data thay đổi nhanh (offset thay đổi sai). Nhưng phức tạp hơn — bài 07 đào sâu.
7. Specification — dynamic query
Khi cần build query dynamic từ user input (filter UI):
public interface ProjectRepository extends JpaRepository<Project, Long>, JpaSpecificationExecutor<Project> {
}
@Service
public class ProjectService {
public Page<Project> search(ProjectFilter filter, Pageable page) {
Specification<Project> spec = Specification.where(null);
if (filter.status() != null) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("status"), filter.status()));
}
if (filter.createdAfter() != null) {
spec = spec.and((root, query, cb) ->
cb.greaterThan(root.get("createdAt"), filter.createdAfter()));
}
if (filter.nameContains() != null) {
spec = spec.and((root, query, cb) ->
cb.like(cb.lower(root.get("name")), "%" + filter.nameContains().toLowerCase() + "%"));
}
return repo.findAll(spec, page);
}
}
Specification = JPA Criteria API wrapped. Type-safe (no string SQL), refactor-friendly.
Pros:
- Dynamic — combine condition runtime.
- Type-safe.
- Reuse: extract specification thành method.
Cons:
- Verbose (Criteria API lambda).
- Steeper learning curve.
Alternatives:
- QueryDSL: simpler API. Thêm dependency.
- JPA Criteria + EntityManager: raw API.
- JOOQ: SQL-first, type-safe, không JPA.
Module 09 sẽ giới thiệu QueryDSL khi cần.
8. Auditing — @CreatedDate, @LastModifiedDate
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@CreatedBy
@Column(name = "created_by", updatable = false, length = 50)
private String createdBy;
@LastModifiedBy
@Column(name = "updated_by", length = 50)
private String updatedBy;
}
@Entity
public class Project extends AuditableEntity {
@Id @GeneratedValue Long id;
String name;
// createdAt, updatedAt inherit
}
Spring Data tự set timestamp khi persist/update. Cho @CreatedBy/@LastModifiedBy cần AuditorAware:
@Component
public class SecurityAuditor implements AuditorAware<String> {
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(Authentication::getName);
}
}
9. Vận hành production — slow query, native query security, soft-delete
Repository là abstraction layer mỏng — sai 1 query là DB tốn tiền hoặc bottleneck production. Section này cover monitoring + security + audit pattern enterprise.
9.1 Slow query detection — log threshold
spring:
jpa:
properties:
hibernate:
session:
events:
log:
LOG_QUERIES_SLOWER_THAN_MS: 100
Boot 3.4+ + Hibernate 6.6 có built-in slow query log. Output:
WARN o.h.SQL_SLOW: SlowQuery: 250 milliseconds. SQL: SELECT ... FROM orders WHERE ...
Pipe sang ELK + alert spike. Threshold theo SLA app — typical 100ms cho REST endpoint.
9.2 Hibernate metrics — Micrometer
spring.jpa.properties.hibernate.generate_statistics: true
management.metrics.enable.hibernate: true
| Metric | Use case |
|---|---|
hibernate.queries.executed | Total query rate per repo |
hibernate.entities.loaded | Detect lazy load thrashing |
hibernate.query.cache.hit.ratio | 2nd level cache efficiency |
hibernate.flushes | Dirty checking overhead |
Alert N+1: hibernate.collections.fetched / http.requests vượt 10 (Module 04 bài 04 cover).
9.3 Native query — SQL injection cảnh báo
@Query(nativeQuery = true) với param concatenation = SQL injection.
// VULNERABLE — string concat
@Query(value = "SELECT * FROM orders WHERE status = '" + status + "'", nativeQuery = true)
// SAFE — Spring binds parameter
@Query(value = "SELECT * FROM orders WHERE status = :status", nativeQuery = true)
List<Order> findByStatus(@Param("status") String status);
CI rule: grep pattern @Query.*\+.* cho native query → fail PR. Hoặc lint với SonarQube rule S3649.
9.4 Soft-delete pattern enterprise
Production rule: không hard-delete. Add column deleted_at:
@Entity
@SQLRestriction("deleted_at IS NULL") // Hibernate 6.3+, query auto filter
@SQLDelete(sql = "UPDATE orders SET deleted_at = NOW() WHERE id = ?")
public class Order extends Auditable {
Instant deletedAt;
}
repo.delete(order) chạy soft-delete SQL. Audit trail đầy đủ. Rollback dễ dàng (set deleted_at = NULL). Compliance GDPR (right to be forgotten) — separate purge job sau retention period.
9.5 Failure runbook
Mode 1 — Slow query spike:
- Diagnose:
hibernate.queries.executedrate per repository, slow query log. - Tool:
EXPLAIN ANALYZEPostgres → missing index hoặc full table scan. - Remediate: thêm index, refactor query, dùng DTO projection (Module 04 bài 04).
Mode 2 — Repository method name typo:
- Triệu chứng: app fail tại startup, "No property X found".
- Diagnose: Spring parse method name fail.
- Remediate: rename method correctly,
mvn clean installvalidate.
Mode 3 — @Modifying không update persistence context:
- Triệu chứng: bulk update OK nhưng entity in cache stale → query subsequent trả old value.
- Remediate:
@Modifying(clearAutomatically = true, flushAutomatically = true).
Mode 4 — Specification dynamic filter slow:
- Diagnose:
Specificationgenerate query không index — vd LIKE%pattern%full text scan. - Remediate: whitelist filter field + ensure index. Dùng full-text search engine (Postgres
tsvectorhoặc Elasticsearch) cho text search.
10. Pitfall tổng hợp
❌ Nhầm 1: Method name typo, fail tại runtime.
List<Project> findByCustmer(String c); // typo "Custmer"
Spring throw startup error. Verify khi run tests.
❌ Nhầm 2: @Query JPQL nhầm SQL.
@Query("SELECT * FROM projects WHERE id = ?1") // SQL — fail JPQL
✅ JPQL: SELECT p FROM Project p WHERE p.id = ?1. Hoặc nativeQuery = true.
❌ Nhầm 3: Modifying query không @Transactional.
@Modifying
@Query("UPDATE Project p SET p.status = ?1")
int updateAll(ProjectStatus s); // throw TransactionRequiredException
✅ Add @Transactional (method hoặc service level).
❌ Nhầm 4: findAll() cho table 1M record.
List<Project> all = repo.findAll(); // OOM
✅ Pageable hoặc Stream.
❌ Nhầm 5: Mix @Modifying + load entity trước.
Project p = repo.findById(42L).get();
repo.bulkUpdate(...); // p stale, persistence context mismatch
✅ Refresh entity hoặc clear context: @Modifying(clearAutomatically = true).
❌ Nhầm 6: Interface projection cho large list.
List<ProjectSummary> findByStatus(...); // load full entity, map
✅ Use DTO projection với JPQL SELECT new ... cho performance.
❌ Nhầm 7: Native query Postgres-specific deploy MySQL. ✅ Test compatibility hoặc switch JPQL.
11. 📚 Deep Dive Spring Reference
Spring Data JPA:
- Spring Data JPA — Repositories — interface hierarchy.
- Query Methods — derived query.
- @Query Annotation
- Specifications
- Auditing
JPA spec:
- Jakarta Persistence — Querying — JPQL grammar.
Resources:
- Vlad Mihalcea — JPQL vs HQL — detail differences.
- Vlad Mihalcea — Best Spring Data JPA tips — collection.
QueryDSL:
- QueryDSL Reference — alternative to Specification.
12. Tóm tắt
JpaRepository=Repository+CrudRepository+PagingAndSortingRepository+ JPA-specific. 99% case dùng nó.- 3 tier query: built-in method → derived query (method name) →
@Query(JPQL/native). - Derived query grammar:
[verb][Distinct]?[Top|First N]?[subject] By [property] [keyword] ... [OrderBy ...]. @QueryJPQL: type-safe, refactor-friendly. Named parameter:namerecommend.- Native query cho DB-specific feature. Lock vendor.
@Modifyingcho UPDATE/DELETE. Require@Transactional. Persistence context không tự update — refresh hoặc clear.- 4 projection pattern: interface, DTO constructor expression, class-based, open SpEL.
- DTO projection (JPQL
SELECT new ...) performance tốt nhất — chỉ select field cần. Pageableauto-bind từ?page=&size=&sort=. Generate LIMIT/OFFSET + COUNT.Slicebỏ COUNT cho infinite scroll. Window/cursor (3.1+) cho data thay đổi nhanh.- Specification dynamic query qua Criteria API. Type-safe nhưng verbose.
- Auditing
@CreatedDate/@LastModifiedDatequaAuditingEntityListener+@EnableJpaAuditing.
13. Tự kiểm tra
Q13 tier query Spring Data JPA: built-in, derived, @Query. Khi nào pick cái nào?▸
Decision tree:
Standard CRUD (save, findById, delete)?
Yes → Built-in method (JpaRepository inherit)
No → Continue
Query 1-3 condition đơn giản?
Yes → Derived query (method name)
No → Continue
Query phức tạp (JOIN, aggregate, subquery)?
Yes → @Query JPQL
No → Continue
DB-specific feature (Postgres array, JSON, full-text)?
Yes → @Query nativeQuery=true
No → Continue
Dynamic query (filter UI, runtime conditions)?
Yes → Specification hoặc QueryDSL
No → ReconsiderVí dụ cụ thể:
// Tier 1: Built-in
public interface ProjectRepository extends JpaRepository<Project, Long> {
// No code — inherit save, findById, delete, findAll, count, ...
}
// Tier 2: Derived
public interface ProjectRepository extends JpaRepository<Project, Long> {
Optional<Project> findByName(String name);
List<Project> findByStatusAndCreatedAtAfter(ProjectStatus s, Instant since);
long countByStatus(ProjectStatus s);
Page<Project> findByStatus(ProjectStatus s, Pageable pageable);
}
// Tier 3a: @Query JPQL
public interface ProjectRepository extends JpaRepository<Project, Long> {
@Query("""
SELECT p FROM Project p
LEFT JOIN p.tasks t
WHERE p.status = :status
GROUP BY p.id
HAVING COUNT(t) > :minTasks
""")
List<Project> findActiveWithManyTasks(@Param("status") ProjectStatus status,
@Param("minTasks") long minTasks);
}
// Tier 3b: Native SQL
public interface ProjectRepository extends JpaRepository<Project, Long> {
@Query(value = """
SELECT * FROM projects
WHERE search_vector @@ to_tsquery('english', :query)
ORDER BY ts_rank(search_vector, to_tsquery(:query)) DESC
LIMIT 20
""", nativeQuery = true)
List<Project> fullTextSearch(@Param("query") String query);
}
// Tier 4: Specification dynamic
@Service
public class ProjectService {
public Page<Project> search(ProjectFilter filter, Pageable p) {
Specification<Project> spec = Specification.where(null);
if (filter.status() != null) spec = spec.and(...);
if (filter.from() != null) spec = spec.and(...);
// ...
return repo.findAll(spec, p);
}
}Quy tắc:
- Default: built-in + derived (90% case).
- Switch
@QueryJPQL khi method name vượt 5 condition hoặc cần aggregate/JOIN. - Native SQL chỉ khi DB-specific feature thật sự cần.
- Specification cho admin search UI với filter dynamic.
Trade-off:
| Tier | Type-safe | Refactor | Complex query | Verbose |
|---|---|---|---|---|
| Built-in | ✅ | ✅ | ❌ | 0 |
| Derived | ✅ | ~ (method name string) | ~ | 1 |
| @Query JPQL | ~ (JPQL string) | ~ | ✅ | 3 |
| Native SQL | ❌ (raw SQL) | ❌ | ✅ | 3 |
| Specification | ✅ | ✅ | ✅ | 5 |
Q2So sánh 4 cách projection: interface, DTO constructor expression, class-based, open. Khi nào pick cái nào?▸
Tóm tắt comparison:
| Aspect | Interface | DTO constructor expression | Class-based | Open SpEL |
|---|---|---|---|---|
| Setup | Interface 5 dòng | JPQL với SELECT new | POJO + getter | Interface với @Value |
| SQL optimization | ❌ Load full entity | ✅ Select chỉ field cần | ~ Phụ thuộc query | ❌ Load full entity |
| Type-safe | ✅ | ✅ | ✅ | ~ (SpEL string) |
| Aggregate/JOIN | ~ | ✅ (in JPQL) | ~ | ~ |
| Computed field | ❌ | ~ (in JPQL) | ~ | ✅ (SpEL) |
| Verbose | 1 | 3 | 2 | 3 |
| Performance | Bad | Best | Mid | Bad |
Decision:
- Default cho list/page (performance critical): DTO constructor expression.
- Đơn giản subset entity field: Interface projection.
- Dynamic computed field: Open SpEL projection.
- Class-based: hiếm — Spring Data 1 lib khác đôi khi pull POJO match field name. Avoid trong Spring Data JPA mainstream.
Code 4 ví dụ:
// 1. Interface projection (closed)
public interface ProjectSummaryView {
Long getId();
String getName();
ProjectStatus getStatus();
}
interface ProjectRepository extends JpaRepository<Project, Long> {
List<ProjectSummaryView> findByStatus(ProjectStatus s);
// Hibernate: SELECT * FROM projects, then map field-by-field
}
// 2. DTO constructor expression
public record ProjectSummaryDto(Long id, String name, long taskCount) {}
@Query("""
SELECT new com.olhub.dto.ProjectSummaryDto(p.id, p.name, COUNT(t.id))
FROM Project p LEFT JOIN p.tasks t
GROUP BY p.id, p.name
""")
List<ProjectSummaryDto> findSummary();
// SQL: SELECT p.id, p.name, COUNT(t.id) FROM projects p LEFT JOIN tasks t ...
// 3. Class-based
public class ProjectSummaryClass {
private Long id;
private String name;
public ProjectSummaryClass(Long id, String name) { ... }
// getters
}
interface ProjectRepository extends JpaRepository<Project, Long> {
List<ProjectSummaryClass> findByStatus(ProjectStatus s);
}
// 4. Open SpEL
public interface ProjectVerbose {
Long getId();
String getName();
@Value("#{target.name + ' (' + target.tasks.size() + ' tasks)'}")
String getDisplayName();
// Force load full entity to compute
}Performance benchmark (10k record):
- DTO constructor expression: ~50ms (3 columns from DB).
- Interface projection (closed): ~120ms (10 columns + map).
- Open SpEL: ~200ms (full entity + lazy fetch tasks).
Recommend 2026 cho TaskFlow: DTO constructor expression cho list endpoint. Entity trả cho single GET (cần modify capability).
Q3Đoạn sau modifying query. Có 2 vấn đề.public interface ProjectRepository extends JpaRepository<Project, Long> {
@Query("UPDATE Project p SET p.status = :status WHERE p.archivedAt < :cutoff")
int archiveOld(@Param("status") ProjectStatus status,
@Param("cutoff") Instant cutoff);
}
@Service
public class ProjectService {
public void archive() {
Instant cutoff = Instant.now().minus(Duration.ofDays(365));
repo.archiveOld(ProjectStatus.ARCHIVED, cutoff);
}
}
▸
public interface ProjectRepository extends JpaRepository<Project, Long> {
@Query("UPDATE Project p SET p.status = :status WHERE p.archivedAt < :cutoff")
int archiveOld(@Param("status") ProjectStatus status,
@Param("cutoff") Instant cutoff);
}
@Service
public class ProjectService {
public void archive() {
Instant cutoff = Instant.now().minus(Duration.ofDays(365));
repo.archiveOld(ProjectStatus.ARCHIVED, cutoff);
}
}- Thiếu
@Modifying: Spring Data treat@Querydefault như SELECT. UPDATE/DELETE phải có@Modifying.Without: throw runtime error "Not supported for DML operations".@Modifying @Query("UPDATE Project p SET p.status = :status WHERE p.archivedAt < :cutoff") int archiveOld(...); - Thiếu
@Transactional: UPDATE/DELETE cần active transaction. Service method không có@Transactional→ throwTransactionRequiredException.Hoặc@Service public class ProjectService { @Transactional // bat tx public void archive() { Instant cutoff = Instant.now().minus(Duration.ofDays(365)); repo.archiveOld(ProjectStatus.ARCHIVED, cutoff); } }@Transactionaltrên repository method (less clean).
Fix đầy đủ:
public interface ProjectRepository extends JpaRepository<Project, Long> {
@Modifying(clearAutomatically = true) // clear persistence context sau modify
@Query("UPDATE Project p SET p.status = :status WHERE p.archivedAt < :cutoff")
int archiveOld(@Param("status") ProjectStatus status,
@Param("cutoff") Instant cutoff);
}
@Service
@Transactional // class-level
public class ProjectService {
private final ProjectRepository repo;
public int archiveOldProjects() {
Instant cutoff = Instant.now().minus(Duration.ofDays(365));
return repo.archiveOld(ProjectStatus.ARCHIVED, cutoff);
}
}Bonus: clearAutomatically = true.
Vấn đề khi không có:
@Transactional
public void example() {
Project p = repo.findById(42L).orElseThrow(); // entity managed in tx
System.out.println(p.getStatus()); // ACTIVE
repo.archiveOld(ARCHIVED, cutoff); // SQL UPDATE — entity 42 status → ARCHIVED in DB
System.out.println(p.getStatus()); // VAN ACTIVE (stale managed entity)
p.setName("New name"); // dirty checking marker
// Tx commit:
// 1. UPDATE projects SET name = 'New name', status = 'ACTIVE' WHERE id = 42
// 2. ^^ status 'ACTIVE' GHI DE 'ARCHIVED' tu archiveOld!
}Stale entity overwrite bulk update result. clearAutomatically = true clear persistence context sau modify → entity managed bị evict → lookup lại sẽ fetch fresh data.
Quy tắc: bulk modify + load entity trong cùng tx = nguy hiểm. Tách:
- Service A: bulk modify only (separate tx).
- Service B: load + modify entity (separate tx).
Hoặc dùng clearAutomatically = true + reload entity sau bulk.
Q4App có endpoint GET /projects?status=ACTIVE&page=0&size=20&sort=createdAt,desc. Spring tự bind Pageable từ query param. Generated SQL ra sao? Có optimization nào nên bật?▸
GET /projects?status=ACTIVE&page=0&size=20&sort=createdAt,desc. Spring tự bind Pageable từ query param. Generated SQL ra sao? Có optimization nào nên bật?Generated SQL (2 query):
-- Query 1: Fetch page data
SELECT p.*
FROM projects p
WHERE p.status = ? -- 'ACTIVE'
ORDER BY p.created_at DESC
LIMIT 20 OFFSET 0; -- size=20, page=0
-- Query 2: Total count
SELECT COUNT(*)
FROM projects p
WHERE p.status = ?; -- 'ACTIVE'Spring Data wrap result trong Page<Project> với:
content: 20 record từ Query 1.totalElements: total từ Query 2.totalPages: ceil(totalElements / size).
Optimization 1 — Slice (skip COUNT):
Slice<Project> findByStatus(ProjectStatus status, Pageable pageable);
// Chi 1 query, fetch (size + 1) record de check hasNextSlice không count total — chỉ check "có next page không" qua fetching `size + 1` record. Phù hợp infinite scroll UI (Twitter feed, Instagram).
Performance: ~50% nhanh hơn Page cho table lớn — bỏ COUNT(*) là expensive trên 1M+ row.
Optimization 2 — Index:
-- Migration script Flyway
CREATE INDEX idx_projects_status_created
ON projects (status, created_at DESC);Composite index match query pattern (status filter + ORDER BY created_at). Postgres dùng index scan thay sequential scan.
Optimization 3 — Cursor pagination (large dataset):
Window<Project> findByStatusOrderByCreatedAtDesc(
ProjectStatus status,
ScrollPosition position
);
// Service:
Window<Project> first = repo.findByStatusOrderByCreatedAtDesc(ACTIVE, ScrollPosition.offset());
Window<Project> next = repo.findByStatusOrderByCreatedAtDesc(ACTIVE, first.positionAt(first.size() - 1));
// SQL: WHERE created_at < :lastCreatedAt ORDER BY created_at DESC LIMIT 20
// (no OFFSET — index lookup faster)Cursor pagination dùng "last seen value" thay OFFSET. Performance constant cho mọi page. Use case: Hacker News, Reddit, log viewer.
Optimization 4 — DTO projection cho list:
@Query("SELECT new com.olhub.dto.ProjectSummary(p.id, p.name, p.status, p.createdAt) " +
"FROM Project p WHERE p.status = :status")
Page<ProjectSummary> findSummaryByStatus(@Param("status") ProjectStatus status, Pageable p);Select 4 column thay full entity (10+ column + association). 50% network data, faster mapping.
Optimization 5 — countQuery custom:
@Query(value = "SELECT p FROM Project p WHERE p.status = :status",
countQuery = "SELECT COUNT(p.id) FROM Project p WHERE p.status = :status")
Page<Project> findByStatus(...);Default count query có thể join association không cần — custom count tối ưu.
Anti-pattern:
findAll()không Pageable cho table lớn → OOM.- Page deep (page=999, size=20 → OFFSET 19980): performance suy giảm tuyến tính. Switch cursor.
- Load entity rồi convert DTO trong service — slower than DTO projection.
Q5Specification vs derived query cho dynamic filter UI (5 optional field). Khi nào dùng cái nào?▸
Derived query không support dynamic optional field tốt.
Vấn đề derived:
// Cố gắng derived cho 5 optional field:
List<Project> findByStatusAndCreatedAtBetweenAndPriorityGreaterThanAndNameContainingAndOwnerEquals(
ProjectStatus status, // có thể null
Instant from, // có thể null
Instant to, // có thể null
Integer priority, // có thể null
String name, // có thể null
String owner // có thể null
);
// Method name 100 ky tu, KHONG handle null - all params requiredDerived query không skip NULL param. Mọi condition trong SQL fixed.
Workaround xấu: 32 method (2^5) cho mọi combination — exponential.
Specification — clean cho dynamic:
public interface ProjectRepository extends JpaRepository<Project, Long>, JpaSpecificationExecutor<Project> {
}
@Service
public class ProjectService {
public Page<Project> search(ProjectFilter filter, Pageable page) {
Specification<Project> spec = (root, query, cb) -> cb.conjunction(); // start with TRUE
if (filter.status() != null) {
spec = spec.and((root, q, cb) -> cb.equal(root.get("status"), filter.status()));
}
if (filter.from() != null) {
spec = spec.and((root, q, cb) -> cb.greaterThan(root.get("createdAt"), filter.from()));
}
if (filter.to() != null) {
spec = spec.and((root, q, cb) -> cb.lessThan(root.get("createdAt"), filter.to()));
}
if (filter.minPriority() != null) {
spec = spec.and((root, q, cb) -> cb.greaterThanOrEqualTo(root.get("priority"), filter.minPriority()));
}
if (filter.nameContains() != null) {
spec = spec.and((root, q, cb) ->
cb.like(cb.lower(root.get("name")), "%" + filter.nameContains().toLowerCase() + "%"));
}
if (filter.owner() != null) {
spec = spec.and((root, q, cb) -> cb.equal(root.get("owner"), filter.owner()));
}
return repo.findAll(spec, page);
}
}SQL generated (varies based on filter):
-- filter.status=ACTIVE, filter.from=null, filter.minPriority=3
SELECT p.* FROM projects p
WHERE 1=1 AND p.status = 'ACTIVE' AND p.priority >= 3
LIMIT 20;
-- filter.status=null, filter.nameContains='Mobile', filter.owner='alice'
SELECT p.* FROM projects p
WHERE 1=1 AND LOWER(p.name) LIKE '%mobile%' AND p.owner = 'alice'
LIMIT 20;Spring Data tự skip null condition.
Quy tắc:
| Scenario | Approach |
|---|---|
| Fixed query, all param required | Derived query / @Query |
| Optional field 1-2 | 2 method overload |
| Optional field 3+ | Specification |
| Complex filter UI (admin search) | Specification + reusable spec method |
| Performance critical | Native SQL hand-tuned |
Reusable Specification:
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() + "%");
}
}
// Usage clean
Specification<Project> spec = Specification.where(ProjectSpecs.hasStatus(filter.status()))
.and(ProjectSpecs.createdAfter(filter.from()))
.and(ProjectSpecs.nameContains(filter.nameContains()));
return repo.findAll(spec, page);Specification methods là reusable — share giữa list, count, export endpoint.
Alternative — QueryDSL: nếu Specification quá verbose, QueryDSL có API gọn hơn. Thêm dependency, generate Q-classes. Module 09 có thể intro.
Q6Auditing với @CreatedDate / @LastModifiedDate. Setup và pattern thực tế?▸
Setup 4 bước:
1. Enable auditing:
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig { }2. AuditorAware (cho @CreatedBy / @LastModifiedBy):
@Component("auditorProvider")
public class SecurityAuditorAware implements AuditorAware<String> {
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}3. Base entity (DRY):
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@CreatedBy
@Column(name = "created_by", updatable = false, length = 50)
private String createdBy;
@LastModifiedBy
@Column(name = "updated_by", length = 50)
private String updatedBy;
// Getters
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public String getCreatedBy() { return createdBy; }
public String getUpdatedBy() { return updatedBy; }
}4. Entity extend:
@Entity
@Table(name = "projects")
public class Project extends AuditableEntity {
@Id @GeneratedValue Long id;
String name;
// 4 audit field inherit tu AuditableEntity
}Lifecycle:
- Persist: Hibernate trigger
AuditingEntityListener.touchForCreate(entity)→ setcreatedAt = Instant.now(),createdBy = auditor. - Update: trigger
touchForUpdate(entity)→ setupdatedAt,updatedBy. Không touchcreatedAt/createdBy(annotationupdatable = false).
SQL generated:
-- INSERT
INSERT INTO projects (name, created_at, updated_at, created_by, updated_by)
VALUES (?, NOW(), NOW(), 'alice', 'alice');
-- UPDATE
UPDATE projects
SET name = ?, updated_at = NOW(), updated_by = 'alice'
WHERE id = ?;
-- created_at, created_by KHONG cap nhatPattern thực tế:
- Base class trong shared module:
AuditableEntityở module common, mọi entity domain extend. - System user cho background job:Background job (scheduled, Kafka consumer) không có auth context →
public Optional<String> getCurrentAuditor() { return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) .filter(Authentication::isAuthenticated) .map(Authentication::getName) .or(() -> Optional.of("system")); // fallback }systemstring. - UTC timezone:
Instantalways UTC.LocalDateTimenếu cần local time, nhưng phải document timezone. - Audit log table riêng: nếu cần history (mọi change),
@EntityListenerscustom write vàoaudit_logtable. Hoặc Hibernate Envers.
Hibernate Envers — full audit history:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
@Entity
@Audited
public class Project { ... }
// Hibernate generate projects_AUD table — track every change
// Query: AuditReader reader = AuditReaderFactory.get(em);
// reader.find(Project.class, 42L, revisionNumber);Envers cho compliance heavy (banking, insurance). Khoá này không cover — overhead lớn.
Quy tắc TaskFlow: 4 audit field cho mọi entity (createdAt/By, updatedAt/By). Detailed history qua dedicated audit log nếu cần — Module 12 (Event-driven).
Bài tiếp theo: Relationships — @OneToMany, @ManyToOne, lazy vs eager, N+1 problem
Bài này có giúp bạn hiểu bản chất không?
Bình luận (0)
Đang tải...