Spring REST API & Data JPA/@Query, @Modifying và Projection — viết query tuỳ chỉnh và trả về đúng shape
31/46
Bài 31 / 46~12 phútRepository & QueriesMiễn phí lượt xem

@Query, @Modifying và Projection — viết query tuỳ chỉnh và trả về đúng shape

Ba kỹ thuật bổ sung derived query: @Query cho JPQL hoặc native SQL khi method name không đủ; @Modifying cho UPDATE/DELETE với pitfall persistence context stale; projection (interface, DTO, class-based) để chỉ fetch field cần thay vì load full entity.

TL;DR: Khi derived query (tên method) không đủ, dùng @Query để viết JPQL hoặc native SQL trực tiếp. Với UPDATE/DELETE, bắt buộc thêm @Modifying + @Transactional — thiếu một trong hai sẽ throw exception; nguy hiểm hơn là bulk update không tự đồng bộ persistence context, entity đang được quản lý sẽ giữ giá trị cũ (stale). Giải pháp: @Modifying(clearAutomatically = true). Projection giải quyết vấn đề khác: thay vì load toàn bộ entity rồi bỏ 80% field, dùng interface projection hoặc DTO constructor expression để SQL chỉ SELECT đúng các cột cần — giảm đáng kể data transfer và mapping overhead.

Bài JpaRepository & derived queries đã bao quát built-in method và tên method grammar. Bài này bước vào tầng tiếp theo: khi method name grammar không đủ hoặc trả về shape khác entity.

1. Vì sao @Query tồn tại — giới hạn của derived query

Derived query rất tiện cho 1–3 điều kiện đơn giản. Nhưng nó fail ở 3 trường hợp:

Trường hợpVí dụVấn đề
Method name quá dàifindByStatusAndOwnerAndCreatedAtAfterAndPriorityGreaterThanKhông readable, IDE warn
Aggregate functionSUM(budget), AVG(score)Derived không support
JOIN + projectionLấy field từ nhiều entity trong 1 queryGrammar không biểu đạt được

@Query (annotation từ Spring Data) cho phép viết JPQL hoặc native SQL trực tiếp, giữ nguyên cơ chế Spring Data proxy (tham số bind, pagination, return type).

JPQL (Jakarta Persistence Query Language) là ngôn ngữ query hướng object — query trên entity class và field, không phải table và cột. Hibernate dịch JPQL sang SQL theo dialect DB cụ thể. Lợi thế: refactor-friendly và DB-agnostic. Native SQL là SQL thật gửi thẳng tới DB — dùng khi cần feature DB-specific.

flowchart TB
    Code["@Query annotation<br/>JPQL hoac native SQL"]
    JPQL["JPQL<br/>SELECT p FROM Project p WHERE p.status = :status<br/>query tren entity class"]
    Native["Native SQL<br/>SELECT * FROM projects WHERE status = :status<br/>query tren table/column"]
    Hibernate["Hibernate<br/>dich JPQL -> SQL theo dialect<br/>hoac chuyen thang native"]
    DB["PostgreSQL / MySQL / ..."]

    Code --> JPQL
    Code --> Native
    JPQL -->|"dialect translation"| Hibernate
    Native -->|"pass-through"| Hibernate
    Hibernate --> DB

2. @Query — JPQL và named parameters

Dưới đây là cách dùng @Query cho các trường hợp phổ biến:

public interface ProjectRepository extends JpaRepository<Project, Long> {

    // JPQL — named parameter :param (recommended)
    @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);

    // JPQL — aggregate: GROUP BY, COUNT
    @Query("SELECT p.status, COUNT(p) FROM Project p GROUP BY p.status")
    List<Object[]> countGroupByStatus();

    // JPQL — JOIN entity field
    @Query("SELECT p FROM Project p JOIN p.tasks t WHERE t.priority = :priority")
    List<Project> findWithHighPriorityTasks(@Param("priority") TaskPriority priority);

    // Native SQL — feature Postgres-specific
    @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);
}

2.1 Named parameter vs positional

// Positional ?1 ?2 — thứ tự phải khớp, dễ sai khi refactor
@Query("SELECT p FROM Project p WHERE p.status = ?1 AND p.name = ?2")
List<Project> findPositional(ProjectStatus status, String name);

// Named :name — self-documenting, refactor-friendly (recommended)
@Query("SELECT p FROM Project p WHERE p.status = :status AND p.name = :name")
List<Project> findNamed(@Param("status") ProjectStatus status,
                         @Param("name") String name);

Dùng named parameter (:name) vì khi thêm/bỏ tham số thứ tự positional sẽ sai ngầm, còn named parameter lỗi rõ ràng tại startup.

2.2 Khi nào dùng native SQL

Native SQL lock vào DB vendor — dùng khi thực sự cần:

  • DB-specific feature: Postgres jsonb @>, tsvector @@, array operator, window function.
  • Performance critical: SQL hand-tuned từ DBA, index hint.
  • Legacy SQL: migrate query có sẵn, chưa muốn rewrite sang JPQL.

Trade-off: native SQL không được Hibernate validate cú pháp lúc startup (chỉ fail tại runtime), và kết quả trả về Object[] hoặc cần @SqlResultSetMapping để map sang type.

3. @Modifying — UPDATE/DELETE qua JPQL

Mặc định, @Query được Spring Data xử lý như SELECT. Với UPDATE và DELETE, bắt buộc thêm @Modifying:

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);
}

int trả về là số record bị ảnh hưởng (rows affected).

@Transactional tại repository method là cần thiết vì UPDATE/DELETE yêu cầu transaction đang active. Trong production thường đặt @Transactional ở service layer (class-level hoặc method), còn repository method không cần annotation riêng — tham chiếu transaction từ caller.

3.1 Cơ chế bên dưới — tại sao persistence context stale

Đây là pitfall nguy hiểm nhất của @Modifying. Hibernate duy trì một persistence context (còn gọi là first-level cache) — map từ entity ID đến object đang quản lý trong transaction. Khi @Modifying thực thi bulk UPDATE trực tiếp qua JDBC, nó bypass persistence context hoàn toàn: Hibernate không biết entity nào bị ảnh hưởng, không cập nhật object đang tracked.

flowchart TB
    PC["Persistence Context<br/>Map id -> managed entity<br/>Project(42) status=ACTIVE"]
    BulkSQL["@Modifying bulk UPDATE<br/>UPDATE projects SET status='ARCHIVED'<br/>WHERE archived_at < cutoff"]
    DB["Database<br/>projects row 42: status=ARCHIVED"]
    Entity["Project(42) in memory<br/>status still = ACTIVE (stale)"]

    PC -->|"holds reference"| Entity
    BulkSQL -->|"SQL via JDBC, bypass PC"| DB
    DB -.->|"PC not notified"| PC
    Entity -.->|"OUT OF SYNC"| DB

Hệ quả nguy hiểm — stale entity có thể ghi đè kết quả bulk update:

@Transactional
public void dangerousExample() {
    Project p = repo.findById(42L).orElseThrow();
    System.out.println(p.getStatus());         // ACTIVE — managed entity

    repo.bulkUpdateStatus(ACTIVE, ARCHIVED);   // SQL UPDATE chay, DB row: ARCHIVED

    System.out.println(p.getStatus());         // VAN LA ACTIVE — entity stale!

    p.setName("New name");                     // dirty checking mark p as dirty
    // Tx commit: Hibernate flush dirty p
    // SQL: UPDATE projects SET name='New name', status='ACTIVE' WHERE id=42
    //                                         ^^^^^^^^^^^^^^^^
    //   GHI DE ket qua bulk update! DB row quay lai ACTIVE
}
Bulk update bypass persistence context — stale entity ghi đè kết quả

@Modifying chạy UPDATE thẳng xuống JDBC, Hibernate không biết entity nào bị ảnh hưởng. Entity đang managed giữ giá trị cũ; nếu sau đó nó dirty (vd setName), flush lúc commit sẽ ghi đè ngược kết quả bulk update — bug âm thầm, không exception nào báo. Luôn dùng @Modifying(clearAutomatically = true) khi transaction có thể load entity cùng loại trước đó.

3.2 Giải pháp: clearAutomatically

@Modifying(clearAutomatically = true) ra lệnh Hibernate clear toàn bộ persistence context ngay sau khi bulk statement thực thi. Mọi managed entity bị evict — lần lookup tiếp theo sẽ fetch fresh từ DB:

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Project p SET p.status = :newStatus WHERE p.status = :oldStatus")
int bulkUpdateStatus(@Param("oldStatus") ProjectStatus oldStatus,
                      @Param("newStatus") ProjectStatus newStatus);
@Transactional
public void safeExample() {
    Project p = repo.findById(42L).orElseThrow();
    System.out.println(p.getStatus());          // ACTIVE

    repo.bulkUpdateStatus(ACTIVE, ARCHIVED);    // clearAutomatically evict p tu PC

    // p la detached object, khong con managed
    Project fresh = repo.findById(42L).orElseThrow();  // SELECT lai tu DB
    System.out.println(fresh.getStatus());      // ARCHIVED — correct
}

flushAutomatically = true (mặc định false) bổ sung flush pending changes trước khi bulk statement chạy — cần thiết khi có unsaved entity change trong cùng transaction cần phản ánh vào điều kiện WHERE của bulk query.

Quy tắc production: nếu bulk modify và load entity xảy ra trong cùng transaction, luôn dùng clearAutomatically = true. Hoặc tốt hơn: tách thành hai transaction riêng biệt (service method riêng) để tránh phức tạp.

4. Projection — vì sao không load full entity

Khi endpoint list trả về 20 record mà entity Project có 15 field (kể cả description text dài, metadata JSON, các audit field), nhưng UI chỉ hiển thị id, name, status — bạn đang gửi 15 cột qua JDBC, map thành object, rồi serialize thành JSON chỉ để bỏ 12 cột. Đây là lãng phí: network, memory, CPU mapping.

Projection là kỹ thuật chỉ SELECT đúng các field cần — tương tự SELECT id, name, status FROM projects thay vì SELECT *.

flowchart TB
    Full["SELECT * entity<br/>15 columns, LazyInit risk<br/>full object heap"]
    Proj["SELECT new ProjectSummary(p.id, p.name, p.status)<br/>3 columns, direct DTO<br/>no entity overhead"]
    UI["UI / API response<br/>chi can 3 field"]

    Full -->|"map 15 field, drop 12"| UI
    Proj -->|"map 3 field direct"| UI

    style Full fill:#fee2e2
    style Proj fill:#d1fae5

4.1 Interface projection (closed)

Khai báo interface với getter cho từng field cần. Spring Data sinh proxy implement interface, populate từ query result:

// Khai bao interface — chi getters cho field can
public interface ProjectSummary {
    Long getId();
    String getName();
    ProjectStatus getStatus();
}

public interface ProjectRepository extends JpaRepository<Project, Long> {
    // Spring Data tu dong dung ProjectSummary lam return type
    List<ProjectSummary> findByStatus(ProjectStatus status);
    Page<ProjectSummary> findByStatus(ProjectStatus status, Pageable pageable);
}

Cú pháp này rất gọn. Tuy nhiên, với interface projection đơn thuần (closed projection), Hibernate vẫn load full entity rồi Spring Data map sang interface proxy — SQL vẫn SELECT *. Lợi ích thực là ở tầng Java API (type-safe, không lộ field thừa ra controller), không phải SQL optimization.

Khi nào dùng: muốn giới hạn field lộ ra API theo kiểu declarative mà không cần viết @Query — trường hợp đơn giản, performance không critical.

4.2 DTO projection — constructor expression (performance tốt nhất)

Đây là pattern đạt SQL optimization thật sự. JPQL SELECT new com.example.ClassName(field1, field2) chỉ SELECT đúng các cột được truyền vào constructor:

// DTO — record la idiomatic Java 21
public record ProjectSummary(Long id, String name, ProjectStatus status) {}

public interface ProjectRepository extends JpaRepository<Project, Long> {

    @Query("""
        SELECT new com.olhub.dto.ProjectSummary(p.id, p.name, p.status)
        FROM Project p
        WHERE p.status = :status
        """)
    List<ProjectSummary> findSummaryByStatus(@Param("status") ProjectStatus status);

    // Voi aggregate
    @Query("""
        SELECT new com.olhub.dto.ProjectSummary(p.id, p.name, p.status)
        FROM Project p
        LEFT JOIN p.tasks t
        WHERE p.ownerId = :ownerId
        GROUP BY p.id, p.name, p.status
        """)
    List<ProjectSummary> findOwnerProjectsSummary(@Param("ownerId") Long ownerId);
}

SQL sinh ra: SELECT p.id, p.name, p.status FROM projects p WHERE p.status = ? — chỉ 3 cột, không JOIN không cần thiết, không lazy-load association.

Khi nào dùng: list endpoint, pagination, export — bất kỳ chỗ nào cần nhiều record và không cần modify entity.

4.3 So sánh ba pattern projection

Interface projectionDTO constructorClass-based
SQL optimizationKhông (SELECT *)Có (SELECT chỉ field)Tuỳ query
Type-safe
Aggregate/JOINHạn chếĐầy đủ (JPQL)Hạn chế
BoilerplateThấp nhấtCần @QueryCần POJO + getter
Khi nào dùngAPI shape control đơn giảnList/page performanceÍt dùng

Class-based projection là POJO với constructor match field name — Spring Data auto-detect. Trong Spring Data JPA mainstream, pattern này hiếm dùng; DTO constructor expression clean hơn vì JPQL rõ ràng về field nào được SELECT.

4.4 Quy tắc chọn

Single GET (cần modify sau) -> entity
List / page endpoint          -> DTO projection (@Query constructor expression)
Đơn giản, no JOIN, no agg    -> interface projection (closed)
Computed field (SpEL)         -> open projection (hiếm)

Nguyên tắc: entity cho write path, projection cho read path. Đây là cách tư duy CQRS đơn giản ở repository layer.

5. Pitfall tổng hợp

Pitfall 1 — JPQL nhầm SQL syntax:

// SAI — SQL syntax trong @Query JPQL
@Query("SELECT * FROM projects WHERE id = ?1")
Project findByIdQuery(Long id);
// Throw: QuerySyntaxException — table "projects" not mapped entity
// DUNG — JPQL query tren entity class
@Query("SELECT p FROM Project p WHERE p.id = ?1")
Project findByIdQuery(Long id);
// Hoac native: nativeQuery = true

JPQL query trên tên entity class (Project), không phải tên table (projects). Field cũng là Java field (p.createdAt), không phải column (created_at).

Pitfall 2 — @Modifying thiếu @Transactional:

// SAI
@Modifying
@Query("UPDATE Project p SET p.status = :status WHERE p.id = :id")
int updateStatus(@Param("status") ProjectStatus status, @Param("id") Long id);

// Goi tu service khong co @Transactional -> TransactionRequiredException
// DUNG — @Transactional tren service method hoac repository method
@Transactional
public void markArchived(Long id) {
    repo.updateStatus(ProjectStatus.ARCHIVED, id);
}

Pitfall 3 — Mix entity load và bulk modify trong cùng transaction:

// NGUY HIEM
@Transactional
public void archiveOld() {
    List<Project> active = repo.findByStatus(ACTIVE);     // managed in PC
    repo.bulkUpdateStatus(ACTIVE, ARCHIVED);               // bypass PC
    // active entities van la ACTIVE trong memory
    // commit flush: ghi de ARCHIVED thanh ACTIVE!
}
// AN TOAN — clearAutomatically = true
@Modifying(clearAutomatically = true)
@Query("UPDATE Project p SET p.status = :new WHERE p.status = :old")
int bulkUpdateStatus(@Param("old") ProjectStatus old, @Param("new") ProjectStatus newS);

Pitfall 4 — Interface projection nhầm là SQL-optimized:

Interface projection không giảm số cột SELECT. Khi cần đo performance list endpoint thực sự, dùng DTO constructor expression và bật SQL log để xác nhận.

6. Liên hệ các bài khác

  • JpaRepository & derived queries: tier 1 và 2 query — built-in method và method name grammar. Bài này là tier 3 (@Query) + projection, đọc sau bài đó để thấy cả 3 tier.
  • Specification & auditing: khi query cần dynamic (optional filter runtime), @Query với điều kiện cố định không đủ — Specification (Criteria API) build query tại runtime. Bài đó cũng cover @CreatedDate/@LastModifiedDate.
  • Bài Relationships — @OneToMany, lazy vs eager: hiểu tại sao projection quan trọng hơn khi entity có association — full entity fetch kéo theo lazy-load N+1; DTO projection cắt đứt vấn đề đó.

Tóm tắt

  • @Query mở rộng derived query cho trường hợp phức tạp: aggregate, JOIN, DB-specific feature. Dùng JPQL (query trên entity) mặc định; native SQL khi thực sự cần feature DB-specific.
  • Named parameter (:name + @Param) recommend hơn positional (?1) — refactor-friendly.
  • @Modifying bắt buộc cho UPDATE/DELETE. @Transactional bắt buộc đi kèm.
  • Bulk modify bypass persistence context — entity đang managed giữ giá trị cũ (stale). @Modifying(clearAutomatically = true) evict toàn bộ PC sau khi bulk statement chạy.
  • Projection = chỉ SELECT field cần, không load full entity. Ba loại chính: interface (declarative, không giảm SQL cột), DTO constructor expression (SQL-optimized, performance tốt nhất), class-based (hiếm).
  • Quy tắc read/write path: entity cho write (modify sau GET), DTO projection cho read (list/page).

Tự kiểm tra

Tự kiểm tra
Q1
Vì sao @Query("SELECT * FROM projects WHERE status = ?1") throw lỗi dù SQL trông đúng? Viết lại câu query cho đúng JPQL.

Lỗi vì JPQL không dùng tên table mà dùng tên entity class. Câu SELECT * FROM projects là SQL, không phải JPQL — Hibernate throw QuerySyntaxException: projects is not mapped.

JPQL đúng:

@Query("SELECT p FROM Project p WHERE p.status = ?1")
List<Project> findByStatus(ProjectStatus status);

Lưu ý thêm: JPQL dùng tên Java field (p.status) không phải tên cột DB (status ở đây trùng tên nhưng với field phức tạp hơn như createdAt vs created_at sẽ khác nhau).

Nếu muốn dùng SQL thật (SELECT *), thêm nativeQuery = true:

@Query(value = "SELECT * FROM projects WHERE status = ?1", nativeQuery = true)
List<Project> findByStatusNative(String status);

Khác biệt: JPQL refactor-friendly (entity rename → query vẫn đúng), native SQL lock table name.

Q2
Repository method sau có 2 lỗi. Chỉ ra và giải thích hệ quả nếu không sửa.
@Query("UPDATE Project p SET p.status = :s WHERE p.ownerId = :id")
int markOwnerArchived(@Param("s") ProjectStatus s, @Param("id") Long id);

Lỗi 1 — thiếu @Modifying: Spring Data mặc định treat @Query như SELECT. Gọi method này sẽ throw exception tại runtime: "Not supported for DML operations". Hibernate không cho phép chạy UPDATE mà không có @Modifying.

Lỗi 2 — thiếu @Transactional: UPDATE yêu cầu transaction đang active. Nếu caller không có @Transactional, Spring throw TransactionRequiredException. Đặt @Transactional ở repository method hoặc service method đủ.

Fix đầy đủ:

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Project p SET p.status = :s WHERE p.ownerId = :id")
int markOwnerArchived(@Param("s") ProjectStatus s, @Param("id") Long id);

clearAutomatically = true thêm vào để tránh stale entity nếu caller load entity trong cùng transaction trước khi gọi method này.

Q3
Giải thích tại sao đoạn code sau có thể ghi đè kết quả bulk update. Cơ chế nào trong Hibernate gây ra điều đó?

Vấn đề: persistence context (first-level cache của Hibernate) giữ reference đến entity đang managed trong transaction. Khi @Modifying bulk UPDATE chạy qua JDBC, nó bypass persistence context — DB row thay đổi nhưng object trong memory không được cập nhật.

Ví dụ nguy hiểm:

@Transactional
public void bad() {
  Project p = repo.findById(42L).get();  // PC map: id=42 -> p (status=ACTIVE)
  repo.bulkUpdateStatus(ACTIVE, ARCHIVED); // DB: status=ARCHIVED, PC: khong hay biet
  // p.status van = ACTIVE trong memory
  p.setName("Updated");                  // dirty checking danh dau p
  // flush khi commit:
  // UPDATE projects SET name='Updated', status='ACTIVE' WHERE id=42
  //                                    ^^^^^^^^^^^^^^^^
  // GHI DE ket qua bulk! DB quay lai ACTIVE
}

Cơ chế: khi transaction commit, Hibernate flush tất cả dirty entity trong PC. Entity p bị đánh dấu dirty (do setName), Hibernate sinh UPDATE với giá trị hiện tại của mọi field — bao gồm status=ACTIVE (giá trị cũ chưa được refresh).

Giải pháp: @Modifying(clearAutomatically = true) — sau bulk statement, Hibernate clear PC, evict mọi entity. Lần lookup tiếp theo sẽ SELECT lại từ DB, trả về giá trị mới nhất.

Q4
Interface projection (closed) và DTO constructor expression đều trả về subset field. Điểm khác biệt ở SQL level là gì? Khi nào chọn cái nào?

Interface projection (closed): Spring Data sinh proxy implement interface, nhưng Hibernate vẫn SELECT toàn bộ entity trước khi map. SQL trên DB là SELECT * FROM projects — không có SQL optimization.

// Interface projection
public interface ProjectSummary {
  Long getId();
  String getName();
}
// SQL: SELECT p.id, p.name, p.status, p.description, p.created_at, ... FROM projects p
// Map xong loai bo field thua

DTO constructor expression: JPQL SELECT new Dto(p.id, p.name) dẫn Hibernate sinh SQL chỉ với các cột được liệt kê:

@Query("SELECT new com.olhub.dto.ProjectSummary(p.id, p.name) FROM Project p WHERE p.status = :s")
List<ProjectSummary> findSummary(@Param("s") ProjectStatus s);
// SQL: SELECT p.id, p.name FROM projects p WHERE p.status = ?
// Chi 2 cot, khong load association, khong object overhead

Khi nào chọn:

  • Interface projection: muốn giới hạn field lộ ra API theo kiểu declarative, không cần viết @Query, performance không critical (nhỏ hơn 500 record/request).
  • DTO constructor expression: list/page endpoint, đặc biệt khi entity nhiều field hoặc có association — đây là mặc định 2026 cho read path.

Đo lường: bật org.hibernate.SQL: DEBUG để xem SQL thật — interface projection vs DTO projection khác nhau rõ ràng.

Q5
Bạn cần endpoint GET /projects/{ownerId}/summary trả về list gồm id, name, status, và taskCount (COUNT tasks). Viết DTO, @Query, và giải thích SQL sinh ra.

DTO:

public record ProjectOwnerSummary(
  Long id,
  String name,
  ProjectStatus status,
  long taskCount
) {}

Repository:

@Query("""
  SELECT new com.olhub.dto.ProjectOwnerSummary(
      p.id, p.name, p.status, COUNT(t.id)
  )
  FROM Project p LEFT JOIN p.tasks t
  WHERE p.ownerId = :ownerId
  GROUP BY p.id, p.name, p.status
  """)
List<ProjectOwnerSummary> findOwnerSummary(@Param("ownerId") Long ownerId);

SQL Hibernate sinh ra (Postgres):

SELECT
  p.id,
  p.name,
  p.status,
  COUNT(t.id)
FROM projects p
LEFT JOIN tasks t ON t.project_id = p.id
WHERE p.owner_id = ?
GROUP BY p.id, p.name, p.status

Đặc điểm:

  • Chỉ 4 cột được SELECT — không load description, metadata, audit field.
  • LEFT JOIN vì project không có task vẫn được trả về (taskCount = 0).
  • GROUP BY cần liệt kê đủ các cột non-aggregate.
  • Hibernate map kết quả trực tiếp sang ProjectOwnerSummary record qua constructor.

Pattern này không thể viết bằng derived query (không support aggregate) và interface projection không đủ (không tính được COUNT trong JPQL).

Bài tiếp theo: Specification & auditing

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên