最近在研究 GraphQL 准备在下一个项目中使用,而大部分项目都会使用 SQL 数据库,我一直在考虑其适用性与最佳使用方式。
这里假定使用 Sequelize 作为 ORM 框架,毕竟我们通常都需要使用 ORM 而不会手写 SQL 语句。
注意:目前我并没有 GraphQL 的实践,以下所有讨论都属于实践前的思考。
特别希望 Egg 团队或有 GraphQL 经验的同志来分享实践经验。我个人是从 C++ 转 前端 Node 的,并没有很多开发 Web 项目的经验。
GraphQL Schema Language 还是 Programmatic API ?
刚开始一个新项目的时候,一般会先设计数据库。数据库有了,应该会想办法批量的将 model 转换为 schema,而不是对照着 model,完全手写 schema。有2种方式定义 schema:
// schema.graphql
type User {
id: ID
name: String
}
type Query {
user(id: String): User
}
// resolver.js
const resolver = {
Query: {
user(source, args, ctx, info) { }
},
User: { }
}
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString }
}
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: GraphQLString }
resolve(source, args, ctx, info) { }
}
}
});
Apollo 有一套 schema, resolver, model, connector 的规范,schema 和 resolver 是分开的, 用 GraphQL Schema Language 来定义 schema,这样的结构很清晰,但是不容易控制。
如果要调整 schema,那么还要修改 resolver;如果 Query 都会支持一些通用的查询条件(比如 orderBy: UserOrderByFieldsEnum),那么会复写很多类似的 schema。
而使用 Programmatic API 容易控制,但是又不直观。这种方式通常是用于 database 自动生成 schema (应该没有人完全对照数据库的 table 手写 schema 吧?),或者定义通用的 Query 查询参数(比如每个 列表查询 都支持分页、排序、比较等等)。
这个本来是 GraphQL 的功能之一,但是这是对客户端来讲的,那么对于数据库呢?实际上数据库还是一次性查询出了所有的字段,然后只返回了客户端所需要的:
# query
user(id: 1) {
name
}
# sql
SELECT id, name, sex, age FROM User
如何让数据库也只获取需要的字段? 在 resolve 的 info 参数里面有当前请求的字段信息:
resolve: (source, args, ctx, info) => {
const requestedFields = getRequestedFieldsFromResolveInfo(info);
const findOpts = { attributes: requestedFields }; // 传递给 sequelize
return sequelizeModel.find(findOpts);
}
设计不好的 resolver 会导致 N+1 query,这对于数据源是 SQL 还好,如果是 BaaS 那就是钱的事了(大部分 BaaS 都是按请求数收费的)
一般来讲,在请求嵌套对象时,都会用到 JOIN 查询,对于 Sequelize 来讲就是 include 属性。
我 Google 过很多 graphql best practice,大部分都没有讲 JOIN,或者说不推荐 JOIN。
当查询嵌套对象时,会单独发起一个 sql 查询,所以对于一个简单的 1:1 模型的嵌套展开,会发起 2N 个请求。
他们推荐使用 dataloader 来进行缓存或批处理,这个库对于 SQL 来讲大概就是 where in 语句?将多余的 N 个请求 id 放到一个数组中一次查询,但是 in 应该要比 join 来得慢吧?
想要实现 JOIN, 大概还是要用到上面的从 resolveInfo 中获取查询字段,如果字段中包含内嵌对象,那么可以在外层查询中先 include,内层的 resolve 则判断 source 中有没有包含这个对象,如果没有再进行查询。这个方式并不优雅,你需要知道哪些字段是 association。那么问题来了,到底是实现 JOIN 更好还是使用 dataloader ?
const resolve = {
Query: {
users: (source, args, ctx, info) => {
const fields = getRequestedFieldsFromResolveInfo(info);
if(fields.role) {
return UserModel.find({ include: Role });
}
}
},
User: {
role: (source, args, ctx, info) => {
if(source.role) return source.role;
return RoleModel.get(source.role_id);
}
}
}
PS:
最主要的 schema 应该就是 Query schema,单个对象的查询支持 id 查询就行了,但是查询一个集合呢?
和 REST 类似的,我觉得分为 通用规范 和 业务规范。
所谓通用规范,就是默认的,所有字段都支持完全匹配查询、模糊查询、比较等查询,以及分页、排序等:
{
users(age_gt: 10, sex: { notEqualTo: "女" }) {
name
}
}
上面是查询 age 大于10,sex 不等于 “女”的 user 列表。这里使用了后缀方式和对象方式来进行组织查询规范,后者需要多定义一个 input 类型的 schema。
那么业务规范,就是只支持部分字段(多为 Scalar 字段,如 name)的查询,然后根据业务要求,增加一些虚拟字段如 hasRole (查询拥有 角色 的所有 user)等。当然,一般也有分页和排序的支持。
所以 GraphQL Query API 怎么设计更好?
基本上就是以上的问题让我纠结上不上 GraphQL,希望各位来探讨一下,并用你们的实践经验提供帮助
确实是这样,Google 上面的文章都是介绍 graphql 如何的好,如何的跨时代。真正深入去了解一下,发现还有好多坑要踩,因此很多后端对 graphql 没兴趣。要使用起来,就必须解决根本的问题。
除了上面的 4 个问题,我也有一些问题要补充。
权限系统是个棘手的问题,比如分散在各个 resover 中,或使用中间件,或使用指令等。
apollo 实现了 graphql 的指令系统,可以用来动态拦截。
schema 的重用比如:
# schema.graphql
type User {
# Id
id: Int!
# 用户名
username: String!
# 邮箱
email: String
# 手机
mobile: String
}
input UserCreateInput {
# 用户名
username: String!
# 邮箱
email: String
# 手机
mobile: String
# 密码
password: String!
}
上述两个类型中该如何抽离公共的字段。input 写多了满屏的重复字段,改起来也特别麻烦。
egg-graphql 的结构问题egg-graphql 将每个 graphql 模型分为 schema.graphql、resolver.js、connector.js 三个文件。按照规定做了一段时间。发现原先的 service 被架空了。如果要写 service,那么直接从 resolver.js 调用 service 好了,connector.js 就没必要存在。
另外访问 this.ctx.connector.xxxXxxx 下划线文件夹到驼峰式没有转换,只能 this.ctx.connector.xxx_xxx 访问,这算 bug 么。
我们在最近的一个项目里实践了 egg 结合 apollo graphql,感觉不错。
关于上面提到的几个问题,发表一下我的感觉。
推荐使用 apollo 的方式来定义 schema。 Graphql 本身只有类型系统,它的 schema 并不存在真正的继承或者组合,但是 schema 本身就是一个字符串,可以考虑用一个合适的模版引擎来处理吧。
个人觉得大部分查询都不会因为多查了几个字段会成为系统的瓶颈,而且借助于 connector 可以做到请求合并,在并发时,会减少打到数据库的请求量。
如果查询真的很复杂,上 redis 缓存
通过 Directive 去做,可以做到字段级别的切面,这个应该会比其他框架更加方便
我也不喜欢这个结构,自己写一个吧,并不复杂。
@lkspc 有了 prisma 还要啥 sequalize?
有个叫 prisma 的项目可以让你直接用 schema 来定义 database table,然后支持在 GraphQL 中使用 where 查询,似乎也没法结合 sequelize
个人感觉目前 apollo 前后端 + prisma,这一套已经基本可用了
@foreleven @MinJieLiu 权限系统这块是我比较纠结的地方,有没有什么通用的实现?
就REST API而言,通用的权限控制库已经很多的,我自己也写了一个Strongloop的动态权限控制, 最底层权限按照 [Model].[Operator] 定义.
让管理者可以在运行时自己定义角色和权限.
但是对于GraphQL如何用指令实现通用的权限控制,感觉没思路.
@snowyu 感觉用指令实现auth目前没有什么成熟的方案,还是把auth逻辑放在resolvers里面比较好
放在resolvers意味😿️你需要为每一个操作都去加上checkPerms的方法.每增加一个操作都要去搞一次.
这不是一劳永逸的办法,对Server Model开发者来说太麻烦.我希望的是他们把注意力放在Model和业务操作上,不用去了解 GraphQL 都没有问题,由框架帮他们自动解决.
resolvers就是普通函数,既然是函数就可以玩各种加壳,加中间件,来分离通用逻辑,就像koa中间件那样。现在社区里有类似graphql-middleware,graphql-shield这类工具帮你加壳。一个例子 https://github.com/maticzav/graphql-shield/blob/master/examples/basic/index.js
Great, let me see it later. The front-end will learn more knowledge about graphql if do so.
Maybe only one GraphQL server exist, and a REST API proxy server to GraphQL.
But DoS should be a problem.
我也遇到了你们综上所述的各种困扰。如今时隔两年,请问你们可以分享一下你们的最佳实践吗.
如今又是3个月过去了,这个最佳实践还是没有任何结果啊,感觉怎么做都显得复杂
如今又是3个月过去了,这个最佳实践还是没有任何结果啊,感觉怎么做都显得复杂
这个 issue 都关闭了怎么还在讨论。。。我早期在这里有过一些分享可以参考 https://github.com/ardatan/graphql-tools/issues/750
Most helpful comment
@lkspc 有了 prisma 还要啥 sequalize?
个人感觉目前 apollo 前后端 + prisma,这一套已经基本可用了