從 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 的邏輯,它是完全沒辦法動作的。

所以實際情況是:

  1. 你定義了 type User { id, name }
  2. 前端寫了查詢 query { user(id: 1) { id name } }
  3. 如果後端沒有實作對應的 Resolver,這段查詢就會失敗,因為系統不知道去哪裡找這筆資料!

資料真正放在哪裡?➡ 資料庫 Table

我們知道 Type 是一種「格式宣告」,但資料本身總要有地方放吧?

在大多數系統中,這些資料是存在關聯式資料庫裡的表格(Table)中,例如:

假設你有一張叫 users 的資料表:

idname
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 系統會這樣處理:

  1. 檢查 Schema 定義:確認 user 是一個合法的查詢,回傳格式應該是一個 User 型別,且包含 idname 欄位。
  2. 交給 Resolver 處理:這時候 GraphQL 系統會呼叫你寫好的 resolver function,讓它去幫你處理查資料這件事。
  3. 拿到資料 → 組資料: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];
    }
  }
};

這段程式碼的邏輯是:

  1. 接收查詢傳進來的 id 參數
  2. 手動寫 SQL,從 users 資料表中撈出對應的那一筆資料
  3. 把查回來的資料(通常是一個物件)回傳給前端

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
2Resolver 接收參數 { id: 1 }
3執行 SQL 查詢 → 找出對應的使用者資料
4把查到的資料物件回傳給 GraphQL 系統
5GraphQL 幫你組好格式,回傳給前端

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 的對比

項目傳統 SQLORM
語法方式手動撰寫 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)會預設將類別名稱轉為複數,例如 Userusers,但 TypeORM 不會自動複數化,它會直接使用類別名(轉成小寫)作為預設表名。

如果你想自訂表名怎麼辦?

若你不想讓表名預設等於類別名稱,可以這樣手動指定:

@Entity('users')
export class User {
  ...
}

這樣 ORM 就會將這個實體類別對應到 users 資料表,而不是預設的 user

🛠️ 這樣的設計有什麼好處?

  1. 程式碼與資料表同步定義
    不需要手動維護 SQL 建表語法,修改 Entity 類別就能同步調整資料庫結構(可配合 migration 機制)。
  2. 型別整合,防止錯誤
    TypeScript 型別直接套用在欄位上,開發時可享有 IntelliSense、自動補全、型別檢查等好處。
  3. 可讀性與維護性高
    Entity 的程式碼就像文件一樣清楚明瞭,誰都可以看懂這張資料表的欄位結構與意圖。
  4. 支援進階功能
    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 幫你存取

這種三層串接的設計,讓你能夠清楚分工、彈性開發、可維護性高,是目前主流全端系統開發的常見模式。

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *