← Back to list

【 Java Persistence API 】- 複雜查詢只能退回寫原生 SQL?掌握 JPQL 寫出優雅的物件導向查詢

I. 查詢的轉捩點

CoreyLi · 2026-05-15 12:01 · 0 claps · 9.4 min read paywalled
#java #jpa #spring-data-jpa #orm #sql-injection
Open on Medium ↗

【 Java Persistence API 】- 複雜查詢只能退回寫原生 SQL?掌握 JPQL 寫出優雅的物件導向查詢

I. 查詢的轉捩點

上一篇文章中,我們成功告別了手動處理 ResultSet 的痛苦,學會用 @Entity 畫出妥善的「物件與資料表對映地圖」。

靠著 Spring Data JPA 繼承 JpaRepository 的超能力,基本的 save()、findById() 或 findAll() 幾乎是一行程式碼都不用寫就能搞定。但隨著專案長大,業務邏輯也開始變得刁鑽:「可以幫我查今天註冊、狀態正常、而且暱稱包含『貓』的會員嗎?」

面對這種多條件查詢,很多人的第一反應是:「完了,JPA 內建方法不夠用,還是加個 nativeQuery = true 退回去寫原生 SQL 吧!」

雖然寫原生 SQL 很直覺,但這會讓我們失去 JPA 最大的優勢物件導向與資料庫移植性。這時候,就是我們請出 JPA 專屬查詢語言 JPQL (Java Persistence Query Language) 的最佳時機!

若無法查看文章,可點擊此連結。

此篇學習文章需要有一定的基礎,建議的基礎能力為 :

  1. 對於使用 Spring Boot 擁有基礎的概念
  2. 會應用 Maven 專案
  3. 對於 SQL 有基礎的概念

實作環境 :

  1. IntelliJ IDEA 程式碼編輯器
  2. Maria DB

案例完整程式碼檔案 : GitHub : https://github.com/KuangHung/java.git

II. 此篇我們可以了解

只要掌握 JPQL,就能用 Java 的邏輯寫出 SQL 並且通用多種關聯式Data Base:

  • 物件導向查詢思維:了解 JPQL 與傳統 SQL 的核心差異。
  • 進階實戰技巧:如何在 Repository 處理模糊搜尋、時間篩選等複雜條件。
  • 效能優化殺手鐧:運用 DTO 投影 (Constructor Expression) 減少資源消耗。
  • 架構決策:精準判斷何時該用 JPQL,何時又該果斷使用 Native SQL。

III. 進階實戰:處理複雜邏輯

資料庫實際案例 :

情境一:安全的模糊搜尋 實作關鍵字搜尋時,新手常犯的錯誤是手動拼接字串,這不僅容易出錯,還可能引發 SQL Injection。在 JPQL 中,我們可以優雅地綁定參數:

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface MemberRepository extends JpaRepository<MemberEntity, Long> {

    // 根據暱稱或 Email 進行模糊搜尋 % 符號可以直接寫在 @Query 裡面,乾淨又安全
    @Query("SELECT m FROM MemberEntity m WHERE m.nickname LIKE %:keyword% OR m.email LIKE %:keyword%")
    List<MemberEntity> searchMembers(@Param("keyword") String keyword);
}

使用案例 : MemberController、MemberService

Local test URL > localhost:8080/members/search?keyword=USER

    // MemberController --- 6. 【查】關鍵字搜尋會員 ---
    @GetMapping("/search")
    public ResponseEntity<List<MemberResponseDTO>> searchMembers(@RequestParam(required = false) String keyword) {
        List<MemberResponseDTO> dtoList = memberService.searchMembers(keyword)
                .stream()
                .map(MemberResponseDTO::fromEntity)
                .collect(Collectors.toList());
        return ResponseEntity.ok(dtoList);
    }

    // MemberService 【查】關鍵字模糊搜尋會員
    public List<MemberEntity> searchMembers(String keyword) {
        if (keyword == null || keyword.trim().isEmpty()) {
            return new ArrayList<>();
        }
        return memberRepository.searchMembers(keyword);
    }

情境二:運用內建函數處理時間 老闆想看「今天剛註冊」的會員清單,不用在 Java 層計算時間再傳進去,JPQL 有內建的標準函,底層換成 MariaDB、MySQL 還是 PostgreSQL,JPA 都會幫我們翻譯成對應資料庫的語法,完全不用改 Code!

// 找出今天註冊的活躍會員 ( status = 1 為活躍) CURRENT_DATE 是 JPQL 提供的標準函數
@Query("SELECT m FROM MemberEntity m WHERE m.status = 1 AND m.createdAt >= CURRENT_DATE")
List<MemberEntity> findTodayActiveMembers();

使用案例 : MemberController、MemberService

Local test URL > localhost:8080/members/getTodayActiveMembers

    // MemberController --- 7. 【查】找出今天註冊的活躍會員 ---
    @GetMapping("/getTodayActiveMembers")
    public ResponseEntity<List<MemberResponseDTO>> getTodayActiveMembers() {
        List<MemberResponseDTO> dtoList = memberService.getTodayActiveMembers()
                .stream()
                .map(MemberResponseDTO::fromEntity)
                .collect(Collectors.toList());
        return ResponseEntity.ok(dtoList);
    }

    // MemberService【查】找出今天註冊的活躍會員
    public List<MemberEntity> getTodayActiveMembers() {
        return memberRepository.findTodayActiveMembers();
    }

IV. 強大殺手鐧:DTO 投影(Constructor Expression)

當我們只需要在列表上顯示會員的「暱稱」和「Email」時,如果把整包 MemberEntity 都撈出來(包含加密密碼 passwordHash 等不必要的資訊),會非常浪費記憶體。

這時候,我們可以直接在 JPQL 中 new 一個 DTO 出來!

// src/main/java/com/example/demo/object/MemberInfoDto.java
public class MemberInfoDto {
    private String email;
    private String nickname;

    public MemberInfoDto(String email, String nickname) {
        this.email = email;
        this.nickname = nickname;
    }
    // ... getter 省略
}

接著在 Repository 寫下這段:

    // 查詢指定狀態的帳號 : 只撈取需要的欄位,直接封裝成 DTO
    @Query("SELECT new com.example.demo.object.MemberInfoDto(m.email, m.nickname) FROM MemberEntity m WHERE m.status = :status")
    List<MemberInfoDto> findMemberInfosByStatus(@Param("status") Integer status);

使用案例 : MemberController、MemberService

Local test URL > localhost:8080/members/findMemberInfosByStatus?status=2

    // --- MemberController 8. 【查】查詢指定狀態的帳號 ---
    @GetMapping("/findMemberInfosByStatus")
    public ResponseEntity<List<MemberInfoDto>> findMemberInfosByStatus(@RequestParam(required = false) Integer status) {
        List<MemberInfoDto> dtoList = memberService.findMemberInfosByStatus(status);
        return ResponseEntity.ok(dtoList);
    }

    // MemberService【查】查詢指定狀態的帳號
    public List<MemberInfoDto> findMemberInfosByStatus(Integer status) {
        return memberRepository.findMemberInfosByStatus(status);
    }

V. 什麼時候該使用JPQL vs. Native SQL

學了這麼多 JPQL 的好處,那我們以後都不要寫 Native SQL 了嗎?不,我們應該要了解何時選用正確的工具。

這裡提供一個參考的決策指南:

✅ 優先選擇 JPQL 的時機:

  1. 處理常規的業務邏輯、多條件過濾。
  2. 專案可能面臨切換資料庫的風險(例如本機開發用MS Sql,正式環境用 MariaDB)。
  3. 希望查詢結果能自動且無縫地封裝為 Java 物件 (Entity 或 DTO)。

⚠️ 果斷切換 Native SQL 的時機(nativeQuery = true):

  1. 使用資料庫專屬語法:例如需要用到 MariaDB 特有的 JSON 處理函數、或是其他資料庫的專屬語法。
  2. 極致的效能調優:面對極度複雜的跨表統計報表,有時候手寫優化過的複雜 SQL,並使用特定的 Index Hint,效能會比 JPA 翻譯出來的好。

VI. 總結 :

從 JDBC 轉向 JPA 的過程中,最難轉換的往往不是語法,而是大腦的思維模式。

把 JPQL 當作是與資料庫溝通的「高級翻譯官」,習慣對著「物件」下指令後,將會發現程式碼變得更加精煉且易於維護。下一次遇到複雜查詢時,先別急著退回原生 SQL,試著用 JPQL 優雅地解決它吧!

把這招技巧收進我們的武器庫!以後不管遇到多嚴苛的效能需求,都能寫出含金量高的查詢,安心推 Code、準時下班啦!實作上如果還有卡卡的地方,隨時來找我討論喔!加油!\ ^@^/


메타데이터
post_id
a67bc4a3c2a9
slug
java-persistence-api-複雜查詢只能退回寫原生-sql-掌握-jpql-寫出優雅的物件導向查詢-a67bc4a3c2a9
url
https://medium.com/@a526629/java-persistence-api-%E8%A4%87%E9%9B%9C%E6%9F%A5%E8%A9%A2%E5%8F%AA%E8%83%BD%E9%80%80%E5%9B%9E%E5%AF%AB%E5%8E%9F%E7%94%9F-sql-%E6%8E%8C%E6%8F%A1-jpql-%E5%AF%AB%E5%87%BA%E5%84%AA%E9%9B%85%E7%9A%84%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E6%9F%A5%E8%A9%A2-a67bc4a3c2a9
canonical_url
https://medium.com/@a526629/java-persistence-api-%E8%A4%87%E9%9B%9C%E6%9F%A5%E8%A9%A2%E5%8F%AA%E8%83%BD%E9%80%80%E5%9B%9E%E5%AF%AB%E5%8E%9F%E7%94%9F-sql-%E6%8E%8C%E6%8F%A1-jpql-%E5%AF%AB%E5%87%BA%E5%84%AA%E9%9B%85%E7%9A%84%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E6%9F%A5%E8%A9%A2-a67bc4a3c2a9
author_url
https://medium.com/@a526629
status
ok
fetched_at
2026-06-11 17:15:47