每日晨間需生成各學院學生人數統計報表,發送 HTML 郵件給各部門 Leader。數據涉及 depts(學院表)與 students(學生表)跨表關聯及聚合統計,結果不對應任何單一實體表,傳統 JPA 實體映射無法直接支援。
-- 學院表
CREATE TABLE depts (
deptId INT PRIMARY KEY AUTO_INCREMENT,
deptName VARCHAR(50) NOT NULL
);
-- 學生表
CREATE TABLE students (
stuNo BIGINT PRIMARY KEY AUTO_INCREMENT,
deptId INT NOT NULL,
stuName VARCHAR(50) NOT NULL,
FOREIGN KEY (deptId) REFERENCES depts(deptId)
);
| 方案 | SQL | 缺陷 |
|---|---|---|
| GROUP BY + LEFT JOIN | SELECT b.deptId, b.deptName, COUNT(*) FROM students a LEFT JOIN depts b ON a.deptId=b.deptId GROUP BY b.deptId |
無學生的學院不顯示(內連接語義) |
| 子查詢(推薦) | SELECT a.deptId, a.deptName, (SELECT COUNT(*) FROM students b WHERE b.deptId=a.deptId) AS totalCount FROM depts a |
全量顯示所有學院,含 0 人項 |
@Entity @Table(name="depts")
public class Depts implements Serializable {
@Id @GeneratedValue @Column(name="deptId") private Integer deptId;
@Column(name="deptName", length=50) private String deptName;
// getter/setter
}
@Entity @Table(name="students")
public class Students implements Serializable {
@Id @GeneratedValue @Column(name="stuNo") private Long stuNo;
@ManyToOne @JoinColumn(name="deptId") private Depts depts;
@Column(name="stuName", length=50) private String stuName;
// getter/setter
}
public class Report implements Serializable {
private Integer deptId;
private String deptName;
private Integer totalCount;
// 必須提供:全參構造函數 + 無參構造函數
public Report() {}
public Report(Integer deptId, String deptName, Integer totalCount) {
this.deptId = deptId;
this.deptName = deptName;
this.totalCount = totalCount;
}
// getter/setter
}
核心規則:
@Service
public class ReportServiceImpl implements ReportService {
@Resource private ObjectDaoService objectDaoService;
@Override
public List<Report> getReport() {
String jpql = String.format(
"SELECT NEW %3$s(a.deptId, a.deptName, " +
"(SELECT COUNT(*) FROM %2$s b WHERE b.deptId = a.deptId) AS totalCount) " +
"FROM %1$s a",
Depts.class.getName(),
Students.class.getName(),
Report.class.getName()
);
EntityManager em = objectDaoService.getEntityManager();
TypedQuery<Report> query = em.createQuery(jpql, Report.class);
return query.getResultList();
}
}
SELECT NEW FullyQualifiedClassName(...) 語法,指示提供者實例化 DTO,而非托管實體AS totalCount 需與 DTO 字段名一致,否則映射失敗ObjectDaoService 集中注入,便於事務邊界控制與測試替換| 錯誤現象 | 根因 | 修正 |
|---|---|---|
| NoSuchMethodException | DTO 缺少對應參數構造函數 | 補全全參構造函數,檢查參數順序/類型 |
| QuerySyntaxException | JPQL 中類名未用全限定名 | 使用 Class.getName() 動態拼接 |
| 結果 totalCount 為 null | 子查詢未加 AS 別名或別名不匹配 | 確保 AS totalCount 與 DTO 字段一致 |
《Pro JPA 2: Mastering the Java Persistence API》(Apress) —— 系統講解 JPQL 進階、Criteria API、鎖機制、效能調優等核心主題,是 JPA 進階必讀經典。
JPA 視圖查詢通過 DTO 構造函數表達式 完美解決「跨表聚合不對應實體」的難題。核心要訣:SQL 先行驗證 → 設計 DTO → 編寫 JPQL NEW 子句 → 映射驗證。掌握此模式,可優雅處理報表、儀表板、異構數據整合等高頻業務場景。