查询最佳实践
Reading time: 10 min
The Graph provides a decentralized way to query data from blockchains. Its data is exposed through a GraphQL API, making it easier to query with the GraphQL language.
Learn the essential GraphQL language rules and best practices to optimize your subgraph.
与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]}}
- 每个操作只能使用一次
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 . - 分配给参数的任何变量都必须匹配其类型。
- 在给定的变量列表中,每个变量必须是唯一的。
- 必须使用所有已定义的变量。
Note: Failing to follow these rules will result in an error from The Graph API.
For a complete list of rules with code examples, check out .
GraphQL is a language and set of conventions that transport over HTTP.
It means that you can query a GraphQL API using standard fetch
(natively or via @whatwg-node/fetch
or isomorphic-fetch
).
However, as mentioned in , it's recommended to use graph-client
, which supports the following unique features:
Here's how to query The Graph with graph-client
:
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()
More GraphQL client alternatives are covered in .
A common (bad) practice is to dynamically build query strings as follows:
const id = params.idconst fields = ['id', 'owner']const query = `query GetToken {token(id: ${id}) {${fields.join('\n')}}}`// Execute query...
While the above snippet produces a valid GraphQL query, it has many drawbacks:
- 它使得从整体上理解查询变得更加困难
- 开发人员负责安全地消毒字符串插值
- 如果不将变量的值作为请求参数的一部分发送,则可能会阻止服务器端的缓存
- 它阻止工具静态分析查询(例如: Linter 或类型生成工具)
For this reason, it is recommended to always write queries as static strings:
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,},})
Doing so brings many advantages:
- 易于阅读和维护查询
- GraphQL 服务器处理变量的净化
- 可以在服务器级别缓存变量
- 查询可以通过工具进行静态分析(下面几节将详细介绍)
You might want to include the owner
field only on a particular condition.
For this, you can leverage the @include(if:...)
directive as follows:
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,},})
Note: The opposite directive is @skip(if: ...)
.
GraphQL became famous for its "Ask for what you want" tagline.
For this reason, there is no way, in GraphQL, to get all available fields without having to list them individually.
- 在查询GraphQL API时,请始终考虑只查询实际使用的字段。
- Make sure queries only fetch as many entities as you actually need. By default, queries will fetch 100 entities in a collection, which is usually much more than what will actually be used, e.g., for display to the user. This applies not just to top-level collections in a query, but even more so to nested collections of entities.
For example, in the following query:
query listTokens {tokens {# 获取最多100个tokenidtransactions {# 获取最多100个交易id}}}
The response could contain 100 transactions for each of the 100 tokens.
If the application only needs 10 transactions, the query should explicitly set first: 10
on the transactions field.
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}}
Your application might require querying multiple types of data as follows:
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))
While this implementation is totally valid, it will require two round trips with the GraphQL API.
Fortunately, it is also valid to send multiple queries in the same GraphQL request as follows:
import { execute } from "your-favorite-graphql-client"const query = `query GetTokensandCounters {tokens(first: 50) {idowner}counters {idvalue}}`const { result: { tokens, counters } } = execute(query)
This approach will improve the overall performance by reducing the time spent on the network (saves you a round trip to the API) and will provide a more concise implementation.
A helpful feature to write GraphQL queries is GraphQL Fragment.
Looking at the following query, you will notice that some fields are repeated across multiple Selection-Sets ({ ... }
):
query {bondEvents {idnewDelegate {idactivestatus}oldDelegate {idactivestatus}}}
Such repeated fields (id
, active
, status
) bring many issues:
- More extensive queries become harder to read.
- When using tools that generate TypeScript types based on queries (more on that in the last section),
newDelegate
andoldDelegate
will result in two distinct inline interfaces.
A refactored version of the query would be the following:
query {bondEvents {idnewDelegate {...DelegateItem}oldDelegate {...DelegateItem}}}# 我们在代码转换器定义了一个碎片(子类型)# 将查询中重复的域分解fragment DelegateItem on Transcoder {idactivestatus}
Using GraphQL fragment
will improve readability (especially at scale) and result in better TypeScript types generation.
When using the types generation tool, the above query will generate a proper DelegateItemFragment
type (see last "Tools" section).
A Fragment cannot be based on a non-applicable type, in short, on type not having fields:
fragment MyFragment on BigInt {# ...}
BigInt
is a scalar (native "plain" type) that cannot be used as a fragment's base.
Fragments are defined on specific types and should be used accordingly in queries.
例子:
query {bondEvents {idnewDelegate {...VoteItem # Error! `VoteItem` cannot be spread on `Transcoder` type}oldDelegate {...VoteItem}}}fragment VoteItem on Vote {idvoter}
newDelegate
and oldDelegate
are of type Transcoder
.
It is not possible to spread a fragment of type Vote
here.
GraphQL Fragment
s must be defined based on their usage.
For most use-case, defining one fragment per type (in the case of repeated fields usage or type generation) is sufficient.
Here is a rule of thumb for using fragments:
- When fields of the same type are repeated in a query, group them in a
Fragment
. - When similar but different fields are repeated, create multiple fragments, for instance:
# 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 .
In order to keep up with the mentioned above best practices and syntactic rules, it is highly recommended to use the following workflow and IDE tools.
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
: 给定的变量是否应该保持未使用状态?- 还有更多!
This will allow you to catch errors without even testing queries on the playground or running them in production!
VSCode and GraphQL
The is an excellent addition to your development workflow to get:
- Syntax highlighting
- Autocomplete suggestions
- Validation against schema
- Snippets
- Go to definition for fragments and input types
If you are using graphql-eslint
, the is a must-have to visualize errors and warnings inlined in your code correctly.
WebStorm/Intellij and GraphQL
The will significantly improve your experience while working with GraphQL by providing:
- Syntax highlighting
- Autocomplete suggestions
- Validation against schema
- Snippets
For more information on this topic, check out the which showcases all the plugin's main features.