0%

建立 GraphQL API (2)

邊與連結

GraphQL 的威力來自於邊 (edge),也就是資料點之間的連結。當你建構 GraphQL 伺服器時,型態 (type) 通常對應模型 (model)。你可以想像這些型態就像資料被存放在資料庫的資料表 (table) 內,我們可以在那裡用連結 (connections) 來連結型態。接下來我們要來探討可以使用哪種連結來定義型態之間的相互關係。

一對多連結

使用者必須能夠讀取貼過的照片。我們要在一個名為 postedPhotos 的欄位讀取這種資料,它會被解析成使用者貼過的照片清單,而且這些照片會被過濾。

因為一位 User 可貼出多張 Photos,我們將它稱為一對多關係。我們將 User 加入 typeDefs:

typeDefs
1
2
3
4
5
6
type User {
appLogin: ID!
name: String
avatar: String
postedPhotos: [Photo!]!
}

此時,我們已經建立一個有向圖了。我們可以從 User 型態走到 Photo 型態。要產生無向圖,我們必須提供一條從 Photo 型態走回 User 型態的連結。我們在 Photo 型態加入 postedBy 欄位:

typeDefs
1
2
3
4
5
6
7
8
type Photo {
id: ID!
url: String!
name: String!
description: String
category: PhotoCategory!
postedBy: User!
}

藉由加入 postedBy 欄位,我們建立一條可返回貼出 Photo 的 User 的連結,建立一個無向圖。這是一對一連結,因為一張照片只能由一位 User 貼出。

為了測試伺服器,我們需要一些樣本資料,往後我們會將資料移到資料庫。先移除 index.js 中設定的空陣列 photos 變數。在專案的根目錄下建立一個子目錄 models 來放我們的樣本資料 data.js:

models/data.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
exports.users = [
{ appLogin: "scott", name: "老虎 Scott" },
{ appLogin: "blake", name: "葛蘭特 Blake" },
{ appLogin: "james", name: "麥克 James" }
];

exports.photos = [
{
id: "1",
name: "Dropping the Heart Chute",
description: "是我最喜歡的 Chute",
category: "ACTION",
appUser: "blake"
},
{
id: "2",
name: "Enjoying the sunshine",
category: "SELFIE",
appUser: "james"
},
{
id: "3",
name: "Gunbarrel 25",
description: "25 laps on gunbarrel today",
category: "LANDSCAPE",
appUser: "james"
}
];

然後將它加入 index.js 中:

index.js
1
const { users, photos } = require('./models/data');

因為連結是用物件型態的欄位建立的,所以它們可以對應解析函式。在這些函式中,我們可以使用父物件的資料來協助找到有關的資料並回傳。

我們將 postedPhotos 與 postedBy 解析函式加入服務:

resolvers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const resolvers = {
...
Photo: {
url: parent => `https://fakeimg.pl/120x160/?text=${parent.id}`,
postedBy: parent => {
return users.find(u => u.appLogin === parent.appUser)
}
},
User: {
postedPhotos: parent => {
return photos.filter(p => p.appUser === parent.appLogin)
}
}
};

我們必須在 Photo postedBy 欄位加入一個解析函式。我們可以自行決定如何在這個解析函式裡找到連結的資料。這裡我們使用陣列的 find( ) 方法取得 appLogin 符合每張照片的 appUser 值的使用者。

我們在 User 解析函式裡面使用陣列的 filter( ) 方法來取得該位使用者貼過的照片。這個 filter( ) 方法會回傳一個照片陣列。

接著我們試著傳送 allPhotos query:

query
1
2
3
4
5
6
7
8
9
query photos {
allPhotos {
name
url
postedBy {
name
}
}
}
result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"data": {
"allPhotos": [
{
"name": "Dropping the Heart Chute",
"url": "https://fakeimg.pl/120x160/?text=1",
"postedBy": {
"name": "葛蘭特 Blake"
}
},
{
"name": "Enjoying the sunshine",
"url": "https://fakeimg.pl/120x160/?text=2",
"postedBy": {
"name": "麥克 James"
}
},
{
"name": "Gunbarrel 25",
"url": "https://fakeimg.pl/120x160/?text=3",
"postedBy": {
"name": "麥克 James"
}
}
]
}
}

我們要自行連接資料與解析函式,但是一旦我們能夠回傳那個連接的資料,用戶端就可以開始編寫功能強大的 query。你可以在 allPhotos query 的 postedBy 下載加入 postedPhotos 看看:

query
1
2
3
4
5
6
7
8
9
10
11
12
query photos {
allPhotos {
name
url
postedBy {
name
postedPhotos {
name
}
}
}
}

多對多

接下來要在服務中加入 “在照片中標記使用者” 的功能。這意味著一位 User 可被標記 (tag) 在許多不同的照片中,而一張照片裡面可以標記許多不同的使用者。使用者與照片透過標記建立的關係稱為多對多,多位使用者對多張照片。

為了建立多對多關係,我們在 Photo 加入 taggedUsers 欄位,在 User 加入 inPhotos 欄位。

typeDefa
1
2
3
4
5
6
7
8
9
type User {
...
inPhotos: [Photo!]!
}

type Photo {
...
taggedUsers: [User!]!
}

taggedUsers 欄位會回傳一串使用者,而 inPhotos 欄位會回傳內含某位使用者的照片串列。

為了實作這個多對多連結,我們要在樣本資料中加入一個標記 (tags) 陣列,這是一個定義多對多關係的資料,它提供了兩組資料之間的交集。在關連式資料庫中稱為 intersection table,它定義兩個多對多資料表之間的關係。

models/data.js
1
2
3
4
5
6
exports.tags = [
{ photoID: "1", userID: "blake" },
{ photoID: "2", userID: "james" },
{ photoID: "2", userID: "scott" },
{ photoID: "2", userID: "blake" }
];

然後 import 到 index.js 中:

index.js
1
const { users, photos, tags } = require('./models/data');

當我們有張照片時,必須搜尋 tags 資料集來找出在照片中被標記的使用者。當我們有一位使用者時,就可以找到內含該使用者的照片串列。因為目前的資料放在 JavaScript 陣列裡面,所以我們在解析函式裡面使用陣列方法來尋找資料

resolvers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Photo: {
...
taggedUsers: parent => tags
// 回傳一個只含當前照片的 tag 陣列
.filter(tag => tag.photoID === parent.id)
// 將 tag 陣列轉換成 userID 陣列,我們只需要 userID 屬性
.map(tag => tag.userID)
// 將 userID 陣列轉換成使用者物件陣列
.map(userID => users.find(u => u.appLogin === userID))
},
User: {
...
inPhotos: parent => tags
// 回傳一個只含有當前使用者的 tag 陣列
.filter(tag => tag.userID === parent.appLogin)
// 將 tag 陣列轉換成 PhotoID 陣列,我們只需要 photoID 屬性
.map(tag => tag.photoID)
// 將 photoID 陣列轉換成照片物件陣列
.map(photoID => photos.find(p => p.id === photoID))
}

taggedUsers 欄位解析函式會濾除所有非當前照片的照片,並將過濾後的串列對應到實際的 User 物件組成的陣列。inPhotos 欄位解析函式會用使用者來過濾標記,並將使用者標記對應到實際的 Photo 物件組成的陣列。

接著我們可以傳送一個 GraphQL query 來查看每一張照片中有哪些使用者被標記:

query
1
2
3
4
5
6
7
8
9
query listPhotos {
allPhotos {
name
url
taggedUsers {
name
}
}
}

你應該會發現,我們有個 tags 的陣列,但沒有稱為 Tag 的 GraphQL 型態。GraphQL 並不要求資料模型完全匹配 schema 內的型態。用戶端可以藉由查詢 User 型態或 Photo 型態在每張照片找到被標記的使用者,以及有某位使用者被標記的照片。他們不需要查詢 Tag 型態,這只會讓事情更複雜。我們已經完成在解析函式中尋找被標記的使用者或照片的工作了,這可以讓用戶端更容易查詢這些資料。

接續下一篇 建立 GraphQL API (3)