10 minutes
Schema The Graph QL
Aperçu
Le schéma de votre Subgraph se trouve dans le fichier schema.graphql
. Les schémas GraphQL sont définis à l’aide du langage de définition d’interface GraphQL.
Remarque : si vous n’avez jamais écrit de schéma GraphQL, il est recommandé de consulter ce guide sur le système de types GraphQL. La documentation de référence pour les schémas GraphQL est disponible dans la section API GraphQL.
Définition des entités
Avant de définir des entités, il est important de prendre du recul et de réfléchir à la manière dont vos données sont structurées et liées.
- Toutes les requêtes seront effectuées à partir du modèle de données défini dans le schéma du Subgraph. Par conséquent, la conception du schéma du Subgraph doit être guidée par les requêtes que votre application devra effectuer.
- Il peut être utile d’imaginer les entités comme des “objets contenant des données”, plutôt que comme des événements ou des fonctions.
- Vous définissez les types d’entités dans
schema.graphql
, et Graph Node générera des champs de premier niveau pour interroger des instances uniques et des collections de ce type d’entité. - Chaque type qui doit être une entité doit être annoté avec une directive
@entity
. - Par défaut, les entités sont mutables, ce qui signifie que les mappages peuvent charger des entités existantes, les modifier et stocker une nouvelle version de cette entité.
- La mutabilité a un prix, donc pour les types d’entités qui ne seront jamais modifiés, comme ceux contenant des données extraites textuellement de la blockchain, il est recommandé de les marquer comme immuables avec
@entity(immutable: true)
. - Si des changements se produisent dans le même bloc où l’entité a été créée, alors les mappages peuvent effectuer des changements sur les entités immuables. Les entités immuables sont beaucoup plus rapides à écrire et à interroger, donc elles devraient être utilisées chaque fois que c’est possible.
- La mutabilité a un prix, donc pour les types d’entités qui ne seront jamais modifiés, comme ceux contenant des données extraites textuellement de la blockchain, il est recommandé de les marquer comme immuables avec
Bon exemple
L’entité Gravatar
suivante est structurée autour d’un objet Gravatar et constitue un bon exemple de la manière dont une entité pourrait être définie.
1type Gravatar @entity(immutable: true) {2 id: Bytes!3 owner: Bytes4 displayName: String5 imageUrl: String6 accepted: Boolean7}
Mauvais exemple
Les exemples d’entités GravatarAccepted
et GravatarDeclined
suivants sont basés sur des événements. Il n’est pas recommandé de mapper des événements ou des appels de fonction à des entités 1:1.
1type GravatarAccepted @entity {2 id: Bytes!3 owner: Bytes4 displayName: String5 imageUrl: String6}78type GravatarDeclined @entity {9 id: Bytes!10 owner: Bytes11 displayName: String12 imageUrl: String13}
Champs facultatifs et obligatoires
Les champs d’entité peuvent être définis comme obligatoires ou facultatifs. Les champs obligatoires sont indiqués par le !
dans le schéma. Si le champ est un champ scalaire, vous obtenez une erreur lorsque vous essayez de stocker l’entité. Si le champ fait référence à une autre entité, vous obtenez cette erreur :
1Null value resolved for non-null field 'name'
Chaque entité doit avoir un champ id
, qui doit être de type Bytes!
ou String!
. Il est généralement recommandé d’utiliser Bytes!
, à moins que l’id
ne contienne du texte lisible par l’homme, car les entités avec des identifiants Bytes!
seront plus rapides à écrire et à interroger que celles avec un id
String!
. Le champ id
sert de clé primaire et doit être unique parmi toutes les entités du même type. Pour des raisons historiques, le type ID!
est également accepté et est un synonyme de String!
.
Pour certains types d’entités, l’id
de Bytes!
est construit à partir des id de deux autres entités ; cela est possible en utilisant concat
, par exemple, let id = left.id.concat(right.id)
pour former l’id à partir des id de left
et right
. De même, pour construire un identifiant à partir de l’identifiant d’une entité existante et d’un compteur count
, let id = left.id.concatI32(count)
peut être utilisé. La concaténation est garantie pour produire des identifiants uniques tant que la longueur de left
est la même pour toutes ces entités, par exemple, parce que left.id
est une Address
.
Types scalaires intégrés
Scalaires pris en charge par GraphQL
Les scalaires suivants sont supportés dans l’API GraphQL :
Type | Description |
---|---|
Bytes | Tableau d’octets, représenté sous forme de chaîne hexadécimale. Couramment utilisé pour les hachages et adresses Ethereum. |
String | Scalaire pour les valeurs de type string . Les caractères nuls ne sont pas pris en charge et sont automatiquement supprimés. |
Boolean | Scalaire pour les valeurs de type boolean (booléennes). |
Int | La spécification GraphQL définit Int comme un entier signé de 32 bits. |
Int8 | Un entier signé de 8 octets, également connu sous le nom d’entier signé de 64 bits, peut stocker des valeurs comprises entre -9 223 372 036 854 775 808 et 9 223 372 036 854 775 807. Il est préférable de l’utiliser pour représenter i64 de l’ethereum. |
BigInt | Grands entiers. Utilisé pour les types Ethereum uint32 , int64 , uint64 , …, uint256 . Note : Tout ce qui est inférieur à uint32 , comme int32 , uint24 ou int8 est représenté par i32 . |
BigDecimal | BigDecimal Décimales de haute précision représentées par un significatif et un exposant. L’exposant est compris entre -6143 et +6144. Arrondi à 34 chiffres significatifs. |
Timestamp | Il s’agit d’une valeur i64 en microsecondes. Couramment utilisé pour les champs timestamp des séries chronologiques et des agrégations. |
Enums
Vous pouvez également créer des énumérations dans un schéma. Les énumérations ont la syntaxe suivante :
1enum TokenStatus {2 OriginalOwner3 SecondOwner4 ThirdOwner5}
Une fois que l’enum est défini dans le schéma, vous pouvez utiliser la représentation en chaîne de caractère de la valeur de l’enum pour définir un champ de l’enum sur une entité. Par exemple, vous pouvez fixer le tokenStatus
à SecondOwner
en définissant d’abord votre entité et en fixant ensuite le champ avec entity.tokenStatus = "SecondOwner"
. L’exemple ci-dessous montre à quoi ressemblerait l’entité Token avec un champ enum :
Pour plus de détails sur l’écriture des énumérations, voir la documentation GraphQL.
Relations entre entités
Une entité peut avoir une relation avec une ou plusieurs autres entités de votre schéma. Ces relations pourront être parcourues dans vos requêtes. Les relations dans The Graph sont unidirectionnelles. Il est possible de simuler des relations bidirectionnelles en définissant une relation unidirectionnelle à chaque « extrémité » de la relation.
Les relations sont définies sur les entités comme n’importe quel autre champ sauf que le type spécifié est celui d’une autre entité.
Relations individuelles
Définir un type d’entité Transaction
avec une relation optionnelle de type un-à-un avec un type d’entité TransactionReceipt
:
1type Transaction @entity(immutable: true) {2 id: Bytes!3 transactionReceipt: TransactionReceipt4}56type TransactionReceipt @entity(immutable: true) {7 id: Bytes!8 transaction: Transaction9}
Relations un-à-plusieurs
Définir un type d’entité TokenBalance
avec une relation obligatoire de type un-à-plusieurs avec un type d’entité Token :
1type Token @entity(immutable: true) {2 id: Bytes!3}45type TokenBalance @entity {6 id: Bytes!7 amount: Int!8 token: Token!9}
Recherches inversées
Les recherches inversées peuvent être définies sur une entité à travers le champ @derivedFrom
. Cela crée un champ virtuel sur l’entité qui peut être interrogé mais qui ne peut pas être défini manuellement par l’intermédiaire de l’API des correspondances. Il est plutôt dérivé de la relation définie sur l’autre entité. Pour de telles relations, il est rarement utile de stocker les deux côtés de la relation, et l’indexation et les performances des requêtes seront meilleures si un seul côté est stocké et que l’autre est dérivé.
Pour les relations “un à plusieurs”, la relation doit toujours être stockée du côté “un” et le côté “plusieurs” doit toujours être dérivé. Le stockage de la relation de cette manière, plutôt que le stockage d’un tableau d’entités du côté “plusieurs”, se traduira par des performances nettement meilleures pour l’indexation et l’interrogation du subgraph. En général, le stockage de tableaux d’entités doit être évité autant que possible.
Exemple
Nous pouvons rendre les soldes d’un token accessibles à partir du token en dérivant un champ tokenBalances
:
1type Token @entity(immutable: true) {2 id: Bytes!3 tokenBalances: [TokenBalance!]! @derivedFrom(field: "token")4}56type TokenBalance @entity {7 id: Bytes!8 amount: Int!9 token: Token!10}
Voici un exemple de la façon d’écrire une correspondance pour un Subgraph avec des recherches inversées :
1let token = new Token(event.address) // Create Token2token.save() // tokenBalances est dérivé automatiquement34let tokenBalance = new TokenBalance(event.address)5tokenBalance.amount = BigInt.fromI32(0)6tokenBalance.token = token.id // Référence stockée ici7tokenBalance.save()
Relations plusieurs-à-plusieurs
Pour les relations plusieurs-à-plusieurs, telles que les utilisateurs pouvant appartenir à un nombre quelconque d’organisations, la manière la plus simple, mais généralement pas la plus performante, de modéliser la relation consiste à créer un tableau dans chacune des deux entités impliquées. Si la relation est symétrique, un seul côté de la relation doit être stocké et l’autre côté peut être dérivé.
Exemple
Définir une recherche inversée d’un type d’entité User
vers un type d’entité Organization
. Dans l’exemple ci-dessous, ceci est réalisé en recherchant l’attribut members
à l’intérieur de l’entité Organization
. Dans les requêtes, le champ organizations
de User
sera résolu en trouvant toutes les entités Organization
qui incluent l’ID de l’user.
1type Organization @entity {2 id: Bytes!3 name: String!4 members: [User!]!5}67type User @entity {8 id: Bytes!9 name: String!10 organizations: [Organization!]! @derivedFrom(field: "members")11}
Une façon plus performante de stocker cette relation est d’utiliser une table de correspondance qui a une entrée pour chaque paire User
/ Organization
avec un schéma tel que
1type Organization @entity {2 id: Bytes!3 name: String!4 members: [UserOrganization!]! @derivedFrom(field: "organization")5}67type User @entity {8 id: Bytes!9 name: String!10 organizations: [UserOrganization!] @derivedFrom(field: "user")11}1213type UserOrganization @entity {14 id: Bytes! # Set to `user.id.concat(organization.id)`15 user: User!16 organization: Organization!17}
Cette approche nécessite que les requêtes descendent vers un niveau supplémentaire pour récupérer, par exemple, les organisations des utilisateurs :
1query usersWithOrganizations {2 users {3 organizations {4 # ceci est une entité UserOrganization5 organization {6 name7 }8 }9 }10}
Cette façon plus élaborée de stocker les relations de plusieurs à plusieurs permettra de stocker moins de données pour le Subgraph et, par conséquent, d’obtenir un Subgraph dont l’indexation et l’interrogation sont souvent beaucoup plus rapides.
Ajouter des commentaires au schéma
Conformément à la spécification GraphQL, des commentaires peuvent être ajoutés au-dessus des attributs des entités du schéma en utilisant le symbole dièse #
. Ceci est illustré dans l’exemple ci-dessous :
1type MyFirstEntity @entity {2 # identifiant unique et clé primaire de l'entité3 id: Bytes!4 address: Bytes!5}
Définir les champs de recherche en texte intégral
Les requêtes de recherche en texte intégral filtrent et classent les entités en fonction d’une entrée de recherche de texte. Les requêtes en texte intégral sont capables de renvoyer des correspondances pour des mots similaires en traitant le texte de la requête saisi en radicaux avant de les comparer aux données textuelles indexées.
Une définition de requête en texte intégrale inclut le nom de la requête, le dictionnaire de langue utilisé pour traiter les champs de texte, l’algorithme de classement utilisé pour classer les résultats et les champs inclus dans la recherche. Chaque requête en texte intégral peut s’étendre sur plusieurs champs, mais tous les champs inclus doivent provenir d’un seul type d’entité.
Pour ajouter une requête fulltext, incluez un type _Schema_
avec une directive fulltext dans le schéma GraphQL.
1type _Schema_2 @fulltext(3 name: "bandSearch"4 language: en5 algorithm: rank6 include: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }]7 )89type Band @entity {10 id: Bytes!11 name: String!12 description: String!13 bio: String14 wallet: Address15 labels: [Label!]!16 discography: [Album!]!17 members: [Musician!]!18}
Le champ d’exemple bandSearch
peut être utilisé dans des requêtes pour filtrer les entités Band
sur la base des documents texte dans les champs name
, description
et bio
. Allez à Requêtes - API GraphQL API pour une description de l’API de recherche plein texte et d’autres exemples d’utilisation.
1query {2 bandSearch(text: "breaks & electro & detroit") {3 id4 name5 description6 wallet7 }8}
Gestion des fonctionnalités: A partir de specVersion
0.0.4
, fullTextSearch
doit être déclaré dans la section features
du manifeste Subgraph.
Langues prises en charge
Le choix d’une langue différente aura un effet définitif, bien que parfois subtil, sur l’API de recherche en texte intégral. Les champs couverts par un champ de requête en texte intégral sont examinés dans le contexte de la langue choisie, de sorte que les lexèmes produits par les requêtes d’analyse et de recherche varient d’une langue à l’autre. Par exemple : lorsque vous utilisez le dictionnaire turc pris en charge, “token” est dérivé de “toke”, tandis que, bien sûr, le dictionnaire anglais le dérivera de “token”.
Dictionnaires de langues pris en charge :
Code | Dictionnaire |
---|---|
simple | Général |
da | Danois |
nl | Néerlandais |
en | Anglais |
fi | Finlandais |
fr | Français |
de | Allemand |
hu | Hongrois |
it | Italien |
no | Norvégien |
pt | Portugais |
ro | Roumain |
ru | Russe |
es | Espagnol |
sv | Suédois |
tr | Turc |
Algorithmes de classement
Algorithmes de classement:
Algorithme | Description |
---|---|
rank | Utilisez la qualité de correspondance (0-1) de la requête en texte intégral pour trier les résultats. |
proximitéRang | Similaire au classement, mais inclut également la proximité des correspondances. |