REST和GraphQL:架构比较

2020-07-07 20:54:10

软件设计人员经常将定义、查询和更新数据的语言规范GraphQL与描述Web的体系结构样式REST进行比较。我们将探讨为什么这种比较没有意义,以及我们应该问些什么问题。在本文中,我们将讨论以下内容:

REST是罗伊·菲尔丁(Roy Fiding)在2000年发表的博士论文中提出的一种架构风格。这项工作研究了使万维网成功的属性,并导出了保留这些属性的约束条件。Roy Fiding也是HTTP/1.0和HTTP/1.1工作委员会的成员。其中一些约束被添加到HTTP和HTML规范中。

在理解REST之前,看看网络上的不同类型的参与者是很有用的:

网站:提供供人类在浏览器上消费和交互的内容的程序。

API服务提供者:旨在使其他程序能够使用数据并与其交互的程序。

API客户端:编写来使用来自API服务提供者的数据并与之交互的程序。

请注意,一个程序可以扮演多个角色。例如:API服务提供商也可以是从另一个API服务提供商消费API的客户端。

还要注意的是,互联网和万维网是不同的。互联网上还有其他我们在这里不谈的参与者(邮件服务器、Torrent客户端、基于区块链的应用程序等)

体系结构样式是一组命名的、协调一致的体系结构约束。体系结构约束是对体系结构的组件施加的限制,以便实现所需的属性。UNIX实用程序设计中使用的统一管道和过滤器体系结构就是一个例子。UNIX实用程序的建议做法是:

可重用性:如果第二个实用程序可以处理第一个实用程序中的数据,则允许混合和匹配任何两个实用程序。例如,我可以将cat或ls或ps的输出通过管道传输到grep。grep的作者并不担心输入是从哪里来的。

但是在遵循这个约束的同时,我们增加了处理的延迟,因为每个实用程序都必须将其输出写入标准输出,并且下一个实用程序必须从标准输入中读取输出。

另一种设计可以是将ls、grep、cat等设计为具有定义良好的接口的库。然后,最终用户需要编写集成不同库的程序来解决他们的问题。这个系统的性能会比以前的系统更高,可重用性也大致一样,但使用起来会更复杂。

软件设计是关于确定最能满足需求的设计约束集。

通过让每个请求发送处理该请求所需的所有内容来保持服务器无状态。

统一接口请求和响应必须具有能够解释它们的所有信息,即它们必须是自描述的。

作为应用程序状态引擎的超媒体(HATEOAS):客户端必须仅依靠对请求的响应来确定可以采取的后续步骤。不能有与此相关的带外通信。

分层系统:系统中的每个组件必须仅依赖于它直接与之交互的系统的行为。

按需编码:这是一个可选约束。服务器可以发送要由客户端执行的代码,以扩展客户端的功能(例如,JavaScript)。

这些约束的目的是使Web易于开发、可伸缩、高效,并支持客户端和服务器的独立发展。本文更详细地解释了这些约束。

HTTP已经成为实现REST架构风格的首选协议。一些约束(如客户端/服务器模式、将资源标记为缓存以及分层系统)被添加到HTTP中。其他的需要明确遵循。

尤其是统一接口&;HATEOAS是最常被违反的REST约束。让我们看看每个子约束:

按照惯例,URI充当资源ID的角色,而HTTP方法是可以在任何资源上执行的统一操作集。使用HTTP时,第一个约束后面会自动跟随定义。大多数后端框架(Rails、Django等)也会将您推向遵循第二个约束的方向。

客户端使用HTML或JSON等媒体类型检索和操作资源。诸如HTML的媒体类型还可以包含客户端可以对资源执行的动作,例如使用表单。这允许服务器和客户端的独立发展。理解特定表示的任何客户端都可以与支持该表示的任何服务器一起使用。

如果您希望有多个服务为类似类型的数据提供服务,并且有多个客户端访问它们,则此约束非常有用。例如,任何Web浏览器都可以呈现内容类型为text/html的任何页面。类似地,任何RSS阅读器都可以与支持应用程序/RSS+XML媒体类型的任何服务器配合使用。

自我描述信息:请求和响应必须具有能够解释它们的所有信息。

同样,这允许服务和客户端独立发展,因为客户端不采用特定的响应结构。这在Web浏览器或RSS阅读器的情况下工作得很好。但是,大多数API客户端都是为访问特定服务而构建的,并且与响应的语义相关。在这些情况下,维护自描述消息的开销并不总是有用的。

此约束还有望允许服务和客户端独立发展,因为客户端不会对下一步进行硬编码。如果消费者是使用浏览器的最终用户,则HATEOAS非常有意义。浏览器将简单地呈现HTML以及用户可以执行的操作(表单和锚定标记)。然后,用户将了解页面上有什么,并采取他们喜欢的操作。如果更改用户可用下一组操作的URL或表单参数,浏览器中不会有任何更改。用户仍然能够阅读页面,了解正在发生的事情(可能不情愿),并采取正确的行动。

如果更改API所需的参数,客户端程序的开发人员很可能必须了解更改的语义,并更改客户端以适应这些更改。

对于客户端开发人员来说,通过逐个浏览来发现API并不是很有用。全面的API文档(如Swagger或Postman集合)更有意义。

在具有微服务的分布式系统中,下一个操作通常由完全不同的系统执行(通过侦听从当前操作激发的事件)。因此,将可用操作列表返回给当前客户端是无用的。

很多API客户端都是针对单个后端编写的,并且API客户端和后端之间总是存在一定程度的耦合。在这些情况下,我们仍然可以通过确保以下各项来减少耦合量:

应该可以为新客户端添加新序列化格式(比如将JSON添加到HTML或protocol buf)。

上面的#1和#2通常是通过API版本控制和演进来实现的。Phil Sturgin有关于API版本控制和演变的很好的帖子,其中谈到了各种最佳实践。#3可以通过尊重HTTP Accept报头来实现。

GraphQL是一种定义数据模式、查询和更新的语言,由Facebook于2012年开发,并于2015年开源。GraphQL背后的关键思想是,不是实现用于获取和更新数据的各种资源端点,而是定义可用数据的总体架构,以及可能的关系和突变(更新)。然后,客户端可以查询他们需要的数据。

#我们的方案由产品、用户和订单组成。我们将首先定义这些类型和关系stype Product{id:int!标题:弦乐!价格:int!}类型用户{id:int!电子邮件:String!}类型OrderItem{id:int!产品:产品!数量:int!}类型订单{id:int!orderItem:[OrderItem!]!user:user!}#一些用于定义查询类型的帮助器类型stype ProductFilter{id:int title_like:string price_lt:int Price_gt:int}type OrderFilter{id:int userEmail:string userID:int ProductTitle:string ProductID:int}#定义可以查询的内容type query{#query user(email:string!):user!#query products(其中:ProductFilter,Limit:Int,Offset:Int)}#定义可以查询的内容type query{#query user(email:string!):user!#query products(其中:ProductFilter,Limit:Int,Offset:Int。Offset:int):[Order]}#定义突变的帮助器类型(更新)类型OrderItemInput{product:product!数量:int!}类型OrderInput{orderItems:[OrderItemInput!]!用户:user!}标量void#定义可能的更新类型突变{insert tOrder(input:OrderInput!):order!updateOrderItem(id:int!,数量:int!):OrderItem!ancelOrder(id:int):void}。

GraphQL的主要优势在于,一旦定义了数据模式和解析器:

客户端可以获取所需的确切数据,从而减少了所需的网络带宽(缓存可能会使这一点变得棘手--稍后将对此进行详细介绍)。

前端团队可以在执行时几乎不依赖后端团队,因为后端几乎公开了所有可能的数据。这使得前端团队可以更快地执行。

由于架构是类型化的,因此可以生成类型安全的客户端,从而减少类型错误。

要真正实现#1和#2,解析器通常需要采用各种筛选、分页和排序选项。

优化解析器很棘手,因为不同的客户端将请求不同的数据子集。

客户端很容易构造复杂的嵌套(并且可能是递归的)查询,这会给服务器带来大量工作。这可能会导致其他客户端的DoS。

GraphQL使前端开发人员可以毫不费力地进行迭代,但代价是后端开发人员必须在前期投入额外的精力。

使用Hasura,您不必担心前3个问题,因为Hasura将GraphQL查询直接编译为SQL查询。所以在使用Hasura时不编写任何解析器。生成的查询使用SQL Join来获取相关数据,避免了N+1查询问题。允许列表功能还提供了#4的解决方案。

理解了什么是GraphQL和REST之后,您可以看到GraphQL和REST是错误的比较。相反,我们需要问以下问题:

GraphQL与客户机/服务器、无状态、分层系统和按需代码的REST约束是一致的,因为GraphQL通常与HTTP一起使用,并且HTTP已经实施了这些约束。但是它打破了统一接口约束,在某种程度上也打破了缓存约束。

高速缓存约束规定,必须将对请求的响应标记为可高速缓存或不可高速缓存。实际上,这是通过使用HTTP get方法以及使用Cache-Control、eTags和If-None-Match报头来实现的。

理论上,如果将HTTP get用于查询并正确使用标头,则可以使用GraphQL并保持与缓存约束一致。然而,在实践中,如果不同客户端发送的查询差异很大,最终会导致缓存键爆炸(因为每个查询结果都需要单独缓存),并且缓存层的效用将会丢失。

像Apollo和Relay这样的GraphQL客户端实现了解决这些问题的客户端缓存。这些客户端将分解对查询的响应,并缓存各个对象。当激发下一个查询时,只需要重新获取缓存中没有的对象。这实际上可以比HTTP缓存带来更好的缓存利用率,因为即使响应的一部分也可以重用。因此,如果您在浏览器、Android或iOS中使用GraphQL,您不必担心客户端缓存问题。

但是,如果您需要共享缓存(CDN&39;s、Varish等),则需要确保您不会遇到缓存键爆炸。

Hasura Cloud通过向您的查询添加@Cached指令来支持数据缓存。阅读有关Hasura如何同时支持查询缓存和数据缓存的更多信息。

GraphQL打破了统一资源的限制。我们已经在上面讨论了为什么API客户端在某些情况下可以打破这个限制。统一资源约束预计会带来以下属性:

简单性-因为一切都是资源,并且具有适用于它的相同的HTTP方法集。

如果您正在构建一个GraphQL服务器,那么您仍然应该致力于将您的API建模为具有唯一ID的资源,并拥有一组统一的操作来访问它们。这使得开发人员可以更轻松地导航您的API。如果您正在构建一个后端GraphQL服务器,那么中继服务器规范是强制执行的,并且可以很好地遵循。

我们最近给Hasura增加了中继支持。由于Hasura自动从您的数据库模式生成GraphQL查询和突变,因此您还可以自动为您的每个资源获得一组统一的操作。

它们也越来越多地内置在javascript框架中,比如React、Vue、ANGLE,而不是从后端呈现模板。然后,javascript应用程序成为后端的API客户端。

前端应用程序也越来越多地通过移动网络访问,移动网络通常速度较慢且不稳定。

在设计当前的系统时,我们需要考虑这些因素。GraphQL有助于在此上下文中构建性能良好的应用程序。

如GraphQL部分所述,使用GraphQL的主要优势是前端的数据获取变得更容易,这使得前端的迭代速度更快。需要权衡的是:

统一资源约束被打破。对于绝大多数API客户端来说,这应该不是问题。

您需要确保可以触发的每个可能的查询都将得到有效的服务。

使用Hasura,您可以通过使用权限系统禁止聚合查询或通过设置允许查询的显式列表来解决上面的第二个问题。

难道我不能只使用查询参数来指定我需要的确切数据而不中断REST吗?

是的,您可以,例如,通过支持稀疏字段集规范。不过,在实践中使用GraphQL会更好,因为:

任何类型的查询语言都需要在后端进行解析和实现。GraphQL有更好的工具来实现这一点。

使用GraphQL,响应形状与请求形状相同,使客户端更容易访问响应。

但是,如果您确实需要遵循统一接口约束,这是您应该采取的方法。请注意,使用稀疏字段集还可能导致缓存键爆炸,从而破坏共享缓存。

我们已经研究了GraphQL和REST,并且比较它们是否有意义。系统设计经历了以下几个过程:

REST描述了Web的这些约束。因此,这些约束中的大多数都适用于大多数系统。例如,按资源组织API,为每个资源分配ID,并公开一组最常用的操作,这些都是GraphQL API设计的最佳实践(就此而言,基于RPC的系统也是如此)。

如果您想试用GraphQL,请前往学习课程。Hasura是一种快速入门和使用GraphQL的方法,因为您的后端是自动设置的。

Hasura使您的数据可以通过实时GraphQL API即时访问,因此您可以更快地构建和发布现代应用程序和API。Hasura连接到您的数据库、REST服务器、GraphQL服务器和第三方API(例如:Stripe),以即时跨所有数据源提供统一的实时GraphQL API。