Spring Boot/Repository abstraction — JpaRepository, derived queries, @Query
~24 phútSpring Data JPAMiễn phí

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
InterfaceMethods
RepositoryMarker — empty
CrudRepositorysave, 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 ...]
KeywordSQL
Equals, no keyword=
Not!=
LessThan, LessThanEqual<, <=
GreaterThan, GreaterThanEqual>, >=
BetweenBETWEEN ? AND ?
In, NotInIN (...), NOT IN (...)
Like, NotLikeLIKE, NOT LIKE
Containing, StartingWith, EndingWithLIKE %?%, LIKE ?%, LIKE %?
IgnoreCaseLOWER(field) = LOWER(?)
True, False= true, = false
IsNull, IsNotNullIS NULL, IS NOT NULL
OrderByORDER BY ?

2.2 Khi derived query không đủ

3 case fail:

  1. Method name quá dài (>5 condition) — không readable.
  2. Aggregate function (SUM, AVG): không support.
  3. 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 -parameters flag 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

CaseApproach
Subset field từ entityInterface projection (closed)
Aggregate (count, sum)DTO projection với JPQL
Full DTO transformDTO projection với JPQL
Computed fieldOpen 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
MetricUse case
hibernate.queries.executedTotal query rate per repo
hibernate.entities.loadedDetect lazy load thrashing
hibernate.query.cache.hit.ratio2nd level cache efficiency
hibernate.flushesDirty 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.executed rate per repository, slow query log.
  • Tool: EXPLAIN ANALYZE Postgres → 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 install validate.

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: Specification generate query không index — vd LIKE %pattern% full text scan.
  • Remediate: whitelist filter field + ensure index. Dùng full-text search engine (Postgres tsvector hoặ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

📚 Tài liệu chính chủ

Spring Data JPA:

JPA spec:

Resources:

QueryDSL:

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 ...].
  • @Query JPQL: type-safe, refactor-friendly. Named parameter :name recommend.
  • Native query cho DB-specific feature. Lock vendor.
  • @Modifying cho 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.
  • Pageable auto-bind từ ?page=&size=&sort=. Generate LIMIT/OFFSET + COUNT.
  • Slice bỏ 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/@LastModifiedDate qua AuditingEntityListener + @EnableJpaAuditing.

13. Tự kiểm tra

Tự kiểm tra
Q1
3 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 → Reconsider

Ví 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 @Query JPQL 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:

TierType-safeRefactorComplex queryVerbose
Built-in0
Derived~ (method name string)~1
@Query JPQL~ (JPQL string)~3
Native SQL❌ (raw SQL)3
Specification5
Q2
So 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:

AspectInterfaceDTO constructor expressionClass-basedOpen SpEL
SetupInterface 5 dòngJPQL với SELECT newPOJO + getterInterface 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)
Verbose1323
PerformanceBadBestMidBad

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);
  }
}
  1. Thiếu @Modifying: Spring Data treat @Query default như SELECT. UPDATE/DELETE phải có @Modifying.
    @Modifying
    @Query("UPDATE Project p SET p.status = :status WHERE p.archivedAt < :cutoff")
    int archiveOld(...);
    Without: throw runtime error "Not supported for DML operations".
  2. Thiếu @Transactional: UPDATE/DELETE cần active transaction. Service method không có @Transactional → throw TransactionRequiredException.
    @Service
    public class ProjectService {
    
      @Transactional                              // bat tx
      public void archive() {
          Instant cutoff = Instant.now().minus(Duration.ofDays(365));
          repo.archiveOld(ProjectStatus.ARCHIVED, cutoff);
      }
    }
    Hoặc @Transactional trê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.

Q4
App 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?

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 hasNext

Slice 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.
Q5
Specification 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 required

Derived 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:

ScenarioApproach
Fixed query, all param requiredDerived query / @Query
Optional field 1-22 method overload
Optional field 3+Specification
Complex filter UI (admin search)Specification + reusable spec method
Performance criticalNative 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.

Q6
Auditing 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) → set createdAt = Instant.now(), createdBy = auditor.
  • Update: trigger touchForUpdate(entity) → set updatedAt, updatedBy. Không touch createdAt/createdBy (annotation updatable = 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 nhat

Pattern thực tế:

  1. Base class trong shared module: AuditableEntity ở module common, mọi entity domain extend.
  2. System user cho background job:
    public Optional<String> getCurrentAuditor() {
      return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
          .filter(Authentication::isAuthenticated)
          .map(Authentication::getName)
          .or(() -> Optional.of("system"));    // fallback
    }
    Background job (scheduled, Kafka consumer) không có auth context → system string.
  3. UTC timezone: Instant always UTC. LocalDateTime nếu cần local time, nhưng phải document timezone.
  4. Audit log table riêng: nếu cần history (mọi change), @EntityListeners custom write vào audit_log table. 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...