微介紹
schema
type User {
id: ID!
name: String!
posts: [Post]!
}
type Post {
id: ID!
title: String!
content: String!
}
type Query {
users: [User]!
}
針對每個 field 寫 resolver
如果沒寫的話就看拿到的 object 有沒有對應的欄位
有的話 那個值如果不是 function 就直接用那個值
如果是 function 的話就用那個 function 當作 resolver
users 有寫 resolver
=> 拿到很多 user object
=> id 跟 name 有了,直接用
=> posts 是一個 function,執行後拿到很多 post object
...
query {
users {
id
name
posts {
id
title
}
}
}
這個設計讓 server 可以做 lazy evaluation
不管 query 有幾層都容易處理,也讓 schema 可以做到 circular referencing
跟資料庫要資料的時候,會有一點小問題
1
2
3
4
5
6
7
如果有 n 個 user,就會跟資料庫 query n + 1 次
(這就是所謂的 n + 1 query)
1. 感覺就比較耗資源
2. 因為 connection pool 有限制同時連線數
和資料庫建立連線很費時,所以大多 server 都會 reuse connection
事先建立好 connection,放在 pool 裡,有人需要就拿去用
connection pool 都有最大 connection 的數量
限制每台 server 佔用的 connection 在合理的範圍
sequelize 預設最大連線數是 5
沒拿到 connection 就要等別人用完
所以沒辦法 n 個 query 一起出發
n 很大的話就要等很久
甚至一個 request 還沒結束還要先等別的 request
database
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
q
query {
users {
id
name
posts {
id
title
}
}
}
resolver 先看看下一層有哪些 field 是需要的
例如這個 query 裡面需要 posts
那在跟資料庫要 users 的時候,就順便一起抓他們的 posts
或是乾脆不用看,就直接一起抓回來
這樣 n + 1 直接變成 1 ~
query {
users {
id
name
posts {
id
title
comments {
id
content
user {
id
name
posts {
id
title
comments {
id
content
user {
id
name
posts {
id
title
}
}
}
}
}
}
}
}
}
不過要再下一層或是再下下一層,還是沒有辦法
硬要做的話,也容易搞得程式碼亂亂的
是一個源自 FACEBOOK (的一位員工)的套件
可以幫助解決這個問題
本來是在各個 function 裡面分別跟資料庫要資料
用 dataloader 的話
換成跟 dataloader 要資料
dataloader 收集完 再一起跟資料庫拿
例如本來在 user 的 posts 這個 function 裡面
我們要自己去跟資料庫要 posts ( by userId )
用 dataloader 的話,我們在 function 裡面就不直接跟資料庫 query
而是把 userId 給 dataloader,請他給我們 posts
然後在 dataloader 只要實作 batch function ( array to array ) 就好了
key
value
batch function
key
key
key
value
value
value
本來是這樣
用 dataloader 變這樣
User.prototype.posts = function getPosts() {
return db.models.Post.findAll({
where: {
UserId: this.id,
},
});
}
User.prototype.posts = function getPosts(args, { postsLoader }) {
return postsLoader.load(this.id);
}
然後 postsLoader 只要實作 batch 處理的 function
先簡介一下 resolver 的 4 個參數
const resolver = (parent, args, context, info) => {
...
}
const apolloServer = new ApolloServer({
schema,
context: ({ request }) => ({
jwtPayload: getJWTPayloadFromRequest(request),
postsLoader: createPostsLoader(),
}),
});
每個 request 進來的時候
我們可以設 context
接下來每個 resolver 都拿得到~
import DateLoader from 'dataloader';
import { Op } from 'sequelize';
import groupBy from 'lodash/groupBy';
import { db } from '../db';
const batchPosts = async (userIds) => {
const posts = await db.models.Post.findAll({
where: {
UserId: {
[Op.in]: userIds,
},
},
});
const postsMap = groupBy(posts, 'UserId');
return userIds.map((userId) => postsMap[userId]);
};
export default () => new DateLoader(batchPosts);
一次跟 db 拿全部
照 UserId 整理好
要對應原來的順序
keys => values
( array to array )
我猜:
dataloader 只要不在同一個 tick 執行 batch 就好了
node: process.nextTick(func)
node or browser: setTimeout(func)
因為 n 個 user 是同時拿到的
他們的 posts method 會在同一個 callstack 執行
1. apollo server 可以開 tracing
可以從 playground 看到每一個 field 花了多少時間
const apolloServer = new ApolloServer({
schema,
context: () => ({
postsLoader: getPostsLoader(),
}),
tracing: true,
});
2. 也可以自己 log 整個 request 處理的時間
在 apollo server 前面加一個 middleware,設定 response 完要算時間然後 log 出來
app.use((ctx, next) => {
if (ctx.request.path === '/graphql') {
const startTime = process.hrtime();
ctx.res.on('finish', () => {
const { operationName } = ctx.request.body;
const [aa, bb] = process.hrtime(startTime);
const elapsedTimeInMs = aa * 1000 + bb / 1e6;
debugResponseTime(`${operationName}: ${elapsedTimeInMs}ms`);
});
}
return next();
});
同樣的 key 如果有結果了就不用再拿
不同 request 如果共用 dataloader 的話就可以共用 cache