JdbcTemplate and Spring Data JPA Repository
最近在整理幾個 Spring Boot 專案的 package 命名時,又遇到 dao、repo、repository 要怎麼放的問題。
如果只看「是不是 Web 專案」,很容易下錯結論。比較關鍵的差異其實是資料存取方式:
- 單 DB +
JdbcTemplate+ 手寫 SQL - 單 DB + Spring Data JPA repository
- 多 DB + 每個 DB 各自切 JPA repository / SQL repository
JdbcTemplate
JdbcTemplate 比較像是把 JDBC 的連線、statement、result set、exception 這些樣板流程收起來,但 SQL 還是由程式自己寫。
在 Spring Framework JDBC Core 這邊有講到:
1
JdbcTemplate is the central class in the JDBC core package.
同一段也提到它會處理資源建立與釋放,避免忘記關 connection 這類常見錯誤。
所以原本要寫很複雜的 JDBC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("""
SELECT UYEAR, TEACHER_RECEIPT_DUE_DATE
FROM NP_TS_REMINDER_SETTING
WHERE UYEAR = ?
""");
statement.setString(1, uyear);
ResultSet rs = statement.executeQuery();
try {
if (!rs.next()) {
return Optional.empty();
}
return Optional.of(new ReminderSetting(
rs.getString("UYEAR"),
rs.getDate("TEACHER_RECEIPT_DUE_DATE").toLocalDate()
));
} finally {
rs.close();
statement.close();
connection.close();
}
用了 JdbcTemplate 後,可以簡化成:
1
2
3
4
5
6
7
8
9
return jdbcTemplate.query(sql, rs -> {
if (!rs.next()) {
return Optional.empty();
}
return Optional.of(new ReminderSetting(
rs.getString("UYEAR"),
toLocalDate(rs.getTimestamp("TEACHER_RECEIPT_DUE_DATE"))
));
}, uyear);
這種寫法適合:
- 查詢以既有資料表為主,欄位命名不一定完全符合 Java model
- SQL 條件需要精準控制
- 不想為了少數查詢建立完整 JPA Entity 關聯
- batch 或報表程式需要直接看懂實際 SQL
因此這類 package 叫 dao 可以接受;若團隊想統一叫 repo,也應該再分出 repo.xxx.sql,避免和 Spring Data JPA repository 混在一起。
Spring Data JPA Repository
Spring Data JPA repository 的重點不是「不用寫 SQL」而已,而是把資料表映射成 Entity,讓 repository 介面代表某個 aggregate 或資料模型的存取入口。
在 Spring Data JPA 這邊有講到:
1
Spring Data JPA provides repository support for the Jakarta Persistence API (JPA).
所以很多 Spring Data JPA 專案會把這類介面放在 repository 或縮寫成 repo package。這不是 Java 或 Spring Boot 強制規定的 package 名稱,而是從 Spring Data 的 Repository 抽象、掃描規則與範例命名延伸出來的慣例。
在 Spring Guide: Accessing Data with JPA 這邊有講到:
1
Spring Data JPA focuses on using JPA to store data in a relational database.
同一份 guide 接著建立的介面就叫 CustomerRepository:
1
2
3
4
5
6
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
Customer findById(long id);
}
也就是說,正統 Spring Data JPA 寫法的核心是「Repository interface」,而不是「一定要有一個叫 repo 的 package」。只是實務上為了讓掃描邊界與職責清楚,常會把這些 interface 集中放在 repository 或 repo package。
在 EnableJpaRepositories API 這邊有講到:
1
Will scan the package of the annotated configuration class for Spring Data repositories by default.
這也是多 DB 專案常把 package 切成 repo.ntnupmb.jpa、repo.ntnusalary.jpa 的原因:不是為了好看,而是讓每個 EntityManagerFactory 只掃到自己那一組 repository。
如果是一般 CRUD,原本可能要自己寫 insert、update、select by id、delete 這些固定程式碼;用了 JpaRepository 後,可以簡化成:
1
2
public interface NpTsRecordRepository extends JpaRepository<NpTsRecord, Long> {
}
Service 就可以直接使用:
1
2
3
4
NpTsRecord record = repository.findById(recordSn)
.orElseThrow(() -> new IllegalArgumentException("查無資料"));
record.setTeacherRecvDate(LocalDateTime.now());
repository.saveAndFlush(record);
查詢條件不複雜時,也可以用 method name 表達。
在 Spring Data JPA Query Methods 這邊有講到:
1
Spring Data tries to resolve a call to these methods to a named query
同份文件也說明 repository 可以直接用 @Query 把查詢綁在方法上:
1
2
3
4
5
6
7
8
9
public interface PersonBasicRepository extends JpaRepository<PersonBasic, Long> {
@Query("""
select p
from PersonBasic p
where p.loginId = :loginId
""")
Optional<PersonBasic> findByLoginId(String loginId);
}
這種寫法適合:
- 專案已經有明確 Entity
- 資料存取常常圍繞同一個 domain model
- CRUD 與簡單查詢很多
- 想讓 transaction、dirty checking、entity mapping 交給 JPA
單 DB 時怎麼取名
如果專案是單 DB,通常不用把 DB 名稱塞進 package。可以簡單分成:
1
2
3
tw.edu.ntnu.itc.project.dao
tw.edu.ntnu.itc.project.service
tw.edu.ntnu.itc.project.model
或如果是 JPA 專案:
1
2
3
tw.edu.ntnu.itc.project.dao
tw.edu.ntnu.itc.project.repo
tw.edu.ntnu.itc.project.service
但要注意:dao 和 repo 不只是換名字。若 repo 裡面全是 Spring Data JPA interface,讀程式的人會預期它是 JPA repository;若裡面其實都是 JdbcTemplate 手寫 SQL,名稱就會造成誤解。
多 DB 時怎麼取名
多 DB 專案要再清楚一點,因為每個 DB 可能有自己的 DataSource、EntityManagerFactory、TransactionManager、JdbcTemplate。
比較清楚的切法是:
1
2
3
4
5
6
7
tw.edu.ntnu.itc.project.dao.ntnupmb
tw.edu.ntnu.itc.project.repo.ntnupmb.jpa
tw.edu.ntnu.itc.project.repo.ntnupmb.sql
tw.edu.ntnu.itc.project.dao.ntnusalary
tw.edu.ntnu.itc.project.repo.ntnusalary.jpa
tw.edu.ntnu.itc.project.repo.ntnusalary.sql
這樣一看就知道:
dao.ntnupmb是 ntnupmb 的 Entityrepo.ntnupmb.jpa是 ntnupmb 的 Spring Data JPA repositoryrepo.ntnupmb.sql是 ntnupmb 的JdbcTemplate/ native SQL repository
對幾個專案的對照
TsNoticeDownload 是單 DB Web 專案,主要使用 Spring Data JPA repository,所以 package 叫 repo 很合理。
TsNoticeApproval 是多 DB 範本,因此拆成 repo.ntnupmb.jpa 與 repo.ntnupmb.sql;這是為了讓不同 DB、不同資料存取技術的邊界更清楚。
TsNoticeReminder 是 CLI batch,但 CLI 不是它使用 dao 的真正理由。真正原因是它目前使用單 DB JdbcTemplate 手寫 SQL,而且這些查詢只是把設定、收件人與寄信紀錄整理出來,沒有要把查到的資料當成長生命週期 Entity 繼續交給 JPA 做 dirty checking、關聯載入或 cascade。
換句話說,這裡用 JdbcTemplate 而不是 Spring Data JPA,是因為查詢目的很直接:查出資料、組成 batch 所需的 DTO 或 record,接著寄信與寫 log。若為了這種流程硬補 Entity 與 JPA repository,反而會讓資料存取層變重。
如果想和 TsNoticeDownload 對齊命名,可以改叫 repo;但比較精確的作法應該是先決定它是不是要走 Spring Data JPA。
若仍維持 JdbcTemplate,我會傾向:
1
tw.edu.ntnu.itc.tsnoticereminder.dao
如果團隊想統一所有資料存取 package 都叫 repo,則可以用:
1
tw.edu.ntnu.itc.tsnoticereminder.repo
但不要讓人誤以為它是 Spring Data JPA repository。若未來真的要擴成多 DB,再改成:
1
tw.edu.ntnu.itc.tsnoticereminder.repo.ntnupmb.sql
結論
命名不是看 Web 或 CLI,而是看資料存取抽象層。
JdbcTemplate:SQL 是核心,Java 程式負責 mapping,dao或repo.xxx.sql比較清楚。- Spring Data JPA:Entity 與 repository 是核心,
repo或repo.xxx.jpa比較自然。 - 多 DB:package 名稱最好帶 DB 名稱與技術類型,避免 service 注入錯資料來源。
回到專案命名,如果只是想讓 TsNoticeReminder 對齊 TsNoticeDownload,那應先問:要對齊的是「package 字面名稱」,還是「Spring Data JPA repository 風格」。前者只是搬 package;後者會牽涉 Entity、Repository interface、transaction 與測試策略,影響範圍完全不同。