9 minutos
O Schema GraphQL
Visão geral
The schema for your Subgraph is in the file schema.graphql
. GraphQL schemas are defined using the GraphQL interface definition language.
Nota: Se você nunca escreveu um schema em GraphQL, recomendamos que confira este manual sobre o sistema de tipos da GraphQL. Consulte a documentação sobre schemas GraphQL na seção sobre a API da GraphQL.
Como Definir Entidades
Antes de definir as entidades, é importante dar um passo atrás e pensar em como os seus dados são estruturados e ligados.
- All queries will be made against the data model defined in the Subgraph schema. As a result, the design of the Subgraph schema should be informed by the queries that your application will need to perform.
- Pode ser bem útil imaginar entidades como “objetos que contém dados”, e não como eventos ou funções.
- Você define os tipos de entidade em
schema.graphql
, e o Graph Node irá gerar campos de nível superior para queries de instâncias únicas e coleções desse tipo de entidade. - Cada tipo feito para ser uma entidade precisa ser anotado com uma diretiva
@entity
. - Por padrão, as entidades são mutáveis, ou seja: os mapeamentos podem carregar as entidades existentes, modificá-las, e armazenar uma nova versão dessa entidade.
- A mutabilidade tem um preço, então, para tipos de entidade que nunca serão modificados, como as que contêm dados extraídos da chain sem alterações, recomendamos marcá-los como imutáveis com
@entity(immutable: true)
. - Se as alterações acontecerem no mesmo bloco em que a entidade foi criada, então os mapeamentos podem fazer alterações em entidades imutáveis. Entidades imutáveis são muito mais rápidas de escrever e consultar em query, então elas devem ser usadas sempre que possível.
- A mutabilidade tem um preço, então, para tipos de entidade que nunca serão modificados, como as que contêm dados extraídos da chain sem alterações, recomendamos marcá-los como imutáveis com
Bom Exemplo
A entidade Gravatar
abaixo é estruturada em torno de um objeto Gravatar, e é um bom exemplo de como pode ser definida uma entidade.
1type Gravatar @entity(immutable: true) {2 id: Bytes!3 owner: Bytes4 displayName: String5 imageUrl: String6 accepted: Boolean7}
Mau Exemplo
As entidades GravatarAccepted
e GravatarDeclined
abaixo têm base em torno de eventos. Não é recomendado mapear eventos ou chamadas de função a entidades identicamente.
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}
Campos Opcionais e Obrigatórios
Os campos da entidade podem ser definidos como obrigatórios ou opcionais. Os campos obrigatórios são indicados pelo !
no schema. Se o campo for escalar, tentar armazenar a entidade causará um erro. Se o campo fizer referência a outra entidade, você receberá esse erro:
1Null value resolved for non-null field 'name'
Cada entidade deve ter um campo id
, que deve ser do tipo Bytes!
ou String!
. Geralmente é melhor usar Bytes!
— a não ser que o id
tenha texto legível para humanos, já que entidades com as ids Bytes!
são mais fáceis de escrever e consultar que aquelas com um id
String!
. O campo id
serve como a chave primária, e deve ser singular entre todas as entidades do mesmo tipo. Por razões históricas, o tipo ID!
também é aceite, como um sinónimo de String!
.
Para alguns tipos de entidade, o id
é construído das id’s de duas outras entidades; isto é possível com o concat
, por ex., let id = left.id.concat(right.id)
para formar a id a partir das id’s de left
e right
. Da mesma forma, para construir uma id a partir da id de uma entidade existente e um contador count
, pode ser usado o let id = left.id.concatI32(count)
. Isto garante a concatenação a produzir id’s únicas enquanto o comprimento do left
for o mesmo para todas as tais entidades; por exemplo, porque o left.id
é um Address
(endereço).
Tipos Embutidos de Escalar
Escalares Apoiados pelo GraphQL
As seguintes escalas são apoiadas na API da GraphQL:
Tipo | Descrição |
---|---|
Bytes | Arranjo de bytes, representado como string hexadecimal. Usado frequentemente por hashes e endereços no Ethereum. |
String | Escalar para valores string. Caracteres nulos serão removidos automaticamente. |
Boolean | Escalar para valores boolean . |
Int | A especificação da GraphQL define Int como um inteiro assinado de 32 bits. |
Int8 | Um número inteiro assinado em 8 bits, também conhecido como um número inteiro assinado em 64 bits, pode armazenar valores de -9,223,372,036,854,775,808 a 9,223,372,036,854,775,807. É melhor usar isto para representar o i64 do ethereum. |
BigInt | Números inteiros grandes. Usados para os tipos uint32 , int64 , uint64 , …, uint256 do Ethereum. Nota: Tudo abaixo de uint32 , como int32 , uint24 ou int8 é representado como i32 . |
BigDecimal | Decimais de alta precisão BigDecimal representados como um significando e um exponente. O alcance de exponentes é de -6143 até +6144. Arredondado para 34 dígitos significantes. |
Timestamp | É um valor i64 em microssegundos. Usado frequentemente para campos timestamp para séries temporais e agregações. |
Enums
Também pode criar enums dentro de um schema. Enums têm a seguinte sintaxe:
1enum TokenStatus {2 OriginalOwner3 SecondOwner4 ThirdOwner5}
Quando o enum for definido no schema, podes usar a representação do string do valor enum para determinar um campo enum numa entidade. Por exemplo, podes implantar o tokenStatus
no SecondOwner
ao definir primeiro a sua entidade e depois determinar o campo com entity.tokenStatus = "SecondOwner"
. O exemplo abaixo demonstra como ficaria a entidade do Token com um campo enum:
Para saber mais sobre como escrever enums, veja a documentação do GraphQL.
Relacionamentos de Entidades
Uma entidade pode ter relacionamentos com uma ou mais entidades no seu schema; estes podem ser tratados nas suas consultas. Os relacionamentos no The Graph são unidirecionais, e é possível simular relacionamentos bidirecionais ao definir um relacionamento unidirecional em cada “lado” do relacionamento projetado.
Relacionamentos são definidos em entidades como qualquer outro campo, sendo que o tipo especificado é o de outra entidade.
Relacionamentos Um-com-Um
Defina um tipo de entidade Transaction
com um relacionamento um-com-um opcional, com um tipo de entidade TransactionReceipt
:
1type Transaction @entity(immutable: true) {2 id: Bytes!3 transactionReceipt: TransactionReceipt4}56type TransactionReceipt @entity(immutable: true) {7 id: Bytes!8 transaction: Transaction9}
Relacionamentos Um-com-Vários
Defina um tipo de entidade TokenBalance
com um relacionamento um-com-vários, exigido com um tipo de entidade Token
:
1type Token @entity(immutable: true) {2 id: Bytes!3}45type TokenBalance @entity {6 id: Bytes!7 amount: Int!8 token: Token!9}
Buscas Reversas
Buscas reversas podem ser definidas numa entidade pelo campo @derivedFrom
. Isto cria um campo virtual na entidade, que pode ser consultado, mas não pode ser configurado manualmente pela API de mapeamentos. Em vez disto, ele é derivado do relacionamento definido na outra entidade. Para tais relacionamentos, faz raramente sentido armazenar ambos os lados do relacionamento, e tanto o indexing quanto o desempenho dos queries melhorarão quando apenas um lado for armazenado, e o outro derivado.
For one-to-many relationships, the relationship should always be stored on the ‘one’ side, and the ‘many’ side should always be derived. Storing the relationship this way, rather than storing an array of entities on the ‘many’ side, will result in dramatically better performance for both indexing and querying the Subgraph. In general, storing arrays of entities should be avoided as much as is practical.
Exemplo
Podemos fazer os saldos para um token acessíveis a partir do mesmo token ao derivar um campo 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}
Here is an example of how to write a mapping for a Subgraph with reverse lookups:
1let token = new Token(event.address) // Crie o Token2token.save() // tokenBalances é derivado automaticamente34let tokenBalance = new TokenBalance(event.address)5tokenBalance.amount = BigInt.fromI32(0)6tokenBalance.token = token.id // Referência armazenada aqui7tokenBalance.save()
Relacionamentos Vários-com-Vários
Para relacionamentos vários-com-vários, como um conjunto de utilizadores em que cada um pertence a qualquer número de organizações, o relacionamento é mais simplesmente — mas não mais eficientemente — modelado como um arranjo em cada uma das duas entidades envolvidas. Se o relacionamento for simétrico, apenas um lado do relacionamento precisa ser armazenado, e o outro lado pode ser derivado.
Exemplo
Defina uma busca reversa a partir de um tipo de entidade User
para um tipo de entidade Organization
. No exemplo abaixo, isto é feito ao buscar pelo atributo members
a partir de dentro da entidade Organization
. Em queries, o campo organizations
no User
será resolvido ao encontrar todas as entidades Organization
que incluem a ID do utilizador.
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}
Uma maneira mais eficiente para armazenar este relacionamento é com uma mesa de mapeamento que tem uma entrada para cada par de User
/ Organization
, com um schema como:
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}
Esta abordagem requer que os queries desçam a um nível adicional para retirar, por exemplo, as organizações para utilizadores:
1query usersWithOrganizations {2 users {3 organizations {4 # isto é uma entidade UserOrganization5 organization {6 name7 }8 }9 }10}
This more elaborate way of storing many-to-many relationships will result in less data stored for the Subgraph, and therefore to a Subgraph that is often dramatically faster to index and to query.
Como adicionar comentários ao schema
Pela especificação do GraphQL, é possível adicionar comentários acima de atributos de entidade do schema com o símbolo de hash #
. Isto é ilustrado no exemplo abaixo:
1type MyFirstEntity @entity {2 # identificador único e chave primária da entidade3 id: Bytes!4 address: Bytes!5}
Como Definir Campos de Busca Fulltext
Buscas fulltext filtram e ordenam entidades baseadas num texto inserido. Queries fulltext podem retornar resultados para palavras semelhantes ao processar o texto inserido antes de compará-los aos dados do texto indexado.
Uma definição de query fulltext inclui: o nome do query, o dicionário do idioma usado para processar os campos de texto, o algoritmo de ordem usado para ordenar os resultados, e os campos incluídos na busca. Todo query fulltext pode ter vários campos, mas todos os campos incluídos devem ser de um único tipo de entidade.
Para adicionar um query fulltext, inclua um tipo _Schema_
com uma diretiva fulltext no schema em 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}
O exemplo bandSearch
serve, em queries, para filtrar entidades Band
baseadas nos documentos de texto nos campos name
, description
e bio
. Confira a página API GraphQL - Consultas para uma descrição da API de busca fulltext e mais exemplos de uso.
1query {2 bandSearch(text: "breaks & electro & detroit") {3 id4 name5 description6 wallet7 }8}
Feature Management: From specVersion
0.0.4
and onwards, fullTextSearch
must be declared under the features
section in the Subgraph manifest.
Idiomas apoiados
Escolher um idioma diferente terá um efeito definitivo, porém às vezes sutil, na API da busca fulltext. Campos cobertos por um campo de query fulltext serão examinados no contexto do idioma escolhido, para que os lexemas produzidos pela análise e pelos queries de busca variem de idioma para idioma. Por exemplo: ao usar o dicionário turco, “token” é abreviado para “toke” enquanto, claro, o dicionário em inglês o categorizará como “token”.
Dicionários apoiados:
Código | Dicionário |
---|---|
simple | Geral |
da | Dinamarquês |
nl | Neerlandês |
en | Inglês |
fi | Finlandês |
fr | Francês |
de | Alemão |
hu | Húngaro |
it | Italiano |
no | Norueguês |
pt | Português |
ro | Romeno |
ru | Russo |
es | Espanhol |
sv | Sueco |
tr | Turco |
Algoritmos de Ordem
Algoritmos apoiados para a organização de resultados:
Algoritmo | Descrição |
---|---|
rank | Organiza os resultados pela qualidade da correspondência (0-1) da busca fulltext. |
proximityRank | Similar ao rank, mas também inclui a proximidade das combinações. |