為什麼 GraphQL 不查資料?它不是缺陷,而是超前的設計
更新日期: 2025 年 5 月 5 日
當你開始學 GraphQL,常會有這樣的疑問:
「我不是已經定義好
type
和query
嗎?為什麼 GraphQL 本身不會查資料,還需要另外寫 Resolver?」「Resolver 為什麼不像語法的一部分?為什麼一定要靠 Apollo、Hasura、gqlgen 這些外部工具來實作?」
對很多人來說,這種設計有點反直覺。
畢竟在傳統的 API 開發中,我們習慣的工具(例如 REST 框架)大多採取的是「語法與邏輯一體化」的設計,也就是:
你在哪裡定義了 API endpoint,就可以直接在那裡寫執行邏輯,包含查資料、轉換格式、回傳結果等。
例如:
在 Express(Node.js 框架)中,你只要寫一行:
app.get('/posts', (req, res) => { ... })
裡面就能立刻連接資料庫、撈資料、處理錯誤、回傳結果。
所有事情都集中在這個 function block 裡完成,語法與邏輯是綁在一起的。
在 Laravel(PHP 框架)中,你只要建立一個 Controller:
public function getPosts() {
return Post::all();
}
框架就幫你自動處理路由綁定、序列化、錯誤回應等。你只需要專注在邏輯上,整個「從 request 到資料回應」的流程是一條龍處理的。
但換到 GraphQL,就完全不是這麼回事。
即使你在 GraphQL 裡定義了完整的 type
, query
, mutation
,讓整個 Schema 看起來非常完備,系統卻仍然完全不會自動查任何資料。
比方說,你定義了一個查詢:
type Query {
post(id: ID!): Post
}
但除非你自己實作這個欄位對應的 Resolver,否則當你真的送出 post(id: 1)
查詢時,GraphQL Server 會直接報錯或回傳 null。
這不是因為你哪裡漏了步驟,而是因為:
GraphQL 的設計就是「只定義格式、不包含實作」。
它只關心:
- 你有哪些查詢可以做?
- 每個欄位的型別是什麼?
- 查詢執行的流程規則是什麼?
而不負責:
- 要連哪個資料庫?
- 要打哪個 API?
- 欄位要怎麼處理錯誤、授權、轉型?
這些全部都留給開發者自己決定該怎麼做,也因此才需要 Resolver。
但 Resolver 的語法並不是 GraphQL 的一部分,而是由像 Apollo、Graphene、gqlgen、Hasura 等工具根據 GraphQL 規範去幫你「補上這一塊執行層」。
這樣的設計乍看麻煩,但它也是 GraphQL 能支援各種語言、多樣資料來源、模組化擴充的根本原因。
本文將一步步帶你理解這種設計背後的思維、優勢,以及為何它會成為現代 API 開發的核心選擇。
GraphQL 的設計哲學:規範導向(Specification-Driven)
GraphQL 採用的是一種名為 規範導向(Specification-Driven) 的設計方式,意思是:
- ✅ 它不是一個執行引擎
- ✅ 它不處理資料查詢邏輯
- ✅ 它只定義語法與執行規則
這與許多初學者熟悉的 REST API 設計方式不同。
GraphQL 更像是一個「語言標準」,而不是一個「具體實作」。你需要結合其他工具(例如 Apollo Server)來讓它「動起來」。
GraphQL 到底定義了什麼?
GraphQL 並不是一個「幫你完成 API」的框架,而是一個語言規範(specification)。
它的核心精神是:定義格式與流程,但不負責執行資料查詢。
這個規範主要涵蓋兩個層面:
語法層(Syntax):你可以寫出怎樣的查詢?
GraphQL 規定了一套結構化的語法格式,讓你可以清楚描述:
- 系統提供哪些查詢或操作?
- 資料有哪些欄位?每個欄位的型別是什麼?
- 查詢時可以帶入哪些參數?輸入格式長什麼樣子?
常見的語法單位包括:
語法單位 | 說明 |
---|---|
query | 查詢資料的語法,例如取得某篇文章的內容 |
mutation | 修改資料的語法,例如新增、刪除、更新 |
subscription | 即時訂閱資料變化(通常與 WebSocket 結合) |
type | 定義資料的結構,例如 type Post { id: ID, title: String } |
input | 定義輸入格式,例如 mutation 需要的參數 |
enum | 枚舉型別,限制欄位只能是特定值之一 |
interface | 定義型別的共通欄位,支援型別繼承 |
fragment | 可重用的欄位區塊,避免查詢內容重複 |
@directive | 可加入在欄位上的執行指令,例如條件查詢 @skip(if: true) |
簡單來說,這一層就是在「規劃地圖」:
你告訴前端:「這些是你可以查的資料」、「這些欄位長這樣」、「這裡可以傳參數」。
但語法層只是一個靜態描述,它不會真的執行資料查詢。
執行規則層(Execution Rules):查詢怎麼跑?
語法定義了查什麼,但查詢發生時要怎麼執行,則是由執行規則來決定。
GraphQL 規範中詳細說明了查詢被送出後,伺服器該怎麼處理,包括:
- Resolver 執行順序:
每個欄位背後都需要一個 Resolver,GraphQL 規定它們會從 parent 開始、一路遞迴呼叫子欄位的 Resolver。 - 參數傳遞與繫結規則:
包含變數如何代入、context 怎麼流通、parent 結果怎麼傳給子欄位。 - 錯誤處理與擴散機制:
當某個欄位報錯時,整個查詢不會直接中斷,而是僅該欄位回傳 null,同時在errors
欄提供錯誤資訊。 - 非同步查詢支援:
規範允許 Resolver 回傳 Promise,讓資料可以延遲、串接、併發查詢等。
🔎 舉個例子:
當你查詢這樣的資料時:
query {
post(id: 1) {
title
author {
name
}
}
}
GraphQL Server 必須依據規範:
- 呼叫
Query.post
的 Resolver(parent) - 拿到文章物件後,再執行
Post.title
和Post.author
的 Resolver(child) - 如果
author
是另一個 API 查回來的,也能獨立執行、處理異步
📌 但這一整套 Resolver 系統,GraphQL 規範只「定義行為」,不「提供實作」。
GraphQL 規範只是說明「怎麼做」,但不「幫你做」
這就像是一本說明書:
- 告訴你 Query 怎麼寫、資料會怎麼被處理
- 但它沒有提供內建指令或函式讓你實作 Resolver
- 也沒有一行指令幫你接資料庫或外部 API
你必須自己選擇實作工具來落實這些規則,也就是「GraphQL Server」。
常見的 GraphQL Server 實作工具
工具名稱 | 支援語言 | 特點 |
---|---|---|
Apollo Server | JavaScript | 社群最活躍、文件完善、功能豐富 |
Graphene | Python | Python 生態圈主流選擇 |
gqlgen | Go | 強型別 + 自動產碼,效能高 |
Strawberry | Python | 使用 Python 型別註解(type hint)自動生成 Schema |
Hasura | 自動產生 | 不寫 Resolver,從資料庫自動產生 API,非常適合快速開發 |
這些工具就是根據 GraphQL 的「語言規範」,自行實作了一套執行引擎(executor),讓你能:
- 建立 Resolver
- 串接資料來源
- 控制授權邏輯
- 管理查詢效能
總結來說:
- GraphQL 自己不會「動」,因為它只是一份規格書
- 你要讓它動起來,必須挑選適合的工具,把「Schema」與「資料來源」連起來
- 正是因為語言與執行分離,GraphQL 才能有這麼高的彈性與跨語言整合能力
那麼問題來了:這些 GraphQL Server 工具有什麼差別?
到了這裡,你可能會好奇:
既然大家都根據同一份 GraphQL 規範實作,那為什麼會有 Apollo、gqlgen、Graphene、Hasura 這麼多工具?
而且看起來差異還不小?
這個問題非常關鍵,因為它關係到你實際開發時的體驗、維護成本,甚至整個系統的效能設計。
答案其實很簡單:
雖然這些工具都遵守 GraphQL 規範, 但在「怎麼實作 Resolver」、「怎麼管理型別」、「怎麼串接資料」這些細節上, 每個工具都有自己的設計哲學與開發策略。
接下來我們就從幾個核心差異來比較這些工具,幫助你選出最適合自己技術棧與需求的實作方式。
他們的共通點:都根據 GraphQL 規範實作
- 都支援
query
,mutation
,subscription
- 都遵守「parent → child」的 Resolver 執行順序
- 都支援變數、fragment、@directive 等語法
- 都支援非同步 Resolver 與錯誤擴散機制
換句話說:查詢怎麼寫、流程怎麼跑,大家都一樣。
差別在於:這些工具用什麼方式幫你實作這些功能?效率好不好?好不好維護?跟你的語言搭不搭?
這些才是讓人真的會「選這個不用那個」的關鍵。
差異的起點:型別整合與實作方式
其中一個最明顯的差異,就是這些工具如何處理「型別」這件事。
你可能已經注意到,不同的 GraphQL Server 工具在使用上有很大的差別:
- 有些工具要你「寫程式碼定義型別」,像是 Go 的 gqlgen
- 有些要你「寫一份像 JSON 的文字格式」定義型別,例如 Apollo 使用的語法
- 有些工具(像 Hasura)甚至完全不用自己寫 Resolver,就能自動產生查詢功能
這些不同的開發方式,背後其實都跟一個關鍵議題有關:
👉 「型別要從哪裡來?怎麼同步不同層的型別?」
釐清重點:GraphQL 開發中有哪三種「型別」?
在實作 GraphQL 的過程中,開發者會同時接觸三層不同的型別系統,它們分別屬於不同領域:
類型 | 用途 | 例子 | 層級 |
---|---|---|---|
GraphQL 型別 | 定義 API 結構,前端要查什麼欄位 | type Post { id: ID!, title: String } | 🟦 規範層 |
程式語言型別 | Resolver 撰寫時操作的資料結構 | interface Post (TS)、struct Post (Go) | 🟨 程式實作層 |
資料庫型別 | 實際儲存在資料庫中的欄位格式 | id UUID, title TEXT | 🟥 資料來源層 |
這三層型別之間如果要手動維護對應,就會變得很繁瑣、很容易出錯。
於是,各種 GraphQL 工具就出現了不同的設計策略:
- 有的幫你從程式語言型別自動產出 GraphQL Schema
- 有的幫你從資料庫型別反推回整套 API
- 有的則維持手寫,但搭配型別產生器來輔助
換句話說,不同工具的差異,其實是:
你要自己寫型別,還是讓工具幫你從資料或程式自動推導型別。
🧠 延伸小知識:什麼是 SDL?什麼是 Code-first?
名稱 | 說明 | 範例 | 常見工具 |
---|---|---|---|
SDL(Schema Definition Language) | 用類似 JSON 的語法來定義 GraphQL 型別結構 | type Post { id: ID!, title: String } | Apollo、GraphQL 官方教學 |
Code-first | 直接用程式語言(如 Go、Python)定義資料結構,工具再幫你自動產生對應的 GraphQL 型別 | struct Post { ID string }(Go) | gqlgen、Strawberry |
Schema-free / Auto-gen | 不用自己定義型別,工具會自動從資料庫反推 | 資料庫欄位就是 API 欄位 | Hasura |
他們的差異總覽:從「型別」延伸到開發體驗
這裡是不同工具在核心設計上的差異比較:
項目 | 差異點 | 說明 |
---|---|---|
🧑💻 使用語言 | JS / Python / Go | Apollo 用 JS、Graphene 用 Python、gqlgen 用 Go,不同語言導致不同開發風格 |
🛠 Schema 定義方式 | SDL vs Code-first | Apollo 使用 SDL;gqlgen、Strawberry 可從程式碼產生 schema |
🔁 型別整合能力 | 手動對應 vs 自動推導 | gqlgen 可從 Go struct 自動產出 GraphQL 型別;Hasura 可從資料庫自動產 API;Apollo 需手動或搭配 TS codegen |
⚡ 效能優化工具 | Cache / Batching / DataLoader | Apollo 內建快取與批次查詢,gqlgen 可整合 DataLoader,Graphene 需手動處理 |
🔐 授權控制方式 | Middleware / Decorator | Apollo 偏好中介層,Strawberry 用 decorator |
🚀 自動化程度 | 手寫 vs 自動產 API | Hasura 幾乎不寫 Resolver 就能用;其他工具則保留高度自訂彈性 |
實例對照:同樣的查詢,兩種實作方式
為了讓你更直觀地理解不同 GraphQL Server 工具的開發差異,我們用一個簡單的範例來對照:
假設你要實作這個查詢:
query {
post(id: 1) {
title
author {
name
}
}
}
這個查詢會根據文章 ID 找出一篇文章,並顯示它的標題與作者名稱。
✅ 使用 Apollo(JavaScript)開發流程
Apollo 是最受歡迎的 JavaScript GraphQL Server,它通常採用「手寫 Schema + 手寫 Resolver」的開發模式。
你會需要定義:
GraphQL Schema(用 SDL 語法):
type Query {
post(id: ID!): Post
}
type Post {
id: ID!
title: String
author: User
}
type User {
id: ID!
name: String
}
Resolver 實作(JavaScript):
const resolvers = {
Query: {
post: (_, { id }) => fetchPostById(id),
},
Post: {
author: (parent) => fetchAuthor(parent.authorId),
},
};
搭配型別工具(選用):
為了避免型別錯誤,你通常還需要使用 TypeScript 搭配像 GraphQL Code Generator
等工具,自動產出型別定義。
📌 特點說明:
- Resolver 你要手動寫每一層(即使只是單純取欄位)
- Schema 與邏輯是分開維護的,需要自己保持同步
- 適合追求彈性高、自訂細節多的開發者
- 初學者會花較多時間在「型別同步」與「程式架構」上
✅ 使用 gqlgen(Go)開發流程
gqlgen 是 Go 語言中最受歡迎的 GraphQL Server,它主打「code-first 與強型別開發體驗」,會根據你的程式碼自動生成 Schema 與 Resolver 框架。
你只要做這幾件事:
定義 Go 型別(struct):
type Post struct {
ID string
Title string
Author *User
}
type User struct {
ID string
Name string
}
實作 Resolver(只需補查資料邏輯):
func (r *queryResolver) Post(ctx context.Context, id string) (*model.Post, error) {
return r.DB.FindPostByID(id)
}
func (r *postResolver) Author(ctx context.Context, post *model.Post) (*model.User, error) {
return r.DB.FindUserByID(post.AuthorID)
}
Schema 自動生成:
gqlgen 會從你的 struct 自動產生 GraphQL 型別與對應的查詢語法,省去手寫 SDL 的工作。
📌 特點說明:
- Schema 是從程式碼反推而來,不需重複定義
- 所有型別都由 Go 語言強型別系統保障,不易出錯
- 工具會幫你產生好 Resolver 的框架,你只需要補「資料從哪裡來」
- 適合中大型專案、重視效能與可維護性的開發者
總結對照表
比較項目 | Apollo (JS) | gqlgen (Go) |
---|---|---|
Schema 定義方式 | 手寫 SDL | 自動從程式碼生成 |
Resolver 寫法 | 每一層欄位都要手寫 | 只補查資料邏輯,框架自動產生 |
型別同步 | 需手動對應或使用外部工具 | 原生整合程式語言型別 |
適合專案類型 | 快速原型、彈性高的小中型應用 | 型別嚴謹、追求可維護的大型專案 |
技術門檻 | 入門較容易但後期需維護型別一致性 | 入門稍難但後期開發效率高 |
該怎麼選?依你的技術棧與需求而定
你在乎的重點 | 建議選擇 |
---|---|
想整合 JS 生態、文件多、社群強 | ✅ Apollo Server |
Python 專案、想和 Django 整合 | ✅ Graphene / Strawberry |
重視效能與靜態型別安全 | ✅ gqlgen (Go) |
想快速產出 CRUD API、不寫 Resolver | ✅ Hasura |
喜歡用型別註解產 Schema | ✅ Strawberry (Python) |
小結:GraphQL 是標準,Server 是選擇
你可以這樣理解:
GraphQL 是一套規範,就像 HTML 是一種語言格式
而 Apollo、gqlgen、Graphene、Hasura 就像不同的「瀏覽器」
它們都能執行 GraphQL,但開發體驗、擴充性與整合方式各有千秋
正因為 GraphQL 僅僅定義「語言與流程」,沒有強制執行方式,才誕生出這麼多彈性又強大的實作。
這讓開發者可以根據自己熟悉的語言與開發需求,自由挑選最合適的 GraphQL Server 工具。
這樣「語言與執行分離」的好處是什麼?
GraphQL 把 Schema 和 Resolver 拆開,不是因為它做不到,而是有意這樣設計。
這種「語言層定義格式、執行層自己實作邏輯」的哲學,帶來了幾個在實務開發中非常重要的優勢:
優點 | 說明 |
---|---|
✅ 更高彈性 | 不綁定特定語言或架構,無論你用 JavaScript、Python、Go 都能實作 GraphQL Server |
✅ 更強可組合性 | Schema 可以獨立設計,支援自動生成文件、自動產生型別,與前端工具串接更容易 |
✅ 清晰的責任劃分 | Schema 僅定義「前端可以查什麼」、Resolver 負責「怎麼查、從哪裡查」 |
✅ 工具鏈支援完整 | 各種工具如 Code Generator、Mock Server、Federation 等,都可只靠 Schema 運作 |
這樣的分離設計,不但讓系統更模組化、可測試性更高,也方便多人協作。
例如你可以先定好 Schema 給前端開發者模擬使用,再慢慢實作後端 Resolver;甚至還能用假資料(mock)先跑通前端頁面。
用類比幫助理解:Schema 就像 HTML,Resolver 就像 JavaScript
我們換個方式來理解這個設計哲學。
想像你在寫前端網頁:
部分 | 功能 |
---|---|
HTML | 描述畫面有哪些元件(欄位) |
JavaScript | 決定那些元件怎麼互動(邏輯) |
同理,在 GraphQL 中:
部分 | 功能 |
---|---|
Schema | 定義有哪些查詢、型別結構長什麼樣 |
Resolver | 實作每個欄位要如何查資料 |
HTML+JavaScript 的組合,讓網頁既有結構又有互動。
GraphQL 也是一樣:Schema 給你結構,Resolver 給你資料邏輯,分工清楚,擴充方便。
如果今天要新增一個欄位或功能,你只要:
- 在 Schema 加一個欄位
- 實作對應的 Resolver
其他地方完全不用動,模組化就是這麼簡單。
為什麼這樣的設計對大型系統特別重要?
當你只有一台資料庫、一套 API,可能會覺得 Resolver 有點麻煩。
但當系統規模一大,你就會發現:這種分離設計是唯一能撐得住多來源資料整合的方法。
來看看現代應用中常見的資料來源:
資料來源類型 | 常見例子 |
---|---|
資料庫 | PostgreSQL、MongoDB 等關聯與非關聯 DB |
外部 API | 企業內部微服務、第三方 REST API、gRPC |
快取或平台服務 | Redis 快取、Firebase、Stripe 等外部平台 |
這些資料來源格式不同、授權方式不同、甚至查詢條件也不一樣,若使用傳統 API:
- 很容易把邏輯「寫死」在每個 endpoint 裡
- 一旦有新需求,就得改整串處理流程
- 欄位也不能自由調整,不然就會破壞前後端協議
而 GraphQL 則提供一種優雅的解法:
GraphQL 功能 | 對應的解法 |
---|---|
每個欄位對應一個 Resolver | 不同資料來源可個別處理,不互相干擾 |
Resolver 支援 async / 串接多來源 | 同時打資料庫 + REST API 也沒問題 |
Schema 與 Resolver 解耦 | 前端不用等後端完成就能模擬與測試 |
欄位層級可控 | 權限檢查、欄位遮蔽都能在 Resolver 中獨立實作 |
這種「可插拔、可調整、可擴充」的架構特性,正是企業在多系統、多資料整合時最需要的能力。
總結:為什麼這樣的設計是現代 API 的未來
GraphQL 的設計哲學──把語言規範(Schema)與資料查詢實作(Resolver)明確拆開──看起來一開始會比 REST 更複雜、更抽象,但實際上,這種分離式架構正是現代大型應用中最能應對複雜性與變化的關鍵設計。
更容易擴充與維護
當系統成長到一定規模後,最常遇到的問題是:
- 一改後端就會影響前端
- API 欄位無法彈性調整
- 資料邏輯混在一起,難以維護
而 GraphQL 將「定義你可以查什麼」(Schema)與「怎麼查」(Resolver)拆開,讓你能:
- 單獨調整 Schema,不影響查詢邏輯
- 或只調整 Resolver,不動到前端約定格式
- 欄位錯誤或資料異常,不會拖垮整體查詢(只會影響單一欄位)
這就像建築物結構做了模組化,不僅可維護性高,也更適合團隊合作與長期演進。
天生適合多來源資料整合
現代的應用系統幾乎都不再是單一資料庫供應資料,而是:
- 一邊打內部 REST API
- 一邊查資料庫
- 同時還要整合 Firebase、Stripe、第三方平台
如果你用傳統 REST API,每種資料要一條 endpoint,然後前端還得自己組資料。
GraphQL 的 Resolver 則讓你可以針對每個欄位串接不同來源,查詢整合就是語言本身的設計目的。
而這種 「欄位即邏輯單元」的設計,比起 REST 的「URL 是單位」,更細緻、可控、可擴充。
更容易配合自動化與前後端協作流程
GraphQL Schema 天然支援:
- 🔍 自動生成文件(如 GraphQL Playground、GraphiQL)
- 💡 自動補全欄位(前端寫查詢像在用 IntelliSense)
- 🧪 前端 Mock 資料:就算後端還沒完成,只要 Schema 在,前端也能先開發
- ⚙️ 自動產生 TypeScript 型別、自動產測試碼、自動產 SDK…
這讓 GraphQL 成為一種具備完整工具鏈的 API 語言標準,不只是資料通道,更是「溝通協議 + 開發工具 + 文件生成」三合一。
順應微服務與多端發展趨勢
當系統走向微服務架構、前端分散到多平台(Web、iOS、Android、IoT),GraphQL 展現出比 REST 更強的適應力:
挑戰 | GraphQL 的解法 |
---|---|
不同端點需要不同欄位 | 前端自己選擇查什麼(避免 over-fetch / under-fetch) |
微服務太多,串接麻煩 | 使用 Apollo Federation 等工具做 API 聚合層 |
權限、欄位控管複雜 | 在 Resolver 層級做欄位級授權、資料遮蔽 |
這些都說明:GraphQL 的設計,不只是現在好用,更是為了未來複雜系統的彈性而生。
總結一句話
GraphQL 把「資料定義」與「資料查詢」分離。
不只是設計選擇──而是應對未來系統複雜性與變化速度的最佳武器。
在 API 越來越像「產品」而不是「程式碼接口」的今天,GraphQL 提供的不只是技術解法,更是一種開發文化的轉變:用更模組化、可組合、語意清晰的方式來溝通與交付資料。