クエリのベストプラクティス
Reading time: 16 min
The Graphは、ブロックチェーンのデータをクエリするための分散化された方法を提供します。
The GraphのネットワークのデータはGraphQL APIで公開され、GraphQL言語によるデータクエリーが容易になります。
このページでは、GraphQLの言語ルールとGraphQLクエリのベストプラクティスに必要不可欠な情報をご案内しています。
REST APIとは異なり、GraphQL APIは実行可能なクエリを定義するSchemaをベースに構築されています。
例えば、token
クエリを使ってトークンを取得するクエリは次のようになります。
query GetToken($id: ID!) {token(id: $id) {idowner}}
以下のような予測可能なJSONレスポンスが返ってきます(適切な$id
変数値を渡す場合)。
{"token": {"id": "...","owner": "..."}}
GraphQLクエリは、仕様で定義されているGraphQL言語を使用します。
上記の GetToken
クエリは、複数の言語部分で構成されています (以下では [...]
プレースホルダーに置き換えられています)。
query [operationName]([variableName]: [variableType]) {[queryName]([argumentName]: [variableName]) {# "{ ... }" express a Selection-Set, we are querying fields from `queryName`.[field][field]}}
構文の「やるべきこと」「やってはいけないこと」を挙げればきりがありませんが、ここではGraphQLクエリを書く際に覚えておきたい基本的なルールを紹介します:
- 各
queryName
は、1回の操作で1回だけ使用しなければなりません。 - 各
フィールド
は、選択の中で一度だけ使用しなければなりません(トークン
の下にid
を二度照会することはできません)。 - いくつかの
field
やクエリ (tokens
など) は、サブフィールドの選択を必要とする複雑な型を返します。期待されたときに選択を提供しない(あるいは期待されていないときに提供する-たとえばid
の場合)ことは、エラーを発生させます。フィールドタイプを知るには、The Graph Explorerを参照してください。 - 引数に代入される変数は、その型と一致しなければなりません。
- 与えられた変数のリストにおいて、各変数は一意でなければなりません。
- 定義された変数はすべて使用する必要があります。
上記のルールに従わない場合、Graph APIからエラーが発生します。
ルールの完全なリストとコード例については、GraphQL Validationsガイドをご覧ください。
GraphQLは、HTTPを介して転送される言語と一連の規約です。
これは、標準のfetch
(ネイティブであれば、@whatwg-node/fetch
やisomorphic-fetch
を介しても)を使用して、GraphQL APIにクエリを送信できることを意味します。
ただし、「アプリケーションからのクエリ」で述べたように、以下のような固有の機能をサポートするgraph-client
を使用することをおすすめします。
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()
その他の GraphQL クライアントの代替手段については、「アプリケーションからのクエリ」 で説明します。
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個取得しますが、通常、実際に使用される量(たとえば、ユーザーに表示される量)よりもはるかに多いです。そのため、クエリはほぼ常にfirst
を明示的に設定し、実際に必要なだけのエンティティを取得するようにする必要があります。これは、クエリ内のトップレベルのコレクションだけでなく、さらにエンティティのネストされたコレクションにも当てはまります。
たとえば、次のクエリでは:
query listTokens {tokens {# will fetch up to 100 tokensidtransactions {# will fetch up to 100 transactionsid}}}
応答には、100 個のトークンごとに 100 個のトランザクションが含まれる可能性があります。
アプリケーションが 10 トランザクションのみを必要とする場合、クエリではトランザクション フィールドに first: 10
を明示的に設定する必要があります。
複数のクエリを組み合わせる
アプリケーションでは、次のように複数の種類のデータをクエリする必要がある場合があります:
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 を使用した 2 つの往復が必要になります。
幸いなことに、次のように同じ GraphQL リクエストで複数のクエリを送信することも有効です:
import { execute } from "your-favorite-graphql-client"const query = `query GetTokensandCounters {tokens(first: 50) {idowner}counters {idvalue}}`
このアプローチは、ネットワークに費やす時間を減少させる(APIへの往復を省略する)ため、全体的なパフォーマンスを向上させます。また、より簡潔な実装を提供します。
GraphQL クエリを作成するのに役立つ機能は、GraphQL Fragment です。
次のクエリを見ると、いくつかのフィールドが複数のSelection-Sets({ ... }
)で繰り返されていることがわかります:
query {bondEvents {idnewDelegate {idactivestatus}oldDelegate {idactivestatus}}}
このような繰り返しフィールド (id
、active
、status
) は、多くの問題を引き起こします。
- より広範囲なクエリに対応するために読みにくくなります
- クエリに基づいて TypeScript 型を生成するツールを使用する場合 (前のセクションで詳しく説明します)、
newDelegate
およびoldDelegate
は、2 つの異なるインライン インターフェイスになります。
クエリのリファクタリングされたバージョンは次のようになります:
query {bondEvents {idnewDelegate {...DelegateItem}oldDelegate {...DelegateItem}}}# we define a fragment (subtype) on Transcoder# to factorize repeated fields in the queryfragment DelegateItem on Transcoder {idactivestatus}
GraphQLのfragment
を使用すると、可読性が向上します(特に大規模な場合)し、さらにはより良い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 フラグメントは、その使用法に基づいて定義する必要があります。
ほとんどのユースケースでは、1つのタイプに対して1つのフラグメントを定義すること(繰り返しフィールドの使用または型生成の場合)で十分です。
Fragment を使用する場合の経験則は次のとおりです:
- 同じ型のフィールドがクエリ内で繰り返される場合、それらをFragmentでグループ化します。
- 同じフィールドが繰り返される場合、複数のフラグメントを作成します。
# base fragment (mostly used in listing)fragment Voter on Vote {idvoter}# extended fragment (when querying a detailed view of a vote)fragment VoteWithPoll on Vote {idvoterchoiceIDpoll {idproposal}}
クエリをアプリケーション内で実行して繰り返しテストするのは手間がかかる場合があります。そのため、クエリをアプリケーションに追加する前に、The Graph Explorerを使用してクエリをテストすることを躊躇しないでください。The Graph Explorerは、クエリをテストするための事前に設定されたGraphQLプレイグラウンドを提供します。
クエリをデバッグやテストするより柔軟な方法を探している場合、AltairやGraphiQLなどの類似のWebベースのツールも利用できます。
上記で述べたベストプラクティスと構文ルールに従うためには、以下のワークフローとIDEツールを使用することを強くお勧めします。
GraphQL ESLint
GraphQL ESLint を使用すると、手間をかけずに GraphQL のベスト プラクティスを常に把握できるようになります。
「operations-recommended」 構成をセットアップすると、次のような重要なルールが適用されます。
@graphql-eslint/fields-on-correct-type
: フィールドは適切なタイプで使用されているか?@graphql-eslint/no-unused variables
: 与えられた変数は未使用のままであるべきか?- ともっと
これにより、 プレイグラウンドでクエリをテストしたり、本番環境で実行したりせずにエラーをキャッチできる ようになります。
VSCodeとGraphQL
The GraphQL VSCode Extension is a great addition to your development workflow, allowing you to:
- 構文の強調表示
- オートコンプリートの提案
- スキーマに対する検証
- snippets
- フラグメントと入力タイプの定義に移動
graphql-eslint
を使用している場合、ESLint VSCode拡張機能はエラーや警告を正しくコード内に表示するために必須です。
WebStorm/Intellij および GraphQL
JS GraphQLプラグインは、以下を提供することで、GraphQLを使用する際のエクスペリエンスを大幅に向上させます。
- 構文の強調表示
- オートコンプリートの提案
- スキーマに対する検証
- snippets
詳細は、このWebStorm の記事で、プラグインの主な機能をすべて紹介しています。