查询最佳实践
Reading time: 13 min
Graph提供了一种从区块链查询数据的去中心化方式。
Graph网络的数据通过GraphQL API公开,使得使用GraphQL语言查询数据更加容易。
本页将指导您了解基本的GraphQL语言规则和GraphQL查询最佳实践。
与REST API不同,GraphQL API构建在定义可以执行哪些查询的模式之上。
例如,使用token
查询获取代币的查询如下所示:
query GetToken($id: ID!) {token(id: $id) {idowner}}
它将返回以下可预测的 JSON 响应(当传递适当的 $id
变量值时):
{"token": {"id": "...","owner": "..."}}
上面的 GetToken
查询由多个语言部分组成(下面用[...]
占位符替换):
query [operationName]([variableName]: [variableType]) {[queryName]([argumentName]: [variableName]) {# "{ ... }" 代表一个选择集, 我们正在从`queryName`查询域。[field][field]}}
虽然语法的“是”和“不是”的列表很长,但在编写 GraphQL 查询时,以下是需要记住的重要规则:
- 每个操作只能使用一次
queryName
。 - 每个
field
在选择中只能使用一次(我们不能在token
下查询id
两次) - Some
field
s or queries (liketokens
) return complex types that require a selection of sub-field. Not providing a selection when expected (or providing one when not expected - for example, onid
) will raise an error. To know a field type, please refer to . - 分配给参数的任何变量都必须匹配其类型。
- 在给定的变量列表中,每个变量必须是唯一的。
- 必须使用所有已定义的变量。
不遵循上述规则将以 GraphAPI 返回的错误结束。
For a complete list of rules with code examples, please look at our .
GraphQL 是一种通过 HTTP 传输的语言和一组协议。
这意味着您可以使用标准fetch
(本机或通过@whatwg-node/提取
或isomorphic-fetch
) 查询 GraphQL API。
但是,正如中所说,我们建议您使用我们的graph-client
,该客户端支持以下独特功能:
以下是如何使用 Graph-client
查询Graph:
import { execute } from '../.graphclient'const query = `query GetToken($id: ID!) {token(id: $id) {idowner}}`const variables = { id: '1' }async function main() {const result = await execute(query, variables)// `result` is fully typed!console.log(result)}main()
现在我们已经介绍了 GraphQL 查询语法的基本规则,接下来让我们看看 GraphQL 查询编写的最佳实践。
一个常见的(不好的) 实践是动态构建查询字符串,如下所示:
const id = params.idconst fields = ['id', 'owner']const query = `query GetToken {token(id: ${id}) {${fields.join('\n')}}}`// Execute query...
虽然上面的代码生成了一个有效的 GraphQL 查询,但是它有很多缺点:
- 它使得从整体上理解查询变得更加困难
- 开发人员负责安全地消毒字符串插值
- 如果不将变量的值作为请求参数的一部分发送,则可能会阻止服务器端的缓存
- 它阻止工具静态分析查询(例如: Linter 或类型生成工具)
因此,建议始终将查询写为静态字符串:
import { execute } from 'your-favorite-graphql-client'const id = params.idconst query = `query GetToken($id: ID!) {token(id: $id) {idowner}}`const result = await execute(query, {variables: {id,},})
这样做有很多好处:
- 易于阅读和维护查询
- GraphQL 服务器处理变量的净化
- 可以在服务器级别缓存变量
- 查询可以通过工具进行静态分析(下面几节将详细介绍)
注意: 如何在静态查询中有条件地包括字段
我们可能希望仅在特定条件下包括 owner
字段。
为此,我们可以利用@include (if:...)
,如下所示:
import { execute } from 'your-favorite-graphql-client'const id = params.idconst query = `query GetToken($id: ID!, $includeOwner: Boolean) {token(id: $id) {idowner @include(if: $includeOwner)}}`const result = await execute(query, {variables: {id,includeOwner: true,},})
注意:相反的指令是@skip(if:…)。
GraphQL以其“问你所想”的口号而闻名。
因此,在GraphQL中,不单独列出所有可用字段,就无法获取所有可用字段。
在查询GraphQL API时,请始终考虑只查询实际使用的字段。
过度获取的一个常见原因是实体集合。默认情况下,查询将获取集合中的100个实体,这通常比实际使用的实体多得多,例如,用于向用户显示的实体。因此,查询几乎总是首先显式设置,并确保它们只获取实际需要的实体。这不仅适用于查询中的一层集合,更适用于实体的嵌套集合。
例如,在以下查询中:
query listTokens {tokens {# 获取最多100个tokenidtransactions {# 获取最多100个交易id}}}
该响应可以包含100个代币中的100个交易。
如果应用程序只需要10个交易,那么查询应该首先在交易字段上显式设置:first: 10
。
By default, subgraphs have a singular entity for one record. For multiple records, use the plural entities and filter: where: {id_in:[X,Y,Z]}
or where: {volume_gt:100000}
Example of inefficient querying:
query SingleRecord {entity(id: X) {idname}}query SingleRecord {entity(id: Y) {idname}}
Example of optimized querying:
query ManyRecords {entities(where: { id_in: [X, Y] }) {idname}}
您的应用程序可能需要查询多种类型的数据,如下所示:
import { execute } from "your-favorite-graphql-client"const tokensQuery = `query GetTokens {tokens(first: 50) {idowner}}`const countersQuery = `query GetCounters {counters {idvalue}}`const [tokens, counters] = Promise.all([tokensQuery,countersQuery,].map(execute))
虽然这个实现是完全有效的,但它需要使用GraphQL API进行两次交互。
幸运的是,在同一GraphQL请求中发送多个查询也是有效的,如下所示:
import { execute } from "your-favorite-graphql-client"const query = `query GetTokensandCounters {tokens(first: 50) {idowner}counters {idvalue}}`const { result: { tokens, counters } } = execute(query)
这种方法将通过减少在网络上花费的时间来提高整体性能(为您节省API交互的时间),并提供更简洁的实现。
编写GraphQL查询的一个有用功能是GraphQL片段。
查看以下查询,您会注意到某些字段在多个选择集({ ... }
) 中重复:
query {bondEvents {idnewDelegate {idactivestatus}oldDelegate {idactivestatus}}}
此类重复字段(id
、active
、status
)会带来许多问题:
- 当查询更广泛时将更难阅读
- 当使用基于查询生成TypeScript类型的工具时(上一节将详细介绍),
newDelegate
和oldDelegate
将产生两个不同的内联接口。
查询的重构版本如下:
query {bondEvents {idnewDelegate {...DelegateItem}oldDelegate {...DelegateItem}}}# 我们在代码转换器定义了一个碎片(子类型)# 将查询中重复的域分解fragment DelegateItem on Transcoder {idactivestatus}
使用GraphQLfragment
将提高可读性(特别是在规模上),也将使更好的TypeScript类型生成。
当使用类型生成工具时,上述查询将生成一个正确的DelegateItemFragment
类型(请参阅上一节“工具”)。
片段必须是一种类型
片段不能基于不适用的类型,简而言之,基于没有字段的类型:
fragment MyFragment on BigInt {# ...}
BigInt
是一个标量(原生“纯”类型),不能用作片段的基础类型。
如何传播片段
片段是在特定类型上定义的,应该在查询中相应地使用。
例子:
query {bondEvents {idnewDelegate {...VoteItem # Error! `VoteItem` cannot be spread on `Transcoder` type}oldDelegate {...VoteItem}}}fragment VoteItem on Vote {idvoter}
newDelegate
和 oldDelegate
属于Transcoder
类型。
无法在此处传播Vote
类型的片段。
将片段定义为数据的原子业务单元。
GraphQL Fragment必须根据其用法进行定义。
对于大多数用例,为每个类型定义一个片段(在重复使用字段或生成类型的情况下)就足够。
以下是使用Fragment的经验法则:
- 当相同类型的字段在查询中重复时,将它们分组为片段
- 当重复类似但不相同的字段时,创建多个片段,例如:
# base fragment (主要在上架中使用)fragment Voter on Vote {idvoter}# extended fragment (当查询投票中一个选项的细节)fragment VoteWithPoll on Vote {idvoterchoiceIDpoll {idproposal}}
Iterating over queries by running them in your application can be cumbersome. For this reason, don't hesitate to use to test your queries before adding them to your application. Graph Explorer will provide you a preconfigured GraphQL playground to test your queries.
If you are looking for a more flexible way to debug/test your queries, other similar web-based tools are available such as and .
为了跟上提到的最佳实践和语法规则,强烈建议使用以下工作流和IDE工具。
GraphQL ESLint
will help you stay on top of GraphQL best practices with zero effort.
config will enforce essential rules such as:
@graphql-eslint/fields-on-correct-type
: 字段是否用于正确的类型?@graphql-eslint/no-unused variables
: 给定的变量是否应该保持未使用状态?- 还有更多!
这将允许您捕获错误,而无需在playground上测试查询或在生产环境中运行查询!
VSCode和GraphQL
- 语法高亮显示
- 自动完成建议
- 根据模式验证
- 片段
- 转到片段和输入类型的定义
如果您使用的是graphql-eslit
,是正确可视化代码中内联的错误和警告的必备工具。
WebStorm/Intellij和GraphQL
- 语法高亮显示
- 自动完成建议
- 根据模式验证
- 片段