Repository 層詳解#
Repository 層負責資料存取,等同於 Django 的 ORM 和 Manager。Spring Data JPA 提供了強大的方法命名查詢功能。
檔案位置#
backend/src/main/java/tw/waterballsa/academy/repository/
├── UserRepository.java
├── CourseRepository.java
├── MissionRepository.java
├── MissionProgressRepository.java
├── CourseEnrollmentRepository.java
├── GymRepository.java
├── ChallengeRepository.java
├── SubmissionRepository.java
└── ...核心概念#
1. JpaRepository 介面#
只需要定義 interface 並繼承 JpaRepository,Spring 會自動產生實作:
public interface UserRepository extends JpaRepository<User, Long> {
// User: Entity 類型
// Long: 主鍵類型
// 不需要寫任何實作!
}Django 對照:
# Django 自動提供 objects manager
User.objects.all() # JpaRepository 的 findAll()
User.objects.get(id=1) # JpaRepository 的 findById(1)
User.objects.create(...) # JpaRepository 的 save()2. 內建方法#
繼承 JpaRepository 後自動擁有以下方法:
| JpaRepository 方法 | Django ORM 對照 | 說明 |
|---|---|---|
findAll() | Model.objects.all() | 取得全部 |
findById(id) | Model.objects.get(id=id) | 依 ID 查詢 |
save(entity) | instance.save() | 新增或更新 |
delete(entity) | instance.delete() | 刪除 |
deleteById(id) | Model.objects.filter(id=id).delete() | 依 ID 刪除 |
count() | Model.objects.count() | 計數 |
existsById(id) | Model.objects.filter(id=id).exists() | 存在檢查 |
// 使用範例
User user = userRepository.findById(1L)
.orElseThrow(() -> new RuntimeException("User not found"));
userRepository.save(user); // 有 ID 就更新,沒 ID 就新增
userRepository.delete(user);方法命名查詢#
這是 Spring Data JPA 最強大的功能! 只要按照命名規則定義方法,框架會自動產生 SQL。
基本規則#
public interface UserRepository extends JpaRepository<User, Long> {
// findBy + 欄位名 → SELECT * FROM users WHERE email = ?
Optional<User> findByEmail(String email);
// existsBy + 欄位名 → SELECT EXISTS(SELECT 1 FROM users WHERE email = ?)
boolean existsByEmail(String email);
// findBy + 欄位 + And + 欄位 → WHERE ... AND ...
Optional<User> findByEmailAndPassword(String email, String password);
// findBy + 欄位 + Or + 欄位 → WHERE ... OR ...
List<User> findByNameOrEmail(String name, String email);
}Django 對照:
# findByEmail
User.objects.get(email=email)
# existsByEmail
User.objects.filter(email=email).exists()
# findByEmailAndPassword
User.objects.get(email=email, password=password)完整命名規則表#
| 關鍵字 | 範例 | 產生的 SQL |
|---|---|---|
And | findByNameAndEmail | WHERE name = ? AND email = ? |
Or | findByNameOrEmail | WHERE name = ? OR email = ? |
Between | findByLevelBetween | WHERE level BETWEEN ? AND ? |
LessThan | findByExpLessThan | WHERE exp < ? |
LessThanEqual | findByExpLessThanEqual | WHERE exp <= ? |
GreaterThan | findByLevelGreaterThan | WHERE level > ? |
GreaterThanEqual | findByLevelGreaterThanEqual | WHERE level >= ? |
IsNull | findByAvatarUrlIsNull | WHERE avatar_url IS NULL |
IsNotNull | findByAvatarUrlIsNotNull | WHERE avatar_url IS NOT NULL |
Like | findByNameLike | WHERE name LIKE ? |
Containing | findByNameContaining | WHERE name LIKE %?% |
StartingWith | findByNameStartingWith | WHERE name LIKE ?% |
EndingWith | findByNameEndingWith | WHERE name LIKE %? |
OrderBy | findByOrderByExpDesc | ORDER BY exp DESC |
In | findByIdIn(List<Long> ids) | WHERE id IN (?, ?, ?) |
實際範例#
// MissionProgressRepository.java
public interface MissionProgressRepository extends JpaRepository<MissionProgress, Long> {
// 查詢特定用戶特定任務的進度
// → SELECT * FROM mission_progress WHERE user_id = ? AND mission_id = ?
Optional<MissionProgress> findByUserIdAndMissionId(Long userId, Long missionId);
// 查詢用戶所有進度
// → SELECT * FROM mission_progress WHERE user_id = ?
List<MissionProgress> findByUserId(Long userId);
// 查詢用戶已完成的任務
// → SELECT * FROM mission_progress WHERE user_id = ? AND completed = ?
List<MissionProgress> findByUserIdAndCompleted(Long userId, Boolean completed);
}// CourseEnrollmentRepository.java
public interface CourseEnrollmentRepository extends JpaRepository<CourseEnrollment, Long> {
// 查詢用戶是否報名特定課程
Optional<CourseEnrollment> findByUserIdAndCourseId(Long userId, Long courseId);
// 查詢用戶所有報名紀錄
List<CourseEnrollment> findByUserId(Long userId);
// 查詢用戶特定狀態的報名
List<CourseEnrollment> findByUserIdAndStatus(Long userId, PaymentStatus status);
// 檢查是否存在
boolean existsByUserIdAndCourseIdAndStatus(Long userId, Long courseId, PaymentStatus status);
}自訂 JPQL 查詢#
當方法命名無法滿足需求時,使用 @Query 註解寫 JPQL:
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL:使用 Entity 名稱和欄位名(不是資料表名)
@Query("SELECT u FROM User u ORDER BY u.exp DESC")
List<User> findTopByOrderByExpDesc();
}public interface MissionProgressRepository extends JpaRepository<MissionProgress, Long> {
// 複雜的 JOIN 查詢
@Query("SELECT COUNT(mp) FROM MissionProgress mp " +
"JOIN mp.mission m " +
"JOIN m.chapter ch " +
"WHERE mp.user.id = :userId " +
"AND ch.course.id = :courseId " +
"AND mp.rewardClaimed = true")
Long countCompletedMissionsByCourse(Long userId, Long courseId);
// 統計課程總任務數
@Query("SELECT COUNT(m) FROM Mission m " +
"JOIN m.chapter ch " +
"WHERE ch.course.id = :courseId")
Long countTotalMissionsByCourse(Long courseId);
}Django 對照:
# countCompletedMissionsByCourse
MissionProgress.objects.filter(
user_id=user_id,
mission__chapter__course_id=course_id,
reward_claimed=True
).count()
# countTotalMissionsByCourse
Mission.objects.filter(chapter__course_id=course_id).count()JPQL vs SQL#
| JPQL | SQL |
|---|---|
| 使用 Entity 類別名 | 使用資料表名 |
| 使用 Java 欄位名 | 使用資料庫欄位名 |
User u | users u |
u.email | u.email(可能不同) |
原生 SQL 查詢#
需要使用資料庫特定功能時:
@Query(value = "SELECT * FROM users WHERE LOWER(name) LIKE %:keyword%",
nativeQuery = true)
List<User> searchByNameNative(@Param("keyword") String keyword);分頁與排序#
分頁#
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByLevel(int level, Pageable pageable);
}
// 使用方式
Pageable pageable = PageRequest.of(0, 10, Sort.by("exp").descending());
Page<User> page = userRepository.findByLevel(10, pageable);
page.getContent(); // 當前頁資料
page.getTotalPages(); // 總頁數
page.getTotalElements(); // 總筆數Django 對照:
from django.core.paginator import Paginator
queryset = User.objects.filter(level=10).order_by('-exp')
paginator = Paginator(queryset, 10)
page = paginator.page(1)排序#
// 方法命名
List<User> findByOrderByExpDesc();
// 或使用 Sort 參數
List<User> findAll(Sort sort);
userRepository.findAll(Sort.by("exp").descending());Optional 處理#
Spring Data JPA 回傳 Optional<T> 表示可能找不到資料:
Optional<User> findByEmail(String email);
// 使用方式 1: orElseThrow
User user = userRepository.findByEmail("test@example.com")
.orElseThrow(() -> new RuntimeException("User not found"));
// 使用方式 2: orElse
User user = userRepository.findByEmail("test@example.com")
.orElse(null);
// 使用方式 3: ifPresent
userRepository.findByEmail("test@example.com")
.ifPresent(user -> {
// 找到時執行
});
// 使用方式 4: orElseGet
User user = userRepository.findByEmail("test@example.com")
.orElseGet(() -> createDefaultUser());Django 對照:
# Django 用 DoesNotExist 例外
try:
user = User.objects.get(email='test@example.com')
except User.DoesNotExist:
user = None
# 或用 first()
user = User.objects.filter(email='test@example.com').first()完整範例#
// repository/CourseEnrollmentRepository.java
public interface CourseEnrollmentRepository extends JpaRepository<CourseEnrollment, Long> {
// 基本查詢
Optional<CourseEnrollment> findByUserIdAndCourseId(Long userId, Long courseId);
List<CourseEnrollment> findByUserId(Long userId);
List<CourseEnrollment> findByUserIdAndStatus(Long userId, PaymentStatus status);
// 存在檢查
boolean existsByUserIdAndCourseIdAndStatus(Long userId, Long courseId, PaymentStatus status);
// 自訂 JPQL
@Query("SELECT COUNT(ce) FROM CourseEnrollment ce " +
"WHERE ce.course.id = :courseId AND ce.status = 'COMPLETED'")
Long countEnrolledStudents(Long courseId);
}// 在 Service 中使用
@Service
@RequiredArgsConstructor
public class EnrollmentService {
private final CourseEnrollmentRepository enrollmentRepository;
public boolean isEnrolled(Long userId, Long courseId) {
return enrollmentRepository.existsByUserIdAndCourseIdAndStatus(
userId, courseId, PaymentStatus.COMPLETED
);
}
public List<CourseEnrollment> getUserEnrollments(Long userId) {
return enrollmentRepository.findByUserIdAndStatus(
userId, PaymentStatus.COMPLETED
);
}
}常見問題#
Q: 為什麼 Repository 是 interface 不是 class?#
Spring Data JPA 在啟動時自動掃描繼承 JpaRepository 的 interface,並動態產生實作類別(使用 Java Proxy)。你只需要定義方法簽名,框架會自動實作。
Q: 方法命名查詢的效能如何?#
很好!Spring 在啟動時會:
- 解析方法名稱
- 產生對應的 JPQL
- 編譯為 SQL
- 快取執行計劃
所以運行時效能跟手寫 SQL 一樣。
Q: 什麼時候用 @Query?#
- 複雜的 JOIN 查詢
- 聚合函數(COUNT, SUM, AVG)
- 方法命名太長不易讀時
- 需要使用資料庫特定功能時
下一步#
理解 Repository 後,接下來看 Service 層 了解業務邏輯如何組織。