面向需求的数据模式设计
面向需求的数据模式设计的最佳实践
以面向需求、抽象的方式进行模式设计
向通用图的转变通常部分动机是为了简化客户端从支持分布式服务架构的GraphQL API获取所需数据的方式。虽然GraphQL承诺采用以客户端为中心的方法进行API设计和开发,但它为任何特定的模式将适用于实际客户用例并没有提供固有的保证。
为了最有效地支持消耗我们联合图形数据的客户端应用程序,我们必须有意识地以抽象、需求为导向的方式设计模式。这一概念被正式化为Principled GraphQL中的“敏捷原则”之一,即模式不应与任何特定客户端紧密耦合,也不应暴露任何特定服务的实现细节。
优先考虑客户需求,而不仅仅是单个客户的需求
创建一个同时面向需求并避免过度优先考虑单个客户需求的模式,需要一些前期工作——具体而言,在API设计初期就应该咨询客户端团队。从数据-作为产品的角度来看,这是一个确保产品满足用户需求的基础研究。随着图形和客户端需求的演变,这项研究也应该持续进行。
在可能的情况下,客户端团队应驱动这些讨论。这意味着在实践中,与其向客户端团队提供模式草案并征求反馈,不如通过练习让团队成员解释为渲染特定视图所需的确切数据,并建议该数据的理想形状。然后,模式设计者的任务是聚合这些反馈,并将其与您想通过您的图形推动的更广泛的产品体验相协调。
在考虑通过图形推动产品体验时,请记住,图形的整体模式是您产品的表示,每个联合模式都是产品内某个领域边界的表示。这也是为什么Apollo Federation在支持全渠道产品策略方面表现出色——图形可以根据产品功能和查询图形的客户端进行需求导向的设计。反过来,这些功能的查询可以随着这些功能的演变而发展。
保持服务实现细节不出现在模式中
咨询客户端团队还可以帮助您避免另一个模式设计的陷阱,即让模式过度受到支持服务或数据源的影响。
其他GraphQL整合方法可能会使您难以回避这一担忧,但联合允许您以表达图形中类型之间自然关系的方式设计模式。例如,在没有联合的分布式GraphQL架构中,外键类似的字段可能在子图模式的schema中是必要的,以便联合您的图形节点:
type Review {id: ID!productID: ID}
然而,在联邦模式下,一个评论服务的schema可以表示整个图的一个真正的子集:
type Product @key(fields: "id") {id: ID!}type Review {id: ID!product: Product}
作为另一个常见的漏出实现细节的例子,这里我们可以看到底层的REST API数据源如何能够影响一个服务schema中变更名称的mutations的名称:
extend type Mutation {postProduct(name: String!, description: String): ProductpatchProduct(id: ID!, name: String, description: String): Product}
更好的方法如下
extend type Mutation {createProduct(name: String!, description: String): ProductupdateProductName(id: ID!, name: String!): ProductupdateProductDescription(id: ID!, description: String!): Product}
修订后的Mutation
字段 更好地描述了从客户端的角度发生的事情,并提供了一种更精细的方法来处理一个产品名称和描述值的更改,这些更改需要在客户端应用程序中独立处理。使用两个单独的更新mutations也可以帮助区分客户端在没有patchProduct
mutations及其相关的参数 的情况下会发生什么(因为mutation可以处理更新一个值或另一个值,但不需要针对任何操作同时处理两个值)并节省了子图在运行时处理这些错误。我们将在下一节中更详细地讨论更精细的mutations的使用场景。
关于在schema中隐藏实现细节的最后一项相关意见,我们还应避免在schema中暴露客户端没有任何理由使用的字段。如果schema是根据产品功能和客户端使用案例的有意且迭代地开发,则可以轻松避免这个问题。
然而,当使用工具自动生成基于后端数据源的 GraphQL 模式时,你的模式中几乎不可避免地会出现一些客户端不需要但未来可能会出现意外用法的字段,这将使你的模式在长期内更难进化。这就是为什么在 Apollo,我们通常不建议使用模辦自动生成工具,因为这些工具会让您偏离以客户端为先的设计方法。
优先级提高模式表达性
一个良好的 GraphQL 模式将传达关于图底层节点及其关系的含义。模式表达性有许多维度——其中很多与其他模式设计最佳实践重叠,但在这里我们将专注于在服务间标准化命名和格式规范、在模式中设计有意义的字段,并通过在其 SDL 中添加详尽的文档来增强模式表达能力,从而最大限度地提高可用性。
标准化命名和格式规范
计算机科学中只有两件困难的事情:缓存失效和命名事物。
—— Phil Karlton
当试图在支持多个团队的分布式 GraphQL 架构中对事物进行一致命名时,这种观察的“命名事物”方面可能更具挑战性!(缓存也同样,但我们会稍后专门讨论这个话题。)
关于如何命名事物的连贯性显然是毫无疑问的,但当从多个 子图 组合成一个单一的联邦 GraphQL API 中时,这种连贯性就显得尤为重要。驱动联邦的原则是为了帮助提高客户端的连贯性,而这种连贯性应该包括命名约定。例如,一个服务中定义的 users 查询和在另一个服务中定义的 getProducts 查询,这并不能为客户提供一致或可预见的体验。与字段一样,类型命名和名称空格约定也应该在整个图中标准化。
此外,当一家公司已经在使用多个 GraphQL API,并将它们合并到联邦图时,那些现有模式中的类型名称可能发生冲突。在这些情况下,必须决定这些冲突的类型是应该成为图中的 实体 或值类型,或者是否需要某种名称空格的方法。
迁移到联邦图的项目伊始就是评估公司当前在现有的 GraphQL 模式 中使用的命名规范、确定哪些规范将成为标准化、将团队培训到这些规范,并在需要的情况下计划弃用和滚动更替的正确时机。此外,在图演变时应实施彻底的审查程序,以确保新的 字段、类型和服务遵循这些规范。
关于分页规范的简侧边栏
在整合 GraphQL API 时,标准化另一个重要领域是为客户端提供跨服务的分页结果的一致体验。GraphQL。关于这个话题,我们提供以下高层次指南:
- 当需要时添加分页。当基本列表就足够用时,不要向字段添加分页 参数。
- 当有理由进行分页时,将您的整合工作作为标准化的机会,标准化支持分页的 类型系统 元素(例如,参数和相关 对象类型 和枚举)。
- 在整个图上进行分页标准化并不意味着偏好一种分页风格而非另一种分页风格(例如,基于偏移量或 游标 的分页)。为工作选择合适的工具,但确保每种分页风格在每个服务中都是一致实现的。
- 您公司的图管理组应积极执行跨子图的分页标准,以保持客户端的一致性。
围绕具体用例设计字段
正如之前提到的,GraphQL 模式应根据客户端用例进行设计,理想情况下,为支持这些用例添加到模式中的字段将是单功能的。在实践中,这意味着要有更具体、更细致的 mutations 和查询。
虽然确保我们不向方案中暴露不必要的 fields 仍然很重要,但这并不意味着我们应该避免添加更多查询和 mutations 到方案中,如果它们是由客户端需求驱动的。例如,有两个 userById
和 userByUsername
查询可能是比接受名称或 ID 作为可空 参数 的单个 user
查询更好的选择。 因为更通用的 user
查询在未包含参数的情况下提交需要可空 参数,这可能会导致客户端在提交查询时产生歧义。
复杂的输入类型也可能使您的图的可见性故事复杂化。如果输入用于包含 query 参数,那么添加到输入中的每个额外字段都会使它在查看操作痕迹时的可见性工具中越来越难以确定导致特定缓慢查询的根本原因。
采用更细致的方法也适用于与更新相关的 mutations。例如,不要只使用一个 updateAccount
mutation 来统治所有,当客户端独立更新这些值时,使用更有目的性的 mutations。例如,考虑以下一系列用于更新用户账户信息的 mutations
:
extend type Mutation {addSecondaryEmail(email: String!): AccountchangeBillingAddress(address: AddressInput!): AccountupdateFullName(name: String!): Account}
如果这些值中的任何一个需要同时更新或者完全不更新,那么将更新组合成一个较粗粒度的变异操作是有意义的。但是,不考虑这个限制,选择更细粒度的变异可以帮助避免像细粒度查询一样陷入相同的陷阱,并节省你在运行时进行额外验证工作以确定提交的参数将导致变异的逻辑结果。
关于字段用例的最后一句话,一个模式是在API中添加一个viewer
或me
查询,并且GitHub GraphQL API提供了一个值得注意的例子:
extend type Query {# ..."The currently authenticated user."viewer: User!}
文档类型、字段和参数
良好的文档不仅仅是GraphQL的一个优点。文档化模式各种方面的必要性是由GraphQL规范所规范的。规范将文档声明为“类型系统的一个第一类功能”,并进一步说明所有可以描述的类型、字段、参数和其他定义如果没有自我描述性,都应该包含描述。
虽然从许多方面来看,精心设计、易于表达的schema将是自我解释的,但使用SDL支持的描述语法定义类型、字段和参数在API中的行为,将为graph消费者提供额外的透明度。例如:
extend type Query {"""Fetch a paginated list of products based on a filter."""products("How many products to retrieve per page."first: Int = 5"Begin paginating results after a product ID."after: Int = 0"""Filter products based on a type.Products with any type are returned by default."""type: ProductType = ALL): ProductConnection}
在上面的例子中,我们看到当查询和它的所有参数都有文档时,一个详细描述的products查询可能是什么样子。就像命名约定一样,在它的启动时建立跨联邦graph的文档标准对于确保API消费者的可预测性非常重要。同样,也应该有治理措施来确保随着schema的持续演变,文档标准得到遵守。
最后一点是,当文档化subgraphs模式文件时,我们无法在扩展类型(包括subgraph schemas中的扩展Query和Mutation类型)之上添加描述字符串,因为GraphQL规范指出,只有类型定义可以有描述,而不是类型扩展