Querying Best Practices
Reading time: 10 min
The Graph fornisce un modo decentralizzato per effettuare query dei dati delle blockchain.
I dati di The Graph network sono esposti attraverso un API GraphQL, che facilita query dei dati con il linguaggio GraphQL.
Questa pagina vi guiderà attraverso le regole essenziali del linguaggio GraphQL e le best practice delle query in GraphQL.
A differenza del REST API, un GraphQL API si basa su uno schema che definisce le query che possono essere eseguite.
Ad esempio, una query per ottenere un token utilizzando la query token
avrà il seguente aspetto:
query GetToken($id: ID!) {token(id: $id) {idowner}}
che restituirà la seguente risposta JSON prevedibile (quando si passa il valore corretto della variabile $id
):
{"token": {"id": "...","owner": "..."}}
Le query GraphQL utilizzano il linguaggio GraphQL, definito da .
La query GetToken
di cui sopra è composta da più parti linguistiche (sostituite di seguito con i segnaposto [...]
):
query [operationName]([variableName]: [variableType]) {[queryName]([argumentName]: [variableName]) {# "{ ... }" express a Selection-Set, we are querying fields from `queryName`.[field][field]}}
Sebbene l'elenco delle cose da fare e da non fare a livello sintattico sia lungo, ecco le regole essenziali da tenere a mente quando si tratta di scrivere query GraphQL:
- Ogni
queryName
deve essere usato una sola volta per ogni operazione. - Ogni
field
deve essere utilizzato una sola volta in una selezione (non si può effettuare query diid
due volte sottotoken
) - 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 . - Qualsiasi variabile assegnata a un argomento deve corrispondere al suo tipo.
- In un determinato elenco di variabili, ciascuna di esse deve essere unica.
- Tutte le variabili definite devono essere utilizzate.
Se non si rispettano le regole di cui sopra, si ottiene un errore dall'API Graph.
For a complete list of rules with code examples, please look at our .
GraphQL è un linguaggio e una serie di convenzioni che si trasportano su HTTP.
Significa che è possibile effettuare query di un API GraphQL utilizzando lo standard fetch
(in modo nativo o tramite @whatwg-node/fetch
or isomorphic-fetch
).
Tuttavia, come indicato in , si consiglia di utilizzare il nostro graph-client
che supporta caratteristiche uniche come:
- Gestione dei subgraph a cross-chain: effettuare query di più subgraph in un'unica query
- Risultato completamente tipizzato
Ecco come effettuare query di The Graph con 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()
Altre alternative di client GraphQL sono trattate in .
Ora che abbiamo trattato le regole di base della sintassi delle query GraphQL, esaminiamo le best practices di scrittura delle query GraphQL.
Una pratica comune (sbagliata) è quella di costruire dinamicamente le stringhe di query come segue:
const id = params.idconst fields = ['id', 'owner']const query = `query GetToken {token(id: ${id}) {${fields.join('\n')}}}`// Execute query...
Sebbene lo snippet di cui sopra produca una GraphQL query valida, ha molti svantaggi:
- rende più difficile la comprensione della query nel suo insieme
- gli sviluppatori sono responsabili della sanificazione sicura dell'interpolazione delle stringhe
- non inviare i valori delle variabili come parte dei parametri della richiesta previene eventuali cache sul lato server
- impedisce agli strumenti di analizzare staticamente la query (ad esempio, Linter o strumenti di generazione dei tipi)
Per questo motivo, si consiglia di scrivere sempre le query come stringhe statiche:
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,},})
Questo comporta molti vantaggi:
- Query facili da leggere e da mantenere
- Il server GraphQL gestisce la sanificazione delle variabili
- Le variabili possono essere messe in cache a livello di server
- Le query possono essere analizzate staticamente dagli strumenti (maggiori informazioni nelle sezioni successive)
Nota: come includere i campi in modo condizionato nelle query statiche
Si potrebbe voler includere il campo owner
solo in una condizione particolare.
Per questo possiamo sfruttare la direttiva @include(if:...)
come segue:
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,},})
Nota: La direttiva opposta è @skip(if: ...)
.
GraphQL è diventato famoso per il suo slogan "Chiedi quello che vuoi".
Per questo motivo, non c'è modo, in GraphQL, di ottenere tutti i campi disponibili senza doverli elencare singolarmente.
Quando si interrogano le GraphQL API, si deve sempre pensare di effettuare query di solo i campi che verranno effettivamente utilizzati.
Una causa comune di over-fetching sono le collezioni di entità. Per impostazione predefinita, le query recuperano 100 entità in un collezione, che di solito sono molte di più di quelle effettivamente utilizzate, ad esempio per la visualizzazione all'utente. Le query dovrebbero quindi essere impostate quasi sempre in modo esplicito e assicurarsi di recuperare solo il numero di entità di cui hanno effettivamente bisogno. Questo vale non solo per le collezioni di primo livello in una query, ma ancora di più per le collezioni di entità annidate.
Ad esempio, nella seguente query:
query listTokens {tokens {# will fetch up to 100 tokensidtransactions {# will fetch up to 100 transactionsid}}}
La risposta potrebbe contenere 100 transazioni per ciascuno dei 100 token.
Se l'applicazione ha bisogno solo di 10 transazioni, la query deve impostare esplicitamente first: 10
sul campo transazioni.
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}}
L'applicazione potrebbe richiedere di effettuare query di più tipi di dati, come di seguito indicato:
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))
Sebbene questa implementazione sia assolutamente valida, richiederà due viaggi di andata e ritorno con API GraphQL.
Fortunatamente, è anche possibile inviare più query nella stessa richiesta GraphQL, come segue:
import { execute } from "your-favorite-graphql-client"const query = `query GetTokensandCounters {tokens(first: 50) {idowner}counters {idvalue}}`const { result: { tokens, counters } } = execute(query)
Questo approccio migliorerà le prestazioni complessive riducendo il tempo trascorso sulla rete (si risparmia un viaggio di andata e ritorno verso l'API) e fornirà un'implementazione più concisa.
Una funzione utile per scrivere GraphQL query è GraphQL Fragment.
Osservando la seguente query, si noterà che alcuni campi sono ripetuti su Selection-Sets multipli ({ ... }
):
query {bondEvents {idnewDelegate {idactivestatus}oldDelegate {idactivestatus}}}
Tali campi ripetuti (id
, active
, status
) comportano molti problemi:
- più difficile da leggere per le query più estese
- quando si usano strumenti che generano tipi TypeScript basati su query (per saperne di più nell'ultima sezione),
newDelegate
eoldDelegate
risulteranno in due interfacce inline distinte.
Una versione riadattata della query sarebbe la seguente:
query {bondEvents {idnewDelegate {...DelegateItem}oldDelegate {...DelegateItem}}}# we define a fragment (subtype) on Transcoder# to factorize repeated fields in the queryfragment DelegateItem on Transcoder {idactivestatus}
L'uso di GraphQL fragment
migliorerà la leggibilità (soprattutto in scala), ma anche la generazione di tipi TypeScript.
Quando si usa lo strumento di generazione dei tipi, la query di cui sopra genererà un tipo DelegateItemFragment
corretto (vedi l'ultima sezione "Strumenti").
La base del frammento deve essere un tipo
Un frammento non può essere basato su un tipo non applicabile, in breve, su un tipo che non ha campi:
fragment MyFragment on BigInt {# ...}
BigInt
è un scalare (tipo nativo "semplice") che non può essere usato come base di un frammento.
Come diffondere un frammento
I frammenti sono definiti su tipi specifici e devono essere usati di conseguenza nelle query.
Esempio:
query {bondEvents {idnewDelegate {...VoteItem # Error! `VoteItem` cannot be spread on `Transcoder` type}oldDelegate {...VoteItem}}}fragment VoteItem on Vote {idvoter}
newDelegate
e oldDelegate
sono di tipo Transcoder
.
Non è possibile diffondere un frammento di tipo Vote
qui.
Definire il frammento come unità aziendale atomica di dati
I Fragment GraphQL devono essere definiti in base al loro utilizzo.
Per la maggior parte dei casi d'uso, è sufficiente definire un fragment per tipo (nel caso di utilizzo di campi ripetuti o di generazione di tipi).
Ecco una regola empirica per l'utilizzo di Fragment:
- quando i campi dello stesso tipo si ripetono in una query, raggrupparli in un Fragment
- quando si ripetono campi simili ma non uguali, creare più fragment, ad esempio:
# 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}}
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 .
Per tenere il passo con le best practice e le regole sintattiche di cui sopra, si consiglia vivamente di utilizzare i seguenti strumenti di workflow e 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
: un campo è utilizzato su un tipo corretto?@graphql-eslint/no-unused variables
: una determinata variabile deve rimanere inutilizzata?- e altro ancora!
Questo vi permetterà di cogliere gli errori senza nemmeno testare le query sul playground o eseguirle in produzione!
VSCode e GraphQL
è un'eccellente aggiunta al vostro flusso di lavoro di sviluppo:
- evidenziazione della sintassi
- suggerimenti per il completamento automatico
- validazione rispetto allo schema
- frammenti
- vai alla definizione dei frammenti e dei tipi dell'input
Se si utilizza graphql-eslint
, è indispensabile per visualizzare correttamente gli errori e gli avvertimenti inseriti nel codice.
WebStorm/Intellij e GraphQL
migliorerà significativamente l'esperienza di lavoro con GraphQL fornendo:
- evidenziazione della sintassi
- suggerimenti per il completamento automatico
- validazione rispetto allo schema
- frammenti
Maggiori informazioni in questo che illustra tutte le caratteristiche principali del plugin.