GraphQL的概念简单解释
由於學習了GraphQL,因此對其進行概述筆記。除了如何使用的內容外,我會依據我自己的理解記錄基本的思維方式。
问题的例子
举个例子,我们来考虑一下管理某个团队和成员的API。非常简单的情况下,我们可以这样设计。
-
- チームが複数ある
-
- チームにはユーザーが複数所属する
- 各ユーザーは、ちょうど1つのチームに所属する
只需提前考虑一个典型要求就行了。
在REST中的一个示例
暫時需要以下以ER圖表達的資料表。
如果是REST API的话,首先可能会准备好GET /user/:id和GET /team/:id这样的选项。然后,根据应用程序的需求如“想要知道用户所属的团队”或“想要知道所属团队的用户”,来设计服务器端的端点。
在这里有一个典型的REST面板,基本上,需要根据应用程序需求来增加服务器端的端点(可以简单地提供一个API并在应用程序端组合所需的查询,但从根本上来说,随着要获取的信息类型增加,逻辑实现也会不断增加,本质上是相同的)。
以下のように記述されています:
– 在GraphQL中
– GraphQL中提供了
– GraphQL中包含有
在GraphQL中,只需注册以下类似的模式,就能够自由地构建并查询应用程序中所需的信息。
type User {
id: Int
name: String
team: Team
}
type Team {
id: Int
name: String
users: [User]
}
type Query {
getUser(id: Int!): User
getTeam(id: Int!): Team
}
查询示例
想知道ID为1的用户的名字。
query {
getUser(id: 1) {
name
}
}
我想知道ID为1的用户的姓名和所属团队名称。
query {
getUser(id: 1) {
name
team {
name
}
}
}
我想知道ID为1的用户的姓名和所属团队名称,以及属于该团队的用户的姓名。
query {
getUser(id: 1) {
name
team {
name
users {
name
}
}
}
}
根据所需信息的粒度,应用程序可以自行遍历对象的层次结构。在此过程中,无需在API端实现新的逻辑。由于GraphQL会自动处理跟踪所需信息的部分,因此非常方便。
机制
GraphQL的查询可以嵌套到任意深度(原则上)。当然,性能和资源是个问题。因此,可以使用延迟评估来处理具有潜在无限可能性的数据结构(也就是说,不能预先确定展开到何处为止)。而且,延迟评估可以通过闭包来轻松实现(参考)。在GraphQL中,用于延迟评估的闭包被称为解析器(Resolver)。
继续使用上述的示例,我们可以实现与以下对象和入口点相对应的函数(请注意,这只是一个代码示例,仅用于模拟氛围,需要查找和阅读类似教程的准确信息)。
type TeamResolver = {
id: number;
name: string;
users: () => Promise<UserResolver[]>;
};
type UserResolver = {
id: number;
name: string;
team: () => Promise<TeamResolver>;
};
export async function getUser(args: { id: number }): Promise<UserResolver> {
const user = await userLoader.load(args.id);
return {
id: args.id,
name: user.name,
team: async () => getTeam({ id: user.teamId }),
};
}
export async function getTeam(args: { id: number }): Promise<TeamResolver> {
const team = await teamLoader.load(args.id);
return {
id: args.id,
name: team.name,
users: async () => Promise.all(team.userIds.map((id) => getUser({ id }))),
};
}
在这里,TeamResolver.users和UserResolver.team的类型不是值而是函数是关键。
GraphQL解析器会根据查询创建所需的对象,但只执行被请求的属性的解析器。
通过这种方式,可以实现对任意深度嵌套查询的支持。
N+1问题
在以上的代码示例中,getTeam返回的TeamResolver.users的实现如下所示。
async () => Promise.all(team.userIds.map((id) => getUser({ id }))),
换句话说,如果要解决一个团队中的用户问题,就需要调用与团队人数相等的getUser函数。这是典型的N+1问题。为了解决这个问题,一种思路是除了getUser: id => User函数之外,再提供类似于getUsers: [id] => [User]这样的函数。然而,这样做会大大减弱GraphQL提供的查询灵活性和服务器实现的简易性等优点。
在GraphQL中,使用一个被称为DataLoader的机制来处理这种情况是很常见的。DataLoader是一个独立于GraphQL的机制,但由同样的开发者提供支持。
DataLoader是一个通用工具,它可以暂时缓存对Key-Value存储的查询。
DataLoader的用户(即我们应用的实现者)需要在构造函数中传递一个名为batchLoadFn:(keys: K[]) => (V | Error)[]的函数。这里的K和V分别表示键和值的类型。这个函数必须返回与给定键对应的值(或错误对象),正如您所见。
只要提供这个方法,DataLoader就可以提供一个load函数,该函数接受一个key并返回一个值或错误。 DataLoaer.load的作用是
- 不需要每次调用时都进行数据查询,而是将发生在一定时间范围内的所有查询键批量传递给batchLoadFn函数,然后将返回的值缓存起来。
进行这种周到的动作。从而实现对数据库查询的最小化。
只要数据源能从键列表中获取对象列表,DataLoader就可以缓存Key-Value存储的查询。虽然将其方便地描述为Key-Value存储的缓存,但本质上可以是任何数据源,包括关系型数据库(通常会执行类似于select … from … where id in (…)的查询)。