# GraphQL:讓 API 查詢更精準的查詢語言
## 為什麼寫這篇
最近在接一個專案,後端 API 設計遇到瓶頸。
一個商品列表頁面,手機版只需要顯示商品名稱和價格,但網頁版要顯示完整資訊(評價、庫存、描述等)。用 REST API 的話,要嘛回傳全部資料讓手機浪費流量,要嘛做兩個不同的 endpoint 增加維護成本。
還有更麻煩的:前端要顯示「訂單詳情」,需要呼叫 3 個 API:
- `/orders/{id}` 取得訂單基本資訊
- `/users/{userId}` 取得客戶資料
- `/products/{productId}` 取得每個商品詳情
這就是傳說中的 **N+1 查詢問題**。
後來突然想起之前有聽過 GraphQL,說它可以「想要什麼就拿什麼」。研究後發現這東西確實解決了 REST 的很多痛點,但也帶來新的複雜度。這篇筆記記錄了 GraphQL 的核心概念和實際應用。
---
## 如果只用 REST API 會怎樣?
想像你去餐廳點餐。REST API 就像套餐——廚師決定套餐有什麼,你不能說「我不要沙拉,多給我一點薯條」。要嘛全部接受,要嘛換別的套餐。
### REST 的典型問題
**過度取得 (Over-fetching)**
就像點了一個套餐,結果送來一堆你不想吃的配菜。API 回傳了 30 個欄位,但手機版只需要顯示 3 個。剩下的 27 個欄位白白浪費了使用者的網路流量。在 4G/5G 吃到飽的時代可能還好,但如果使用者在國外漫遊,或是在網路不穩的環境,這就是個大問題。
**不足取得 (Under-fetching)**
這更麻煩。就像你點了漢堡,發現沒有薯條,要再點一份。吃到一半發現沒飲料,又要再點。每次點餐都要排隊等待。
在程式的世界,這代表你需要發送多次請求才能組合出一個完整的頁面。每次請求都有網路延遲,使用者體驗就像在看投影片,一張一張慢慢載入。
**版本管理地獄**
REST API 更新版本是個大工程。你有 `/api/v1/users`,後來發現要加新欄位,但不能直接改,因為舊版 App 還在用。於是你做了 `/api/v2/users`。一年後又要改,變成 `/api/v3/users`。
最後你的後端要同時維護三個版本的 API,每次改 bug 要改三個地方。名副其實的版本地獄QQ。
---
## GraphQL 是什麼?
GraphQL 不是資料庫,也不是程式語言,而是一種「查詢語言」。
把它想像成去自助餐廳。你拿著盤子,想要什麼就夾什麼。要兩塊雞排?可以。不要青菜?沒問題。這就是 GraphQL 的精神:**客戶端決定要什麼資料**。
### 從一個簡單的例子開始
假設你要顯示使用者的個人頁面。用 REST 的話:
```
GET /api/users/123
回傳一大包資料:
{
"id": 123,
"name": "Moofon",
"email": "moofon0222@gmail.com",
"phone": "0912345678",
"address": "...",
"created_at": "2025-10-14",
"updated_at": "2024-11-14",
"last_login": "...",
// ... 還有 20 個欄位
}
```
但如果你只需要顯示名字和 email,那其他欄位都是浪費。
GraphQL 的做法:
```graphql
{
user(id: 123) {
name
email
}
}
```
回傳的資料:
```json
{
"data": {
"user": {
"name": "Moofon",
"email": "moofon0222@gmail.com"
}
}
}
```
看到差異了嗎?你要什麼,就給你什麼,不多不少。
### 更複雜的例子:關聯資料
這才是 GraphQL 真正強大的地方。假設你要顯示一個部落格文章,包含作者資訊和留言。
REST 的做法需要多次請求:
```
1. GET /posts/456 → 取得文章
2. GET /users/789 → 取得作者資料
3. GET /posts/456/comments → 取得留言列表
4. GET /users/111 → 取得留言者 1 的資料
5. GET /users/222 → 取得留言者 2 的資料
...
```
五個請求!如果有 10 則留言,可能要發 13 個請求。
GraphQL 一次搞定:
```graphql
{
post(id: 456) {
title # 1. 我要編號 456 的文章
content # 2. 給我文章的標題
author {
name # 3. 給我作者的名字
avatar # 4. 作者的頭像
}
comments {
content # 5. 每則留言的內容
createdAt # 6. 留言建立時間
author {
name # 7. 留言者的名字
}
}
}
}
```
一個請求,拿到所有需要的資料。而且你注意到了嗎?文章作者我們要 `name` 和 `avatar`,但留言作者只要 `name`。這種細緻的控制,REST 做不到。
---
## GraphQL 的三大操作
GraphQL 不只是查詢,它有三種操作類型,對應不同的使用情境。
### 1. Query(查詢)
Query 就像 REST 的 GET,用來取得資料。但它更靈活,可以精確指定要什麼欄位,還可以一次查詢多個資源。
想像你在建立一個儀表板頁面,需要顯示多種資訊:當前使用者、今日訂單、低庫存商品。用 REST 你要發三個請求,但 GraphQL 可以這樣:
```graphql
query DashboardData {
currentUser {
name
unreadNotifications
}
todayOrders {
count
totalRevenue
}
lowStockProducts {
name
remaining
}
}
```
一個查詢,三種資料,一次到位。
### 2. Mutation(變更)
Mutation 對應 REST 的 POST、PUT、DELETE,用來修改資料。名字取得很好——"mutation" 就是「變化」的意思(順便學一下英文xd)。
創建新使用者的例子:
```graphql
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
createdAt
}
}
```
注意到了嗎?即使是修改操作,你還是可以指定要回傳什麼資料。這在 REST 中通常要再發一個 GET 請求才能拿到。
### 3. Subscription(訂閱)
這是 REST 做不到的:即時更新。
想像你在做一個拍賣網站,使用者需要即時看到出價更新。用 REST 你只能不斷輪詢(每秒問一次「有新出價嗎?」),但 GraphQL Subscription 可以這樣:
```graphql
subscription OnNewBid($auctionId: ID!) {
bidPlaced(auctionId: $auctionId) {
amount
bidder {
name
}
timestamp
}
}
```
當有新出價時,伺服器會主動推送資料給客戶端。就像訂閱 YouTube 頻道,有新影片會自動通知你。
**補充一下:** 這點我在寫的時候其實發現與Webhook滿像的,概念上都是「事件發生 → 通知」,但機制不太一樣:
**GraphQL Subscription**:客戶端主動透過 WebSocket 長連線與伺服器保持連線,伺服器有事件時直接推送到該連線。適用於即時的畫面更新(如聊天室、拍賣即時報價)。
**Webhook**:伺服器主動向外部服務發送一個 HTTP POST 請求來通知事件。外部服務不需要與伺服器保持連線。
| 特性 | GraphQL Subscription | Webhook |
| :--- | :--- | :--- |
| **發起方** | **客戶端**主動建立連線 | **伺服器**主動向外部 URL 發送請求 |
| **連線協定** | **WebSocket** | **HTTP POST** |
| **連線類型** | **長連線** (保持開啟,等待 Server 推送) | **單次請求** (事件發生時發送) |
| **資料流向** | 伺服器 **推送** 至已連線的客戶端 | 伺服器 **呼叫** 外部服務的 API |
| **適用情境** | 網頁/App 等**即時畫面**更新 | 服務之間**後台通知** (支付完成、版本發佈) |
有興趣也可以參考我 [Webhook](https://hackmd.io/@Moofon/ByyIJnbgWg) 的文章
---
## Schema:GraphQL 的規則書
Schema 是 GraphQL 的核心,它定義了「遊戲規則」:有什麼資料可以查、資料長什麼樣子、資料之間的關係。
### 為什麼 Schema 這麼重要?
想像你去圖書館借書。REST API 就像沒有目錄的圖書館——你要自己去書架上找,或者問管理員(看文件)。而且文件可能過期了,管理員(後端工程師)可能也不確定某本書在哪裡。
GraphQL 的 Schema 就像一個永遠準確的電子目錄。你可以查詢:
- 有哪些書(資料類型)
- 每本書的資訊(欄位)
- 書在哪個書架(關聯)
- 能不能借出(權限)
而且這個目錄是**自動更新**的。Schema 改了,文件就自動更新,不會有文件和程式不一致的問題。
### Schema 的基本結構
```graphql
# 定義一個使用者類型
type User {
id: ID! # ! 表示必填,一定會有值
name: String!
email: String # 沒有 ! 表示可能是 null
posts: [Post!]! # 文章陣列,陣列和內容都不能是 null
}
# 定義文章類型
type Post {
id: ID!
title: String!
content: String!
author: User! # 每篇文章都有作者
published: Boolean!
}
# 定義可以查詢什麼
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
# 定義可以修改什麼
type Mutation {
createPost(title: String!, content: String!): Post!
deletePost(id: ID!): Boolean!
}
```
Schema 就像合約,前後端都要遵守。前端知道可以查什麼、會拿到什麼格式的資料。後端知道要提供什麼資料、接受什麼參數。
---
## GraphQL 解決了什麼問題?
### 1. 不同裝置,不同需求
現代應用要支援各種裝置:手機、平板、電腦、智慧手錶。每種裝置的螢幕大小不同,需要的資料量也不同。
想像你在做一個新聞網站:
- **智慧手錶**:只要標題
- **手機**:標題 + 摘要 + 一張圖
- **平板**:標題 + 摘要 + 內容預覽 + 圖片
- **電腦**:完整內容 + 相關新聞 + 留言
用 REST,你有兩個選擇:
1. 做四個不同的 API(維護地獄)
2. 一個 API 回傳所有資料(浪費頻寬)
用 GraphQL,同一個 API,不同的查詢,各取所需。
### 2. 前後端並行開發
傳統開發流程是這樣的:
1. 後端定義 API
2. 後端實作
3. 前端才能開始串接
4. 發現資料不夠,後端再改
5. 無限循環...
有了 GraphQL Schema,流程變成:
1. 前後端一起定義 Schema
2. 前端用 Mock 資料(假資料)開發
3. 後端實作 Schema
4. 自然串接
前後端可以並行開發,不用互相等待。Schema 就是雙方的合約。
### 3. API 演進不再痛苦
REST API 的演進是個難題。你不能隨便改變回傳格式,因為可能會弄壞正在使用的 App。所以才會有 v1、v2、v3...
GraphQL 的做法優雅很多:
- **新增欄位**:直接加,舊的客戶端不查詢就不會收到
- **刪除欄位**:先標記為 `@deprecated`,給開發者時間遷移
- **改變邏輯**:可以新增欄位,讓新舊邏輯並存
再複習一下:
假設你的豆漿原本是甜的,你想改成無糖。
👉 如果你直接改掉,習慣喝甜的客人會崩潰 [如果是RestAPI 就得再發新版本(新菜單)]。
所以你會這樣做:
原本的「甜豆漿」還是留著(舊欄位)
新增「無糖豆漿」給新客人(新欄位)
GraphQL 做法:
```graphql
sweetSoyMilk: String @deprecated
soyMilk: String
}
```
兩種版本並存,讓舊客人不會壞。
再來看看下面的例子:
```graphql
type User {
id: ID!
name: String!
# 2024 年新增的欄位,舊 App 不會查詢
avatarUrl: String
# 準備棄用的欄位
username: String @deprecated(reason: "請使用 name 欄位")
}
```
---
## 實務上的挑戰
GraphQL 不是萬靈丹,它也有自己的問題。
### 1. 後端複雜度增加
REST API 的實作相對單純:一個 endpoint 對應一個功能,你清楚知道要回傳什麼資料。
GraphQL 不一樣。同一個查詢,客戶端可能要不同的欄位組合。這代表你要處理各種可能性。
舉個例子,查詢使用者的文章:
```graphql
# 查詢 A:只要文章標題
{ user(id: 1) { posts { title } } }
# 查詢 B:要文章和所有留言
{ user(id: 1) { posts { title content comments { text } } } }
# 查詢 C:要文章、留言、留言者資訊
{ user(id: 1) {
posts {
title
comments {
text
author { name email }
}
}
} }
```
三個完全不同的查詢,後端要能聰明地處理,不能傻傻地把所有資料都撈出來。
### 2. N+1 查詢問題
這是 GraphQL 最惡名昭彰的問題。
假設你要顯示 10 篇文章的作者:
```graphql
{
posts { # 1 次資料庫查詢
title
author { # 每篇文章查一次作者 = 10 次查詢
name
}
}
}
```
總共 11 次資料庫查詢!如果是 100 篇文章,就是 101 次。
解決方法是使用 DataLoader 或類似的批次載入機制。把 10 個作者 ID 收集起來,一次查詢,而不是查 10 次。但這又增加了實作的複雜度。
### 3. 安全性考量
GraphQL 讓客戶端有很大的自由度,但自由度太大可能造成問題。
**查詢深度攻擊**
惡意使用者可能送出超深的查詢:
```graphql
{
user {
posts {
author {
posts {
author {
posts {
# 繼續往下 100 層...
}
}
}
}
}
}
}
```
這種查詢可能讓你的伺服器當機。
**查詢廣度攻擊**
或者要求大量資料:
```graphql
{
users(limit: 10000) {
posts(limit: 10000) {
comments(limit: 10000) {
# 10000 × 10000 × 10000 = 爆炸
}
}
}
}
```
所以生產環境的 GraphQL 需要加上各種保護:
- 查詢深度限制
- 查詢複雜度計算
- Rate limiting
- 查詢白名單(預先定義可用查詢)
### 4. 快取不容易
REST API 的快取很簡單。`GET /users/123` 的結果可以直接被瀏覽器、CDN、反向代理快取。
GraphQL 的查詢都是 POST 請求到 `/graphql`,每個查詢都可能不同,傳統的 HTTP 快取機制都失效了。
你需要在應用層做快取,或者使用支援 GraphQL 的 CDN。這又是額外的複雜度。
---
## 什麼時候該用 GraphQL?
### 適合的場景
**1. 客戶端有不同設備**
如果你的 API 要服務各種不同的客戶端(iOS、Android、Web、智慧電視、手錶),而且每個客戶端需要的資料差異很大,GraphQL 是好選擇。
Facebook 就是因為這個原因發明了 GraphQL。他們要服務各種裝置,從功能手機到高階電腦,每個裝置的需求都不同。
**2. 快速迭代的產品**
新創公司或是快速成長的產品,需求經常變動。GraphQL 讓前端可以自由調整查詢,不用等後端改 API。
**3. 資料關聯複雜**
如果你的資料像蜘蛛網一樣互相關聯(社群網站、知識圖譜),GraphQL 的圖狀查詢特別合適。
想像 LinkedIn 的人脈網路:你認識 A,A 認識 B,B 認識 C。用 REST 查詢三層人脈關係會很痛苦,但 GraphQL 很自然。
**4. 微服務架構**
如果後端是微服務架構,GraphQL 可以作為 API Gateway,對外提供統一的介面,對內整合各個微服務。
Netflix 就是這樣用。他們有幾百個微服務,但對外只露出一個 GraphQL API。
### 不適合的場景
**1. 簡單的 CRUD 應用**
如果你只是做一個簡單的待辦事項 App,REST 就夠了。GraphQL 會增加不必要的複雜度。
:能用簡單方法解決的問題,就不要用複雜的方法。
**2. 檔案上傳下載**
GraphQL 不擅長處理二進位資料。如果你的應用主要是處理圖片、影片、檔案,還是用專門的服務比較好。
---
## 實際案例分享
### GitHub 的 GraphQL API
GitHub 在 2016 年推出 GraphQL API,他們分享了幾個有趣的數據:
使用 REST API 時,顯示一個 Pull Request 頁面需要呼叫超過 10 個 endpoint。改用 GraphQL 後,一個查詢搞定。
但他們也遇到挑戰。有使用者送出極度複雜的查詢,消耗大量資源。他們的解法是計算每個查詢的「成本」,超過限制就拒絕。
### Shopify 的經驗
Shopify 是電商平台,他們的 GraphQL API 要服務數十萬個網路商店。
他們發現最大的好處是**減少往返次數**。以前顯示一個商品頁面,平均要 5-6 個 API 呼叫。用 GraphQL 後降到 1-2 個。
但他們也強調,GraphQL 不是萬能的。對於簡單的操作(like 查詢庫存),REST 反而更簡單直接。
---
## 開始使用 GraphQL
如果你決定嘗試 GraphQL,這裡有一些建議:
### 1. Schema 優先
花時間好好設計 Schema。Schema 是 GraphQL 的基石,設計不好後面會很痛苦。
幾個原則:
- 命名要一致(統一用 `userId` 或 `user_id`)
- 善用型別系統(別什麼都用 String)
- 考慮未來擴展(但不要過度設計)
### 2. 工具很重要
GraphQL 有很棒的工具生態系:
- **GraphQL Playground**:互動式的查詢介面
- **Apollo Client**:強大的前端框架
- **GraphQL Code Generator**:自動產生型別定義
善用工具可以大幅提升開發效率。
### 3. 監控和優化
上線後要密切監控:
- 查詢效能(哪些查詢特別慢?)
- 錯誤率(哪些查詢容易出錯?)
- 使用模式(使用者實際上查詢什麼?)
根據實際使用情況持續優化。
---
## 總結
最後來 Wrap up 一下這個酷東西吧,GraphQL 的核心價值是**讓前端掌控資料需求**。
它解決了 REST 的一些痛點:
- Over-fetching 和 Under-fetching
- 版本管理困難
- 多次往返的效能問題
但也帶來新的挑戰:
- 後端實作複雜度增加
- N+1 查詢問題
- 快取和安全性考量
**GraphQL 不是要取代 REST**,就像 NoSQL也沒有取代 SQL一樣,他們各有適用的場景。
如果你正在建構複雜的、資料密集的應用,需要服務多種客戶端,GraphQL 還是推薦 👍。
---
## 參考資料
- https://graphql.org/learn/
- https://ithelp.ithome.com.tw/m/articles/10200678
- https://aws.amazon.com/tw/compare/the-difference-between-graphql-and-rest/
- https://zh.wikipedia.org/zh-tw/GraphQL