從 Resolver 開始,理解 GraphQL Type 與資料庫的關係
更新日期: 2025 年 5 月 1 日
初學者常會好奇:
「我寫了一個 GraphQL Type,為什麼前端就能查資料?」
「GraphQL 怎麼知道資料在哪張表?」
這些疑問其實很有道理,因為你看到的 Type 和真正的資料來源(資料庫)之間,並沒有自動連線的魔法,中間其實藏著一個重要的角色:Resolver。
這篇文章就會從這個核心問題切入,帶你搞懂:
- GraphQL Type 到底是什麼?
- 資料怎麼從資料庫流進前端?
- Resolver 和 ORM 扮演什麼關鍵角色?
GraphQL Type 不會查資料,它只是「定義格式」
當你在寫 GraphQL 的時候,第一步通常會先定義好 Schema 裡的 Type,例如:
type User {
id: ID!
name: String!
}
這段定義的意思是告訴前端開發者:
「我們的系統有一個
User
型別,它包含兩個欄位:id
是一個唯一識別碼,name
是一個字串。」
但請注意:這只是「格式描述」,並不等於你真的可以拿到這些資料。
把 GraphQL Type 想像成「菜單」
你可以把 GraphQL Type 想成是一間餐廳的菜單,它清楚列出:
- 哪些資料可以點(欄位是什麼)
- 每道資料會長什麼樣(資料型別)
但它不是廚房,不會煮菜給你吃。
換句話說:GraphQL Type 不會幫你去資料庫撈資料、組資料、處理邏輯,它只是一個規格說明書。
初學者常見誤解
很多初學者在剛接觸 GraphQL 時,會以為「只要定義好 Type,系統就會自己幫我查資料」。
但實際上,GraphQL Type 就像空殼一樣,沒有 Resolver 的邏輯,它是完全沒辦法動作的。
所以實際情況是:
- 你定義了
type User { id, name }
- 前端寫了查詢
query { user(id: 1) { id name } }
- 如果後端沒有實作對應的 Resolver,這段查詢就會失敗,因為系統不知道去哪裡找這筆資料!
資料真正放在哪裡?➡ 資料庫 Table
我們知道 Type 是一種「格式宣告」,但資料本身總要有地方放吧?
在大多數系統中,這些資料是存在關聯式資料庫裡的表格(Table)中,例如:
假設你有一張叫 users
的資料表:
id | name |
---|---|
1 | 小明 |
2 | 小美 |
這些表格才是真正「儲存資料的地方」,也就是我們想要透過 GraphQL 查出來的東西。
那我們的任務是什麼?
我們的任務就變成:
「如何把 GraphQL 查詢,轉成資料庫查詢?」
「如何把 GraphQL 的User
Type,對應到users
資料表?」
而這個「查詢轉換」和「資料搬運」的任務,正是由 Resolver 層負責完成的。
它是整個系統中,把 GraphQL 和資料庫接起來的橋梁。
Resolver 是誰?為什麼它是串連 Type 與資料庫的橋樑?
Resolver 的核心任務
當前端發出一段 GraphQL 查詢時,GraphQL 本身不會主動去查資料庫,它只會依照 Schema 的定義「確認這段查詢是否合法」。
那麼,實際執行查詢、拿資料、組回傳格式的動作,要交給誰來做?
答案就是:Resolver。
Resolver 是 GraphQL 系統中的「資料處理員」,負責:
- 接收前端的查詢請求
- 去資料庫查資料(或叫 API、或做計算)
- 回傳符合 GraphQL Type 結構的資料
Resolver 在整個流程中扮演什麼角色?
我們來看一段典型的查詢流程:
query {
user(id: 1) {
id
name
}
}
這段查詢的意思是:「我要查一個 ID 是 1 的使用者,請給我他的 id 和 name。」
GraphQL 系統會這樣處理:
- 檢查 Schema 定義:確認
user
是一個合法的查詢,回傳格式應該是一個User
型別,且包含id
、name
欄位。 - 交給 Resolver 處理:這時候 GraphQL 系統會呼叫你寫好的
resolver function
,讓它去幫你處理查資料這件事。 - 拿到資料 → 組資料:Resolver 拿到資料後,會依照 GraphQL Type 的結構包裝起來,送回前端。
Resolver 的位置圖解:
flowchart TD A[GraphQL Query] -->|查詢| B[Schema: 這個查詢合法嗎? 預期回傳什麼型別?] B -->|驗證通過| C[Resolver: 根據查詢參數,去資料庫查資料] C -->|請求資料| D[Database: 回傳原始資料] D -->|資料| E[Resolver: 格式化成符合 GraphQL Type 的資料] E -->|回傳結果| F[前端收到結果]
舉例:查一位使用者
假設前端送出這段查詢:
query {
user(id: 1) {
id
name
}
}
你可以這樣實作 Resolver(以 JavaScript + Node.js 為例):
const resolvers = {
Query: {
user: async (_, { id }) => {
const result = await db.query(
"SELECT id, name FROM users WHERE id = $1",
[id]
);
return result.rows[0];
}
}
};
這段程式碼的邏輯是:
- 接收查詢傳進來的
id
參數 - 手動寫 SQL,從
users
資料表中撈出對應的那一筆資料 - 把查回來的資料(通常是一個物件)回傳給前端
Query: { user: ... }
這表示你在 schema.graphql
中定義了以下語法:
type Query {
user(id: ID!): User
}
也就是說:
你允許前端發出 user(id: 1)
這樣的查詢,並期望回傳一筆 User
型別的資料。
這個 user
查詢在程式中對應到 resolvers.Query.user
這段函式。
async (_, { id }) => { ... }
這是負責處理 user
查詢的 resolver 函式:
- 第一個參數
_
是「父層回傳值」(parent),這裡沒用到所以用底線代替 - 第二個參數
{ id }
是這個查詢傳入的參數,也就是user(id: 1)
裡的id
➡️ 當前端執行這段查詢:
query {
user(id: 1) {
id
name
}
}
系統就會呼叫這個函式,並把 { id: 1 }
傳進來。
await db.query(...)
這行是資料查詢的重點邏輯,你用 SQL 向資料庫要資料:
SELECT id, name FROM users WHERE id = $1
$1
是一個參數佔位符,會被替換成第二個參數[id]
的值(這裡是1
)- 使用參數化的方式可以避免 SQL Injection 攻擊
這個查詢會從 users
表中找到 id 為 1 的那一筆資料。
result.rows[0]
這是查詢的回傳結果:
- 多數資料庫函式會回傳一個「資料列陣列」
- 這裡只需要一筆資料 → 使用
rows[0]
拿出陣列中的第一筆
這筆資料會是像這樣的 JavaScript 物件:
{ id: 1, name: "小明" }
這筆資料會被傳回去給 GraphQL 系統,根據 schema 中 User
的型別描述,自動整理成前端想要的格式。
小結:這段 Resolver 做了什麼?
步驟 | 描述 |
---|---|
1 | 前端查詢 user(id: 1),GraphQL 找到對應的 resolver |
2 | Resolver 接收參數 { id: 1 } |
3 | 執行 SQL 查詢 → 找出對應的使用者資料 |
4 | 把查到的資料物件回傳給 GraphQL 系統 |
5 | GraphQL 幫你組好格式,回傳給前端 |
Resolver 自己要寫 SQL 嗎?這時候 ORM 登場
當我們在 Resolver 裡處理查詢邏輯時,一個直覺的做法是直接用 SQL 查資料庫。
這種方式雖然清楚直接,但在實務開發中往往不夠靈活與安全。
傳統方式:直接撰寫 SQL 查詢
假設你要在 resolver 裡查一筆使用者資料,傳統 SQL 寫法可能像這樣:
const result = await db.query("SELECT * FROM users WHERE id = $1", [id]);
return result.rows[0];
這段程式碼的意思是:
- 手動撰寫 SQL 語句
- 使用參數化(例如
$1
)避免 SQL Injection 攻擊 - 查詢結果需要自行解析(例如
rows[0]
)
雖然這樣的寫法可以精確控制查詢邏輯,對資料庫也非常透明,
但它也有以下缺點:
缺點 | 說明 |
---|---|
❌ 易出錯 | 每個欄位都要自己打、自己拼字串,容易拼錯或打錯欄位名 |
❌ 可讀性低 | 多數查詢邏輯都藏在 SQL 字串中,不易維護 |
❌ 型別不一致 | 查出來的資料是原始物件,與程式中的型別無法自動對應 |
❌ 難以重複利用 | 若要用同樣的查詢邏輯,需要複製貼上,缺乏模組化結構 |
更好的選擇:使用 ORM(Object-Relational Mapping)
為了解決這些問題,開發者常會引入 ORM(物件關聯對應)工具,讓你能:
用「操作物件」的方式來查詢、更新資料庫,而不是手動撰寫 SQL。
ORM 會幫你把資料表對應成「實體類別(Entity)」,並提供許多內建的查詢方法,
大幅簡化開發流程。
ORM 範例:使用 TypeORM 查一筆資料
const user = await UserRepository.findOne({
where: { id: 1 }
});
這段程式碼的功能與剛剛的 SQL 相同,但語意更清晰、結構更安全。
你不用再寫 SELECT * FROM ...
這種字串查詢,也不用手動處理回傳的 rows 陣列。
TypeORM 幫你把所有結果自動轉成你定義的 User
類別物件。
小結:SQL 與 ORM 的對比
項目 | 傳統 SQL | ORM |
---|---|---|
語法方式 | 手動撰寫 SQL 字串 | 操作物件與方法 |
可讀性 | 較低,易出錯 | 較高,語意清楚 |
型別整合 | 無,回傳原始資料 | 有,自動對應類別欄位 |
重用性 | 差,邏輯分散 | 高,可封裝於 Repository / Service |
控制彈性 | 高,自訂 SQL 靈活 | 中,可搭配 query builder 使用 |
ORM 如何做到?—實體類別定義(Entity)
ORM(Object-Relational Mapping)工具之所以能讓你用「物件」操作資料庫,關鍵就在於:
你必須先定義一個實體類別(Entity)來對應資料庫中的資料表。
這個類別就像是程式碼中的「資料模型」,它描述了這張資料表的結構、欄位名稱、型別與限制。
ORM 就能根據這些設定,自動產生對應的 SQL 指令並執行。
你可以把它想成是:
- 一份描述資料結構的「合約」
- 一個能讓 ORM 自動操作資料庫的「中介層」
範例:使用 TypeORM 定義一個 User
Entity
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
這段程式碼的背後含義如下:
裝飾器 | 功能說明 |
---|---|
@Entity() | 宣告這個類別是資料表對應的實體。預設會使用類別名稱作為資料表名稱。在 PostgreSQL 等資料庫中,如果你沒有手動指定,User 類別會對應到 user 表(小寫,非複數)。 |
@PrimaryGeneratedColumn() | 指定 id 為主鍵,並由資料庫自動產生遞增值,相當於 SERIAL PRIMARY KEY。 |
@Column() | 定義一般欄位,預設會依據 TypeScript 的型別自動推斷 SQL 欄位型別(如 string → VARCHAR)。 |
📌 註:有些 ORM(如 Sequelize 或 Mongoose)會預設將類別名稱轉為複數,例如
User
→users
,但 TypeORM 不會自動複數化,它會直接使用類別名(轉成小寫)作為預設表名。
如果你想自訂表名怎麼辦?
若你不想讓表名預設等於類別名稱,可以這樣手動指定:
@Entity('users')
export class User {
...
}
這樣 ORM 就會將這個實體類別對應到 users
資料表,而不是預設的 user
。
🛠️ 這樣的設計有什麼好處?
- 程式碼與資料表同步定義
不需要手動維護 SQL 建表語法,修改 Entity 類別就能同步調整資料庫結構(可配合 migration 機制)。 - 型別整合,防止錯誤
TypeScript 型別直接套用在欄位上,開發時可享有 IntelliSense、自動補全、型別檢查等好處。 - 可讀性與維護性高
Entity 的程式碼就像文件一樣清楚明瞭,誰都可以看懂這張資料表的欄位結構與意圖。 - 支援進階功能
ORM 提供的裝飾器還可以設定欄位預設值、是否可為空、是否唯一,甚至是關聯(如一對多、多對一)與索引等。
使用 Entity 的實際操作範例
定義好 User
類別之後,就能用這個實體進行各種操作,ORM 會自動幫你轉換成對應的 SQL 查詢。
新增資料
const user = new User();
user.name = "小明";
await userRepository.save(user);
自動產生的 SQL:
INSERT INTO user (name) VALUES ('小明');
查詢資料
const user = await userRepository.findOne({
where: { id: 1 }
});
對應 SQL:
SELECT * FROM user WHERE id = 1;
查出來的結果也會自動變成 User
類別的實體,可以這樣操作:
console.log(user.name); // "小明"
✏️ 更新資料
user.name = "小華";
await userRepository.save(user);
ORM 會自動比對差異並產生:
UPDATE user SET name = '小華' WHERE id = 1;
小提醒:Entity 是結構,不是資料本身
Entity 是「資料表的描述」,就像前端的 interface:
- 它不包含資料,只描述資料長什麼樣子
- 真正執行存取的是 ORM 的各種方法(如
.save()
、.find()
)
Entity 讓 ORM 知道應該如何對應表格、產生 SQL、解析結果,這是它最核心的價值。
小結:Entity 是資料邏輯與資料庫之間的橋梁
概念 | 說明 |
---|---|
Entity 類別 | 描述資料表的結構與型別 |
@Entity() 裝飾器 | 宣告這是一個資料表對應的實體類別 |
ORM 自動產生 SQL | 根據你對 Entity 的操作產生正確的查詢語法 |
與 TypeScript 整合 | 提供開發效率、型別檢查、自動完成等好處 |
實體類別的設計,是 ORM 能夠簡化資料存取、整合 GraphQL resolver 邏輯的基礎。
GraphQL 是怎麼知道該執行哪個 Resolver 的?
當我們學會定義 GraphQL 的 Type 與查詢語法(schema)之後,下一個重要的問題就出現了:
這些查詢實際上是怎麼被執行的?
GraphQL 是怎麼知道資料要從哪裡來?又該執行哪一段邏輯?
答案的關鍵在於:Resolver 是如何被 GraphQL Server 串接進來的。
GraphQL 本身不會自動執行查詢邏輯
GraphQL 的設計理念,是將「查什麼資料」與「怎麼查資料」分開。
- Schema 負責定義前端可以查詢什麼資料、每個欄位的結構與型別。
- Resolver 則負責實際執行查詢邏輯,例如從資料庫取資料、轉換格式或處理驗證。
但請注意:GraphQL 本身不會自動知道 Resolver 的位置,而是需要靠你所使用的 GraphQL Server 工具(例如 Apollo Server、Yoga、express-graphql 等)來手動對接。
Resolver 是怎麼串進 GraphQL Server 的?
Resolver 本質上是一個普通的 JavaScript 物件,它不是 GraphQL 的內建概念。
只有當你將它「明確地」傳給 GraphQL Server 工具時,這些工具才會幫你把 Resolver 與 Schema 做對應。
以 Apollo Server 為例:
const { ApolloServer } = require('@apollo/server');
const typeDefs = `# GraphQL schema 定義`;
const resolvers = {
Query: {
user: (parent, args) => {
// 查詢邏輯
}
}
};
const server = new ApolloServer({
typeDefs,
resolvers // 在這裡指定 Resolver,Apollo 才能幫你對應查詢邏輯
});
這段程式碼的重點在於:你把 typeDefs
(查詢格式)與 resolvers
(查詢邏輯)組合起來,Apollo Server 就能在執行查詢時,自動找到對應的函式來處理。
✔️ 你也可以把變數名稱改成 myResolvers
或其他,只要傳給 ApolloServer
時對應得上即可。
Resolver 的結構如何對應 Schema?
Resolver 的結構必須與你定義的 Schema 一一對應。
假設 Schema 如下:
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
}
那麼對應的 Resolver 必須長這樣:
const resolvers = {
Query: {
user: (parent, args) => {
// 查詢邏輯
}
},
User: {
name: (parent) => {
// 欄位處理邏輯(可選)
}
}
};
GraphQL Server 會根據這個結構,自動找到對應的 resolver 函式。
Query.user
:處理根層查詢user(...)
User.name
:處理User
型別中的name
欄位(如果沒寫,會自動回傳物件中的name
屬性)
理解 Resolver,需要搭配 GraphQL Server 的基礎知識
要正確理解 Resolver 的運作方式,建議先了解 GraphQL Server 的基本架構,特別是 Apollo Server 常用的幾個關鍵概念:
元件 | 說明 |
---|---|
typeDefs | 定義查詢格式(Query、Mutation、Type 等) |
resolvers | 定義每個查詢/欄位對應的執行邏輯 |
ApolloServer 設定 | 把 schema 和 resolver 組合起來,建立伺服器 |
Resolver 參數 | 每個 resolver 會收到 parent, args, context, info 四個參數 |
GraphQL 的執行邏輯依賴 GraphQL Server 的對應機制
項目 | 說明 |
---|---|
Resolver 本身不具魔法 | 它只是個普通物件,要手動傳給 GraphQL Server 才會生效 |
對應關係不是自動建立 | 需要你在 Apollo Server 中明確設定 resolvers |
結構需對應 schema 型別 | Resolver 的 key 結構必須與 schema 中的 Type/Field 對應 |
理解 Resolver 要懂 Apollo | 因為 resolver 是搭配 GraphQL Server(如 Apollo)一起運作的核心角色 |
小結:GraphQL Resolver 是真正撐起 GraphQL 資料流的橋梁
- GraphQL Type 只是定義資料長相,不會查資料
- Resolver 是中間的「資料搬運工」,接收請求、查資料、轉格式
- ORM 協助 Resolver 更方便查資料庫
- 真正的資料存在資料庫 Table 中
- 整體形成一條資料傳遞鏈:Type → Resolver → ORM → 資料庫
整體流程圖:GraphQL 查詢背後的三層串接模型
現在讓我們把這些角色串起來,看整體架構是如何從「查詢」一路通到「資料庫」。
flowchart TD A[GraphQL Type] -->|定義資料格式| B[Resolver] B[Resolver] -->|執行查詢邏輯| C[ORM] C[ORM] -->|協助資料存取| D[Database Table] style A fill:#f9f9f9,stroke:#6b8e23,stroke-width:2px style B fill:#f9f9f9,stroke:#4682b4,stroke-width:2px style C fill:#f9f9f9,stroke:#cd853f,stroke-width:2px style D fill:#f9f9f9,stroke:#708090,stroke-width:2px
你可以把它理解成一條從「語意 → 邏輯 → 存取 → 資料」的資料流通路線:
- 🔹 前端門面:GraphQL Type ➡ 前端說「我想查這些欄位」
- 🔸 中介邏輯:Resolver + ORM ➡ 後端負責查資料、轉格式、處理邏輯
- 🔹 資料來源:Database ➡ 真正的資料存放在資料表中,由 ORM 幫你存取
這種三層串接的設計,讓你能夠清楚分工、彈性開發、可維護性高,是目前主流全端系統開發的常見模式。