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
AndfindByNameAndEmailWHERE name = ? AND email = ?
OrfindByNameOrEmailWHERE name = ? OR email = ?
BetweenfindByLevelBetweenWHERE level BETWEEN ? AND ?
LessThanfindByExpLessThanWHERE exp < ?
LessThanEqualfindByExpLessThanEqualWHERE exp <= ?
GreaterThanfindByLevelGreaterThanWHERE level > ?
GreaterThanEqualfindByLevelGreaterThanEqualWHERE level >= ?
IsNullfindByAvatarUrlIsNullWHERE avatar_url IS NULL
IsNotNullfindByAvatarUrlIsNotNullWHERE avatar_url IS NOT NULL
LikefindByNameLikeWHERE name LIKE ?
ContainingfindByNameContainingWHERE name LIKE %?%
StartingWithfindByNameStartingWithWHERE name LIKE ?%
EndingWithfindByNameEndingWithWHERE name LIKE %?
OrderByfindByOrderByExpDescORDER BY exp DESC
InfindByIdIn(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#

JPQLSQL
使用 Entity 類別名使用資料表名
使用 Java 欄位名使用資料庫欄位名
User uusers u
u.emailu.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 在啟動時會:

  1. 解析方法名稱
  2. 產生對應的 JPQL
  3. 編譯為 SQL
  4. 快取執行計劃

所以運行時效能跟手寫 SQL 一樣。

Q: 什麼時候用 @Query#

  • 複雜的 JOIN 查詢
  • 聚合函數(COUNT, SUM, AVG)
  • 方法命名太長不易讀時
  • 需要使用資料庫特定功能時

下一步#

理解 Repository 後,接下來看 Service 層 了解業務邏輯如何組織。