GraphQL 如何根據 Query 動態生成 SQL?
更新日期: 2025 年 4 月 27 日
在現代應用中,GraphQL 的一大亮點就是其動態生成 SQL 的能力,這與傳統 REST API 的固定路由和手寫 SQL 有著本質上的不同。
比較項目 | REST API | GraphQL |
---|---|---|
SQL 產生方式 | 開發時手寫 SQL,寫死 | 根據前端送來的 Query,動態產生 SQL |
查詢自由度 | 受限於後端預設的欄位和路由 | 前端自己選要哪些欄位、要查什麼資料 |
路由數量 | 多條路由,各自獨立 | 單一路由,靠 Query 指定需求 |
GraphQL 的設計讓後端不再需要為每個需求設計固定的路由, 而是透過一個統一的端點,根據前端的查詢需求動態生成 SQL,實現了更靈活、精準、高效的資料查詢方式。
接下來,我們將分步解析 GraphQL 如何完成這一過程。
定義 GraphQL Schema(資料結構說明書)
在 GraphQL 中,Schema 是整個系統的靈魂。
後端開發者一開始最重要的工作,就是設計並定義這份 Schema。
你可以把 Schema 想成一份:
📚 資料目錄表 ➔ 告訴前端:「我這邊有哪些資料可以查?每個資料有什麼欄位?可以怎麼操作?」
這份 Schema 不僅是前後端共同的溝通標準,同時也是 GraphQL 伺服器解析前端請求、驗證查詢正確性的依據。
Schema 主要包含兩大部分
📋 資料型別(Types)
- 定義有哪些「資料實體」(例如 User、Post、Product)
- 每個實體有哪些欄位(每個欄位又有自己的型別)
這些資料型別,就像是現實世界中物件的規格書。
例如:
type User {
id: ID!
name: String!
email: String!
posts: [Post]
}
這個型別(Type)代表了一個「使用者(User)」有:
欄位名稱 | 資料型別 | 說明 |
---|---|---|
id | ID(唯一識別碼) | 必填,通常對應資料庫的主鍵 |
name | String(文字) | 必填,使用者的名字 |
String(文字) | 必填,使用者的電子郵件 | |
posts | [Post](文章陣列) | 可以有很多篇文章(多對多關係) |
✅ 備註:!
代表「必填」(non-nullable),[Post]
則代表「Post 型別的陣列」。
🔧 可以操作的項目(Query / Mutation / Subscription)
除了定義資料型別,Schema 還要定義:
- Query(查資料):可以查什麼資料?查詢時需要哪些參數?
- Mutation(改資料):可以新增、更新、刪除哪些資料?
- Subscription(訂閱資料變化):有沒有需要即時監聽的資料更新?(進階)
範例中的 Query 定義:
type Query {
user(id: ID!): User
}
這表示:
- 有一個
user
的查詢功能 - 必須傳一個
id
參數(型別是ID!
) - 回傳的是一個
User
資料型別
如果想支援新增文章(Post),可以再加一個 Mutation:
type Mutation {
createPost(title: String!, content: String, authorId: ID!): Post
}
這代表:
- 前端可以送一個
createPost
請求 - 需要給三個參數:
title
,content
,authorId
- 後端會建立一個新的 Post,然後回傳這篇文章的資料
這樣一來,不只是查資料,連新增資料都靠 Schema 描述清楚了!
✅ 小提醒:
- Query 代表「讀取資料」。
- Mutation 則是「改變資料」(新增、更新、刪除)時才用。
延伸說明:Schema 的三個特性
為什麼 GraphQL Schema 這麼重要?因為它具有這三個特性:
✅ 型別明確(Strongly Typed)
- 可以及早抓錯(例如傳錯型別時,GraphQL Server 直接拒絕)
- 每個欄位都有固定型別(String、Int、Boolean、ID、甚至是自訂型別)
📚 自動生成 API 文件(Self-Documenting)
- 只要定義好 Schema,很多工具(像 GraphiQL、Apollo Studio)就能自動產生互動式 API 文件。
- 前端可以自己看 Schema 文件,不用一直問後端資料長怎樣。
🔄 靈活組合(Flexible Querying)
- 前端可以依照需要挑欄位,不必照後端一開始寫死的格式走。
- 減少資料浪費,提升效能。
前端如何發送 Query
在傳統的 REST API 設計中,前端如果要拿不同的資料,通常需要打不同的 URL,例如:
/api/users/1
拿 user 資料/api/users/1/posts
拿 user 的文章/api/posts/123
拿某篇文章詳細資料
每個功能一條路徑(endpoint),後端還得設計一堆對應的路由。
但在 GraphQL 世界裡,一切更簡單了:
- 前端不再需要知道很多不同的 API 路徑。
- 只要把所有請求統一送到一個固定的端點(通常是
/graphql
)。 - 在請求中,前端自己用Query 語法描述「我要什麼資料」。
這樣後端就能解析指令,根據指示取回資料,回傳給前端。
Query 的基本結構
舉個例子,前端可以寫出這樣的查詢:
query {
user(id: 1) {
name
email
posts {
title
}
}
}
這段 Query 的含義可以拆解成:
- 我要查一個 User,且這個 User 的
id
是1
- 但我只想要這些欄位:
name
(使用者名稱)email
(電子郵件)posts
(使用者發表的文章清單)- 對於每篇 post,只要
title
欄位,不要其他東西(例如不需要文章內容 content)
- 對於每篇 post,只要
✅ 小提醒:這裡的「資料長相(哪些欄位)」完全是前端自己決定的,不是後端硬塞給前端一大包固定格式。
真實送出時的 HTTP 請求範例
雖然 GraphQL 本身用的是「Query」語法,但底層還是透過HTTP 請求送到後端。
通常會包在 POST 請求裡,像這樣:
POST /graphql HTTP/1.1
Content-Type: application/json
{
"query": "query { user(id: 1) { name email posts { title } } }"
}
這個 POST 請求:
- URL 永遠是
/graphql
- 請求內容是 JSON 格式,裡面有一個
query
欄位,放入 GraphQL 的指令
✅ 即使查不同資料,URL 也不用變,只改 Query 本身就好!
使用變數(Variables)的好處
當我們在開發應用時,前端請求的資料往往不是固定的,而是根據使用者輸入或畫面互動,動態變動的參數。
例如:
- 使用者查詢不同 ID 的使用者資訊
- 表單送出不同的搜尋條件
- 頁面上根據點擊不同商品,查詢不同商品詳細資料
如果每次都把這些變動的值直接硬寫進 Query 字串,會讓程式碼變得很難維護、很醜亂,而且也比較容易出錯。
所以 GraphQL 提供了一個機制:Variables(變數)
它的設計目的是:
✨ 把變動的資料和查詢結構分開管理,讓 Query 更乾淨、模組化,也更容易重用。
簡單來說,就是:
- 查詢結構(Query)保持不變 ➔ 只描述「資料結構」與「需要什麼欄位」
- 真正要查的參數值 ➔ 放到另一個
variables
區塊裡面
✨ 使用變數的寫法
原本的(沒用變數的寫法)
如果我們不用變數,必須這樣直接寫死:
query {
user(id: 1) {
name
email
}
}
- 每次查不同的人,就要改這個 Query
- 程式碼裡可能有很多地方重複類似的 Query,不好管理
使用變數後的寫法
我們可以把 id
抽成一個變數:
query GetUser($userId: ID!) {
user(id: $userId) {
name
email
}
}
這裡注意:
GetUser
是這個查詢的名字(可以不寫,但大型應用建議取名)($userId: ID!)
表示這個 Query 需要一個名叫userId
的變數,型別是ID!
(必填)- 然後在
user(id: $userId)
這邊,把變數帶進去
✅ 這樣 Query 本身就變得乾淨又通用,只管描述資料格式。
📤 HTTP 請求內容:Query 和 Variables 分開送出
真正送出時,會用一個 POST 請求,把兩個東西一起包進 JSON:
{
"query": "query GetUser($userId: ID!) { user(id: $userId) { name email } }",
"variables": {
"userId": 1
}
}
query
:存放 GraphQL 的查詢指令(可以重複使用)variables
:存放這次要帶入的實際值(可以動態變動)
✅ 這樣一來,不同的 userId,只需要改變 variables,Query 本身完全不需要重寫或修改。
⚠️ 小提醒:變數型別要正確
在 Query 裡宣告變數時,一定要指明變數型別,而且要符合 Schema 定義。
常見型別像是:
型別 | 說明 |
---|---|
ID | 唯一識別碼(通常是字串或數字) |
String | 字串文字 |
Int | 整數數字 |
Float | 小數數字 |
Boolean | 布林值(true / false) |
加上 !
代表這個變數是必填的。
例如:
query GetPost($postId: ID!) {
post(id: $postId) {
title
content
}
}
如果沒有傳 postId
,後端會直接拒絕請求,回傳錯誤!
GraphQL Server 的工作流程
當後端的 GraphQL 伺服器(例如 Apollo Server、Yoga、Mercurius 等,收到前端送來的一段 Query 指令後。
它會按照以下步驟系統化地處理每一個請求,確保資料正確、安全且高效地回傳:
解析 Query 結構
首先,GraphQL Server 會解析前端送來的文字格式 Query,變成機器可操作的結構化資料(AST,Abstract Syntax Tree)。
這一步會進行:
- 讀懂 Query 是哪一種操作(是 Query、Mutation,還是 Subscription?)
- 逐層解析每個節點,確定要查什麼資料、要哪些欄位。
範例:如果收到這段 Query:
query {
user(id: 1) {
name
email
posts {
title
}
}
}
Server 會解析出:
項目 | 內容 |
---|---|
根操作類型 | Query |
要查的資料型別 | user |
帶的參數 | id = 1 |
要的欄位 | name、email、posts ➔ posts 裡面只要 title |
✅ 這個解析結果會被暫存在記憶體中,後續處理流程會一直依賴這份結構化資料。
驗證 Query 合不合法
解析完後,Server 不會馬上執行查資料,而是進行一個很重要的步驟:驗證(Validation)。
這個步驟會做:
型別驗證
- 確認每個傳進來的參數,型別是否符合 Schema 定義。
- 例如:
id
應該是ID
類型,如果你傳字串或數字,要符合要求。
欄位驗證
- 確認 Query 裡要查詢的每個欄位,都必須事先在 Schema 中有定義。
- 如果 Query 中查了一個不存在的欄位(例如
user(age)
,但 Schema 沒有age
),
➔ 直接拋出錯誤,不會進入下一步。
範例錯誤訊息:
{
"errors": [
{
"message": "Cannot query field 'age' on type 'User'.",
"locations": [{ "line": 4, "column": 5 }]
}
]
}
✅
這一步保護系統不會亂查資料,確保 API 安全、穩定、可預期。
找到對應的 Resolver 函式
驗證通過後,Server 才會開始真正執行查資料的流程。
這時候,Server 根據解析出來的 Query 結構,自動呼叫對應的 Resolver 函式。
每個資料節點(像是 user
、posts
)都有一個專屬的 Resolver。
Resolver 的負責內容:
- 根據前端傳來的參數(args)去資料庫查資料
- 根據前端要求的欄位(info)控制資料回傳內容
- 將資料組成符合 GraphQL Schema 的結構,交還給 Server
小範例流程:
- 查詢
user(id: 1)
➔ Server 呼叫user
對應的 Resolver - 查詢
posts
➔ Server 呼叫posts
對應的 Resolver(通常巢狀呼叫)
✅ Server 幫你把參數、欄位需求都準備好,Resolver 只需要專心負責:「拿資料,組資料,回傳資料」。
Resolver 執行查詢 ➔ 根據前端需求產生 SQL
Resolver(解析器) 是 GraphQL 後端中,負責根據前端送來的 Query,真正去查資料庫並拿回資料的角色。
你可以把 Resolver 想成:
🧑🍳 廚房裡的廚師,根據客人的點餐(Query),準備指定的菜色(資料)。
Resolver 的主要任務:
- 讀取前端送來的參數(例如 id、搜尋條件)
- 確認前端這次要求哪些欄位(透過
info
參數) - 組合出正確的資料庫查詢(手動 SQL 或用 ORM)
- 拿到資料後整理回傳給前端
資料欄位由前端決定
在 GraphQL 中,前端可以自由選擇要哪些欄位。
所以後端 Resolver 不能硬寫死所有欄位,必須要根據這次的 Query,要什麼查什麼。
這個資訊可以從 Resolver 的第四個參數 info
拿到。
例如解析欄位清單的方式:
function getSelectedFields(info) {
return info.fieldNodes[0].selectionSet.selections.map(
(selection) => selection.name.value
)
}
這樣你就能動態知道,這次 Query 要 name
、email
還是 posts
,依照需要查對應欄位!
Resolver 可以查資料的兩種方式
🛠️ 手動撰寫 SQL 查詢
如果你是直接操作資料庫(像 PostgreSQL、MySQL),在 Resolver 裡,你必須自己根據解析出來的欄位清單,手動組出對應的 SQL 語句。
範例:
async function getUser(parent, args, context, info) {
const fields = getSelectedFields(info) // 解析欄位
const selectClause = fields.join(', ') // 組成 SQL select 子句
const sql = `SELECT ${selectClause} FROM users WHERE id = ?`
const user = await db.query(sql, [args.id])
return user[0]
}
✅ 這樣就可以動態根據前端要求,只撈需要的欄位,避免多撈無用資料,提高效能!
🌟 使用 ORM 自動生成查詢(推薦)
在現代 GraphQL 專案中,使用 ORM(Object-Relational Mapping 工具)來操作資料庫,是一種非常常見的做法。
常見的 ORM 工具有:
- Prisma(現代、型別友善)
- TypeORM(支援多種資料庫)
- Sequelize(老牌、穩定)
🤔 為什麼推薦用 ORM?
因為:
- 不需要自己手動拼接 SQL 字串
- ORM 會根據你指定的條件,自動生成安全又高效的 SQL
- 能夠防止 SQL Injection(安全性高)
- 可讀性高,維護容易,適合中大型專案
- 直接用物件語法描述查詢,開發體驗更順暢
✅ 特別適合 GraphQL 這種「前端欄位需求多變」的應用場景!
🧩 ORM 如何配合 GraphQL Query?
在 GraphQL 中,前端可以自由選擇想要的欄位。
使用 ORM 時,後端的 Resolver 不需要自己去解析 info
、動態拼接 select,只需要在 ORM 提供的 select
參數中,列出這次需要哪些欄位就好。
ORM 會:
- 根據你列出來的欄位,自動產生 SQL 查詢
- 只查你要的欄位
- 自動處理關聯資料(像是 User 的 Posts)
💡 實際範例(以 Prisma 為例)
假設前端送出 Query:
query {
user(id: 1) {
name
email
posts {
title
}
}
}
後端的 Resolver 可以這樣寫:
async function getUser(parent, args, context, info) {
const user = await prisma.user.findUnique({
where: { id: args.id }, // 條件:找 id = 1 的 user
select: {
name: true, // 只選 name
email: true, // 只選 email
posts: {
// 也拿到這個 user 的 posts
select: {
title: true, // 只拿每篇 post 的 title
},
},
},
})
return user
}
✅ 這裡的 select
區塊,就是告訴 Prisma:
- 只拿
name
、email
- 拿
posts
,但每篇 post 只要title
Prisma 看到這樣的設定,就會自己產生底層 SQL,像是:
SELECT name, email FROM users WHERE id = 1;
SELECT title FROM posts WHERE user_id = 1;
✅ 完全不需要你手動寫 SQL,ORM 幫你搞定一切查詢細節!
select 的設計與效能優化
在使用 ORM(例如 Prisma)進行資料庫查詢時,我們常常會在查詢中指定一個 select
區塊。
這個 select
的作用是:
📋 精確告訴 ORM:「這次我只需要哪些欄位,其他不用。」
透過這個方式,ORM 就能只生成必要的 SQL 查詢,避免多查無用資料,大幅提升資料傳輸與查詢效能。
📝 select 的基本語法與概念
基本的 select
設計是這樣的:
select: {
欄位名稱: true,
另一個欄位: true,
...
}
每一個欄位後面寫 true
,意思就是:
✅ 「這個欄位我要查出來」
如果欄位沒有寫在 select
裡面,就代表這次查詢不需要這個欄位,ORM 也不會產生對應的 SQL 查詢。
小範例
假設有一個 User
表,欄位有:
- id
- name
- password
- createdAt
但前端只要 name
和 email
,那麼 select 可以這樣寫:
select: {
name: true,
email: true
}
這樣 ORM 最後產生的 SQL 會是:
SELECT name, email FROM users WHERE id = ?
✅ 只查 name
和 email
,password
、createdAt
這些欄位完全不會出現在結果裡!
🌐 巢狀關聯資料(Nested Relation)也可以用 select 控制
如果資料型別之間有關聯(例如 User 有很多 Posts),你可以在巢狀關聯裡面,再一次使用 select 指定子欄位要哪些。
範例:
select: {
name: true,
posts: {
select: {
title: true,
createdAt: true
}
}
}
這樣的意思是:
- 在 User 資料裡,要拿
name
- 同時拿這個 User 的
posts
(文章) - 但每篇文章只需要
title
和createdAt
ORM 會根據這個設定,生成兩段查詢,只取出指定欄位,非常乾淨、輕量。
🚀 select 的三大優勢
這種 select 設計可以帶來三大關鍵優勢:
📈 減少多餘資料傳輸 ➔ 降低前後端網路壓力
- 不查用不到的欄位 ➔ 回傳 JSON 更小
- 資料封包小 ➔ 網路速度快 ➔ 使用者體感更順暢
- 尤其是資料量很大的時候,效果會超明顯(例如一次查 1000 筆資料)
🔥 只拿必要資料 ➔ 整體查詢效能更好
- SQL 只 select 少量欄位 ➔ 資料庫查詢負擔減輕
- ORM/Server 資料整理速度也更快
- 省下大量無謂的資料處理(不必查回一堆根本沒用到的欄位)
尤其是在高流量、高併發的環境(例如大型平台、電商、社群網站),
這種細節會直接影響系統整體吞吐量(throughput)與成本。
🧹 保持後端程式碼一致、易於管理
- 透過 select,明確列出「這次要哪些欄位」
- 每個 Resolver 查什麼資料一目了然
- 減少錯誤:例如意外回傳敏感欄位(像 password、token)被外洩
- 對團隊合作、程式碼維護來說,非常重要!
✅ 乾淨、清楚的 select 寫法,就是後端 API 設計品質的一種體現!
⚠️ 如果不加 select 會發生什麼?
如果你在 ORM 查詢時沒有使用 select,
那麼預設情況下,ORM 會:
- 查出整張表的所有欄位
- 包含一些前端根本不需要、甚至不該給的敏感資料(例如密碼)
這會導致:
- 資料量變大 ➔ 傳輸變慢
- 記憶體消耗增加 ➔ 伺服器壓力變大
- 安全風險提高(例如誤傳密碼欄位到前端)
所以養成「凡是資料庫查詢都要用 select 篩選」的習慣是非常重要的!
組成資料 ➔ 回傳給前端
當後端的 Resolver 已經成功從資料庫查到需要的資料後,接下來的工作就是:
把這些資料根據前端一開始要求的欄位,組成符合 GraphQL 規範的 JSON 結構,然後回傳給前端。
這個步驟主要由 GraphQL Server 自動完成,不需要開發者自己手動組 JSON。
但是,開發者要負責確保:
- Resolver 回傳的資料結構,要符合 Schema 定義
- Resolver 沒有多給或少給前端要的欄位
Resolver 查好資料
前一個步驟,Resolver 已經:
- 根據
args
查到了正確的資料 - 根據
info
(或者 ORM 的 select)只拿了前端要求的欄位 - 把結果(通常是物件或陣列)回傳給 GraphQL Server
範例 Resolver 回傳的資料可能是:
{
name: "Alice",
email: "[email protected]",
posts: [
{ title: "我的第一篇文章" },
{ title: "旅行日記" }
]
}
✅ 這就是最乾淨、最純粹的資料內容。
GraphQL Server 根據 Query 組成 JSON 回應格式
GraphQL 規定,伺服器回傳給前端的資料必須遵守一個標準格式,大致結構是:
{
"data": {
"你查詢的根節點名稱": { 這次查詢的資料內容 }
}
}
所以根據這次前端送來的 Query:
query {
user(id: 1) {
name
email
posts {
title
}
}
}
GraphQL Server 最後打包出來的 JSON 回應就是:
{
"data": {
"user": {
"name": "Alice",
"email": "[email protected]",
"posts": [{ "title": "我的第一篇文章" }, { "title": "旅行日記" }]
}
}
}
✅
- 外層有一個
data
欄位 data
裡的user
對應到查詢時的名稱user
裡面包含這次要求的name
、email
和posts.title
- 沒有出現沒要求的欄位(例如 password、createdAt)
只回傳必要欄位的好處
這樣的資料回傳有很多優點:
優點 | 說明 |
---|---|
📦 資料體積小 | 沒有多餘欄位,網路傳輸更快 |
🔥 效能提升 | 伺服器處理更輕量,資料庫查詢更精簡 |
🔒 安全性高 | 不會意外把敏感資料(如密碼)傳給前端 |
🧹 JSON 結構乾淨 | 前端拿到的資料只包含自己需要的內容,開發起來更直覺 |
✅ 尤其在大型應用(例如一頁面同時查好多資料)時,這種「只回傳必要欄位」的設計可以大幅減少延遲,提升整體使用者體驗。
小細節補充:錯誤處理呢?
如果查詢成功,GraphQL Server 回傳的格式就是上面的:
{
"data": { ... }
}
但如果查詢過程中發生錯誤(例如:
- 查無此 id
- 欄位型別錯誤
- 權限不足
)
那麼 GraphQL Server 會回傳這種格式:
{
"data": null,
"errors": [
{
"message": "找不到使用者",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"]
}
]
}
✅ 這樣前端可以根據 errors
區塊,顯示錯誤訊息給使用者,而不是當掉整個頁面。
責任分工與流程總結
在一個完整的 GraphQL 資料請求過程中,每個角色負責的事情其實是很有邏輯分工的,可以分成以下四個主要階段:
責任分工表
階段 | 負責人 | 主要工作內容 |
---|---|---|
前端送 Query | 前端工程師 | 用 GraphQL 語法定義自己要的資料與欄位 |
GraphQL Server 解析 Query | 伺服器自動 | 解析 Query 結構,確認要查的 Type、欄位、參數 |
找到對應的 Resolver | 伺服器自動 | 根據 Schema,自動呼叫正確的 Resolver 函式 |
查資料庫(產生 SQL) | 你(後端開發者) | 在 Resolver 裡,設計資料查詢邏輯(手動 SQL 或 ORM) |
flowchart TD A(前端送出 Query) --> B(GraphQL Server) B(GraphQL Server 解析 Query) --> C(呼叫對應 Resolver) C(Resolver 收到 args + info) --> D1{自己決定查資料方式} D1 --> D2(手動組 SQL 查) D1 --> D3(用 ORM 查) D2 --> E(回傳資料) D3 --> E(回傳資料)
✅ 可以看到,「解析 Query」跟「查資料」是兩個完全不同的階段,負責的人也不同!
前端送出 Query ➔ 由前端負責
做什麼?
- 前端開發者或應用程式(App、Web)會寫出一段 GraphQL Query。
- 用 GraphQL 語法描述自己要拿哪個資料、哪些欄位。
- 可以自由選欄位、帶入變數,設計自己想要的資料結構。
小範例:
query {
user(id: 1) {
name
email
posts {
title
}
}
}
✅這裡前端明確告訴後端:
- 我要
id=1
的 user - 只需要
name
、email
和posts.title
- 其他欄位(例如
password
、createdAt
)不要回傳
GraphQL Server 解析 Query ➔ 由 Server 自動處理
做什麼?
- 後端的 GraphQL Server(像 Apollo Server、Yoga 等)自動解析前端送來的 Query。
- 分析出這個 Query 裡的:
- 要查哪一個 Type(例如 User)
- 需要哪些欄位(例如 name、email、posts.title)
- 有沒有帶參數(例如 id = 1)
這個步驟不用工程師手動做,Server 自己根據 Schema 定義,一層層拆解 Query。
✅ 這樣可以確保前端寫的 Query 是「合法的」,如果前端要求一個不存在的欄位,這裡就會直接報錯。
找到對應的 Resolver ➔ 由 Server 自動處理
做什麼?
- Server 根據剛剛解析好的 Query,
去對應 Schema 中定義的 Resolver 函式。 - 每一個 Query、Mutation、Field 都有對應的 Resolver。
例如:
type Query {
user(id: ID!): User
}
就對應到 Resolver 裡的一個 user
函式。
Server 會把解析出來的資訊(像是 args.id = 1
,info
欄位清單)自動傳給對應的 Resolver。
✅
這個步驟也是自動完成,不需要後端工程師干預。
查資料庫(產生 SQL 查詢) ➔ 由你寫的 Resolver 負責
做什麼?
到了這一步,
Server 只是負責「把參數、欄位清單交給 Resolver」而已。
真正怎麼查資料庫,是 Resolver 的責任。
在 Resolver 裡,你可以自由決定:
- 要自己手動組 SQL?
- 還是用 ORM 自動查詢?
- 要不要根據
info
解析出前端要求的欄位,動態產生 select? - 要不要做巢狀資料查詢(例如查 User 同時查 Posts)?
這是在 Resolver 裡自己選的方法:
做法 | 說明 |
---|---|
手動寫 SQL | 自己根據 args 和 info 拼 SQL 字串去資料庫查 |
使用 ORM | 呼叫 ORM API(例如 Prisma 的 findUnique),讓 ORM 自動根據條件產生 SQL 查 |
範例(自己寫 SQL):
const fields = getSelectedFields(info)
const sql = `SELECT ${fields.join(', ')} FROM users WHERE id = ?`
const user = await db.query(sql, [args.id])
範例(用 ORM 查資料):
const user = await prisma.user.findUnique({
where: { id: args.id },
select: {
name: true,
email: true,
posts: { select: { title: true } },
},
})
✅ 是不是自己產生 SQL?要查哪些欄位?要不要查關聯資料?這些細節全部是你(後端開發者)在 Resolver 裡決定的!
最後,總結一次 GraphQL 資料查詢的完整旅程
到這裡,我們已經完整走過了從前端發送 Query 到後端回傳資料的整個流程。
讓我們快速回顧一下,每一個重要步驟:
階段 | 說明 |
---|---|
前端發送 Query | 前端開發者用 GraphQL 語法,自由描述要拿哪些資料、哪些欄位 |
GraphQL Server 解析 | Server 解析 Query 結構、驗證參數與欄位、確保合法性 |
呼叫對應 Resolver | 根據 Query 自動呼叫對應的 Resolver,負責查資料 |
Resolver 查資料庫 | Resolver 根據參數和欄位需求,動態查詢資料庫(手動 SQL 或 ORM) |
整理資料回傳 | 將資料打包成標準 JSON 格式,只回傳前端需要的欄位 |
這篇文章的核心,你需要記住的是:
- GraphQL 是 Schema 驅動 的:前端能查什麼、怎麼查,都要靠後端事先定義好 Schema。
- GraphQL 最大的特色是 自由選欄位,只拿需要的資料,避免浪費。
- GraphQL Server 本身不查資料,它的角色是:「解析 Query ➔ 呼叫 Resolver」。
- Resolver 是真正去資料庫拿資料的人,可以選擇:
- 手動組 SQL 查資料(靈活度高)
- 或使用 ORM 自動查資料(開發速度快、安全性高)
- 使用 select 機制(無論手寫 SQL 還是 ORM select),可以讓資料查詢更輕量、更高效。
- 最後回傳給前端的是一個乾淨、符合 Query 結構的 JSON 回應,幫助前端更快渲染畫面。
小小展望:為什麼懂這一套很重要?
在現代前端越來越強大、資料需求越來越複雜的時代:
- 前端不只需要資料,還需要「靈活選取資料」的能力。
- 後端也不應該硬塞一大包資料給前端,而是要能「根據請求精準查詢」。
- GraphQL 正好完美地搭起了這座橋樑,讓資料流動變得更聰明、更高效。
只要理解了這一整套 Query → 解析 → Resolver → 查資料 → 回傳的流程,你就已經掌握了 GraphQL 核心運作的基礎功。
未來無論是開發中大型 Web App、行動 App,甚至是微服務架構,都可以運用這套思維設計出更乾淨、靈活、可擴展的 API 系統!