Hibernate/JPA 提供靜態定義、預編譯、可復用的查詢定義機制:@NamedQuery (JPQL/HQL)、@NamedNativeQuery (原生 SQL) 配合 @SqlResultSetMapping 映射結果集。本文系統講解定義語法、參數綁定、Hint 優化、結果映射、調用方式,解決動態拼接 SQL 的維護痛點與注入風險。
@Entity
@NamedQueries({
@NamedQuery(name = "User.findByEmail",
query = "SELECT u FROM User u WHERE u.email = :email"),
@NamedQuery(name = "User.findActiveByRole",
query = "SELECT u FROM User u WHERE u.status = 1 AND u.role = :role",
hints = {
@QueryHint(name = "org.hibernate.cacheable", value = "true"), // 啟用查詢緩存
@QueryHint(name = "org.hibernate.timeout", value = "5000"), // 5秒超時
@QueryHint(name = "org.hibernate.fetchSize", value = "50"), // JDBC fetch size
@QueryHint(name = "org.hibernate.readOnly", value = "true") // 僅讀優化
})
})
public class User { ... }
| 屬性 | 說明 | 示例 |
|---|---|---|
| name | 全局唯一查詢名 (建議 Entity.method 格式) | "User.findByEmail" |
| query | JPQL/HQL 語句,支持命名參數 :param | "SELECT u FROM User u WHERE u.email = :email" |
| hints | 查詢提示 (Vendor-specific),常用 Hibernate Hint | 見下表 |
| lockMode | 鎖模式 (PESSIMISTIC_WRITE 等) | LockModeType.PESSIMISTIC_WRITE |
| Hint 名稱 | 作用 | 典型值 |
|---|---|---|
| org.hibernate.cacheable | 啟用查詢緩存 | true/false |
| org.hibernate.cacheRegion | 指定查詢緩存區域 | "user.query.cache" |
| org.hibernate.timeout | 查詢超時 (毫秒) | 5000 |
| org.hibernate.fetchSize | JDBC Statement fetch size | 50/100 |
| org.hibernate.readOnly | 實體加載為只讀 (無脏檢查開銷) | true/false |
| org.hibernate.comment | SQL 註釋 (便於數據庫審計) | "User.findByEmail" |
| org.hibernate.flushMode | Flush 模式 (MANUAL/ALWAYS/COMMIT) | FlushModeType.COMMIT |
// EntityManager / Session 通用
TypedQuery query = em.createNamedQuery("User.findByEmail", User.class);
query.setParameter("email", "test@example.com");
User user = query.getSingleResult();
// Spring Data JPA 直接在 Repository 定義
// @Query(nativeQuery = false) 不需要,直接方法命名規約或 @NamedQuery 引用
用於:複雜 SQL、存儲過程調用、特定數據庫特性、性能調優、非實體映射 (DTO/報表)。
@Entity
@NamedNativeQueries({
@NamedNativeQuery(
name = "User.findWithNative",
query = "SELECT id, username, email, created_at FROM t_user WHERE status = 1",
resultSetMapping = "UserMapping"
)
})
@SqlResultSetMappings({
@SqlResultSetMapping(
name = "UserMapping",
entities = @EntityResult(entityClass = User.class) // 自動映射同名列
)
})
public class User { ... }
@SqlResultSetMapping(
name = "UserPartialMapping",
entities = @EntityResult(
entityClass = User.class,
fields = {
@FieldResult(name = "id", column = "user_id"),
@FieldResult(name = "username", column = "user_name"),
@FieldResult(name = "email", column = "email_addr")
// 未列出字段將不填充 (或用默認值)
}
)
)
@SqlResultSetMapping(
name = "UserSummaryDTOMapping",
classes = @ConstructorResult(
targetClass = UserSummaryDTO.class, // 非實體類,需有對應構造函數
columns = {
@ColumnResult(name = "id", type = Long.class),
@ColumnResult(name = "username", type = String.class),
@ColumnResult(name = "email", type = String.class),
@ColumnResult(name = "order_count", type = Long.class)
}
)
)
@NamedNativeQuery(
name = "User.findSummary",
query = "SELECT u.id, u.username, u.email, COUNT(o.id) as order_count " +
"FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id " +
"GROUP BY u.id, u.username, u.email",
resultSetMapping = "UserSummaryDTOMapping"
)
// DTO 類
public class UserSummaryDTO {
private Long id;
private String username;
private String email;
private Long orderCount;
public UserSummaryDTO(Long id, String username, String email, Long orderCount) { ... }
}
// 返回實體
List users = em.createNamedQuery("User.findWithNative", User.class).getResultList();
// 返回 DTO
List summaries = em.createNamedQuery("User.findSummary", UserSummaryDTO.class).getResultList();
// 注意:返回類型必須與 ConstructorResult targetClass 一致
@SqlResultSetMapping(
name = "UserWithDepartmentMapping",
entities = {
@EntityResult(entityClass = User.class, discriminatorColumn = "u_"),
@EntityResult(entityClass = Department.class, discriminatorColumn = "d_")
}
)
@NamedNativeQuery(
name = "User.findWithDept",
query = "SELECT u.id as u_id, u.username as u_username, d.id as d_id, d.name as d_name " +
"FROM t_user u JOIN t_dept d ON u.dept_id = d.id",
resultSetMapping = "UserWithDepartmentMapping"
)
// 調用返回 Object[] 數組:[User, Department]
List
| 場景 | 推薦方案 | 避坑點 |
|---|---|---|
| 簡單 JPQL 查詢 | @NamedQuery + Hint 優化 | 動態拼接字符串 → 注入風險、無預編譯 |
| 複雜報表/多表 JOIN | @NamedNativeQuery + ConstructorResult 映射 DTO | 實體映射部分字段 → 未映射字段為 null 導致 N+1 |
| 存儲過程調用 | @NamedNativeQuery + callable=true (Hibernate) | 參數註冊 OUT/INOUT 需 Hibernate 專用 API |
| 查詢緩存 | @NamedQuery + hint cacheable=true + 實體 @Cache | 忘記實體 @Cache → 查詢緩存命中但實體仍查庫 |
| 參數綁定 | 命名參數 :param (JPQL) / ?1 (Native) | Native SQL 位置參數易錯位,建議用命名參數 (Hibernate 支持) |
| 結果集列名衝突 | SQL 別名 u.id as u_id + discriminatorColumn | 多實體同名列導致映射錯誤 |
@NamedQuery 為 JPQL 提供預編譯、類型安全、可緩存、可 Hint 優化的靜態定義;@NamedNativeQuery + @SqlResultSetMapping 解決原生 SQL 映射實體/DTO/多實體的復雜場景。兩者均在啟動階段解析驗證,運行時零解析開銷。現代 Spring Data JPA 雖提供方法命名規約與 @Query 註解,但對於跨 Repository 復用、複雜 Hint 組合、原生 SQL 映射 DTO等場景,註解映射查詢仍是不可替代的利器。