GraphQL应用程序中的五个常见问题(以及如何修复它们)
学习解锁GraphQL的强大功能而不会遇到其缺点
GraphQL现在风靡一时,并且有充分的理由:它是一种优雅的方法,可以解决许多与传统REST API相关的问题。
但是,如果我告诉你GraphQL没有自己的问题,那我就撒谎了。如果你不小心,这些问题可能不仅会导致代码库臃肿,甚至会导致应用程序显着减慢。
我在谈论的问题包括:
1.架构重复
2.服务器/客户端数据不匹配
3.多余的数据库调用
4.应用程序性能不佳(响应时间,带宽消耗)。
5.样本代码过量
6. restFull API过多难管理
7.restFull API过多学习成本高
问题:架构重复
从头开始编写GraphQL后端时,您首先意识到的一点是,它涉及许多类似但不完全相同的代码,特别是在涉及模式时。
也就是说,您的数据库需要一个模式,而GraphQL端点需要另一个模式。不仅要两次写同一个东西不仅令人沮丧,而且你现在有两个独立的事实来源,你需要不断保持同步。
解决方案:GraphQL Schema Generation
GraphQL生态系统中出现了许多解决此问题的方法。例如,PostGraphile从PostgreSQL数据库生成GraphQL架构,Prisma也将帮助您为查询和突变生成类型。
我还记得来自GraphQL团队的Laney Zamore和Adam Kramer描述了他们如何直接从他们的PHP类型定义生成他们的GraphQL模式。
对于Vulcan,我独立地发现了一个非常类似的解决方案。我使用SimpleSchema将我的模式描述为JavaScript对象,我开始只是将JavaScript的String类型转换为GraphQL字符串,将数字转换为Int或Float,依此类推。
所以这个JavaScript字段:
标题:{
type:String
}
将成为此GraphQL字段:
title:字符串
但是,GraphQL架构当然也可以有自定义类型:User,Comment,Event等。
我不想在模式生成步骤中添加太多魔法,因此我想出了字段解析器,这是一种让您指定这些自定义类型的简单方法。这个JavaScript字段:
userId{
type: String,
resolveAs: {
fieldName: 'user',
type: 'User',
resolver: document => {
return Users.findOne(document.userId)
}
}
}
变成:
user: User
如您所见,我们也在字段上定义了实际的解析器函数,因为它也与GraphQL字段直接相关。
因此,无论您使用PostGraphile之类的东西还是编写自己的架构生成代码,我都鼓励您避免在自己的应用程序中进行架构重复。
或者,您当然也可以使用Graphcool等托管服务来使用其仪表板管理您的架构并完全绕过该问题。
问题:服务器/客户端数据不匹配
正如我们刚刚看到的,您的数据库和GraphQL API将具有不同的模式,这些模式将转换为不同的文档形状。
因此,虽然数据库中新发布的帖子将具有userId属性,但通过API获取的相同帖子将具有用户属性。
这意味着在客户端上获取帖子作者的姓名将如下所示:
const getPostNameClient = post => {
return post.user.name
}
但在服务器上,它将完全是一个不同的故事:
const getPostNameServer = post => {
const postAuthor = Users.findOne(post.userId)
return postAuthor.name
}
当您尝试在客户端和服务器之间共享代码以简化代码库时,这可能是一个问题。除此之外,这意味着您错过了GraphQL在服务器上进行数据查询的出色方法。
我最近在尝试建立一个系统来制作每周时事通讯时感到很痛苦:每个时事通讯都包含多个帖子和评论,以及有关其作者的信息;换句话说,GraphQL的完美用例。但是这个通讯一代正在服务器上发生,这意味着我没有办法查询我的GraphQL端点......
解决方案:服务器到服务器GraphQL查询
或者我呢?事实证明,您可以运行服务器到服务器的GraphQL查询!只需将GraphQL可执行模式与GraphQL查询一起传递给graphql函数:
const result = await graphql(executableSchema,query,{},context,variables);
在Vulcan中,我将此模式概括为runQuery帮助程序,并且还为每个集合添加了queryOne函数。这些行为就像MongoDB的findOne一样,除了它们通过GraphQL API返回文档:
const user = await Users.queryOne(userId, {
fragmentText: `
fragment UserFragment on User {
_id
username
createdAt
posts{
_id
title
}
}
`
});
服务器到服务器的GraphQL查询帮助我大大简化了代码。它允许我将我的通讯生成调用从一系列连续的数据库调用和循环重构为单个GraphQL查询:
query NewsletterQuery($terms: JSON){
SiteData{
title
}
PostsList(terms: $terms){
_id
title
url
pageUrl
linkUrl
domain
htmlBody
thumbnailUrl
commentsCount
postedAtFormatted
user{
pageUrl
displayName
}
comments(limit: 3){
user{
displayName
avatarUrl
pageUrl
}
htmlBody
postedAt
}
}
}
这里要点:不要将GraphQL看作纯粹的客户端 - 服务器协议。 GraphQL可用于在任何情况下查询数据,包括使用Apollo Link State的客户端到客户端,甚至在使用Gatsby的静态构建过程中。
问题:多余的数据库调用
想象一下帖子列表,每个帖子都附有一个用户。您现在要显示其中10个帖子以及作者的姓名。
对于典型的实现,这将意味着两个数据库调用。一个获得10个帖子,一个获得与这些帖子相对应的10个用户。
但是GraphQL呢?假设我们的帖子有一个带有自己解析器的用户字段,我们仍然有一个初始数据库调用来获取帖子列表。但是我们现在有一个额外的调用来获取每个解析器的每个用户,总共11个数据库调用!
现在想象每个帖子也有5条评论,每条评论都有一个作者。我们的通话次数现已激增至:
1列表帖子
10位作者
10个评论的每个子列表10个
50位评论作者
对于总共71个数据库调用来显示单个视图!
没有人想要向他们的老板解释为什么主页需要25秒才能加载。值得庆幸的是,有一个解决方案:Dataloader。
解决方案:Dataloader
Dataloader将允许您批量和缓存数据库调用。
批处理意味着如果Dataloader发现您多次访问同一个数据库表,它会将所有调用一起批处理。在我们的例子中,10位作者和50位评论作者的电话都将被分成一个电话。
缓存意味着如果Dataloader检测到两个帖子(或帖子和评论)具有相同的作者,它将重用其已在内存中的用户对象,而不是进行新的数据库调用。
在实践中,你并不总能实现完美的缓存和批处理,但Dataloader仍然是一个巨大的帮助。
而且Vulcan使得使用Dataloader非常容易。开箱即用,每个Vulcan模型都包含Dataloader函数,作为“普通”MongoDB查询函数的替代品。
因此,除了collection.findOne和collection.find之外,您还可以使用collection.loader.load和collection.loader.loadMany。
一个限制是Dataloader仅在使用文档ID查询时有效。因此,您可以使用它来查询ID已知的文档,但是如果您想要请求最近创建的帖子,则仍需要访问数据库。
问题:性能不佳
即使启用了Dataloader,复杂视图仍然可以触发多个数据库调用,这反过来会导致加载时间变慢。
这可能令人沮丧:一方面,您希望充分利用GraphQL的图形遍历功能(“向我展示作者的评论作者......”等)。但另一方面,您不希望您的应用变得缓慢且无响应。
解决方案:查询缓存
但是有一个解决方案,即缓存整个GraphQL查询响应。与Dataloader不同,Dataloader的范围仅限于当前请求(意味着它只会在同一个查询中缓存文档),我在这里谈论缓存整个查询一段时间。
Apollo Engine是一个很好的方法。它是一个托管服务,提供有关GraphQL查询的分析,但它也有一个非常有用的缓存功能。
Vulcan带有内置引擎集成,您只需将API密钥添加到设置文件中即可。然后,您可以将enableCache:true参数添加到GraphQL查询中,以使用Engine缓存它们。
或者,使用Vulcan的内置数据加载高阶组件:
withList({
收藏:帖子,
enableCache:true
})(PostsList)
这种方法的优点在于,即使对于相同的解析器,您也可以轻松控制哪些查询被缓存,哪些查询不被缓存。例如,您可能希望缓存频繁访问的主页上显示的最近帖子列表,但不会缓存档案页面上可用帖子的完整列表。
最后一点:缓存可能并不总是可行的。例如,不建议频繁更改的数据,也不建议依赖于当前登录用户的数据。
问题:样本代码过量
这绝不是GraphQL应用程序独有的问题,但它们通常要求您编写大量类似的样板代码。
通常,向您的应用添加新模型(例如评论)将涉及以下步骤:
编写解析器以获取注释列表。
编写一个更高阶的组件(a.k.a。容器)来加载该注释列表。
(可选)编写解析器以通过ID或slug以及相应的高阶组件获取单个注释。
编写突变以插入新评论,编辑评论和删除评论。
添加相应的表单和表单处理代码。
这是一大堆CRUD!
解决方案:通用解析器,突变和高阶组件
Vulcan的方法是为每个提供智能,易用的通用选项。你会得到:
用于显示文档列表和单个文档的默认解析器。
用于加载文档列表或单个文档的预制高阶组件。
用于插入,编辑和删除文档的默认变异解析器。
生成的表单基于您的架构,可以使用上述所有内容。
这些都是以通用的方式编写的,它们可以与开箱即用的任何新模型一起使用。
可以肯定的是,这种“一刀切”的方法并非没有缺点。例如,因为查询是由通用的高阶组件动态生成的,所以使用静态查询会有点困难。
但是这种策略仍然是一种很好的入门方式,至少在你有时间将应用程序的每个部分重构为更加量身定制的解决方案之前。
GraphQL仍然相对较新,这意味着虽然每个人都在忙于颂扬它的优点,但很容易忽视构建GraphQL应用程序所涉及的真正挑战。
值得庆幸的是,这些挑战都有解决方案,而且我们讨论的越多(Vulcan Slack就是顺便说一句的好地方!),这些解决方案将会变得更好!
6. restFull API过多难管理
一个项目,几百上千个RESTfull API,即使使用了Swagger 管理,也只是降低了点难度,并没有根治问题。
7.restFull API过多学习成本高
我曾经参与过一个产品开发,之前用的都是restFULL API,几个外协和公司其他使用者 抱怨连连,一直吐苦水,你们的产品API太难用了,该怎么用,这么多API,要学到什么时候,我们才能基于你们的API正式进入项目开发。