8 минуты
Схема GraphQL
Обзор
Схема для вашего субграфа находится в файле schema.graphql
. Схемы GraphQL определяются с использованием языка определения интерфейса GraphQL.
Примечание: Если вы никогда не писали схему GraphQL, рекомендуется ознакомиться с этим введением в систему типов GraphQL. Справочную документацию по схемам GraphQL можно найти в разделе GraphQL API.
Определение Объектов
Прежде чем определять объекты, важно сделать шаг назад и задуматься над тем, как структурированы и связаны Ваши данные.
- Все запросы будут выполняться против модели данных, определенной в схеме субграфа. Поэтому проектирование схемы субграфа должно основываться на запросах, которые ваше приложение будет выполнять.
- Может быть полезно представить объекты как «объекты, содержащие данные», а не как события или функции.
- Вы определяете типы объектов в файле
schema.graphql
, и Graph Node будет генерировать поля верхнего уровня для запроса отдельных экземпляров и коллекций этих типов объектов. - Каждый тип, который должен быть объектом, должен быть аннотирован директивой
@entity
. - По умолчанию объекты изменяемы, то есть мэппинги могут загружать существующие объекты, изменять и сохранять их новую версию.
- Изменяемость имеет свою цену, поэтому для типов объектов, которые никогда не будут изменяться, например, содержащих данные, извлеченные дословно из чейна, рекомендуется пометить их как неизменяемые с помощью
@entity(immutable: true)
. - Если изменения происходят в том же блоке, в котором был создан объект, то мэппинги могут вносить изменения в неизменяемые объекты. Неизменяемые объекты гораздо быстрее записываются и запрашиваются, поэтому их следует использовать, когда это возможно.
- Изменяемость имеет свою цену, поэтому для типов объектов, которые никогда не будут изменяться, например, содержащих данные, извлеченные дословно из чейна, рекомендуется пометить их как неизменяемые с помощью
Удачный пример
Следующий объект Gravatar
структурирован вокруг объекта Gravatar и является хорошим примером того, как можно определить объект.
1type Gravatar @entity(immutable: true) {2 id: Bytes!3 owner: Bytes4 displayName: String5 imageUrl: String6 accepted: Boolean7}
Неудачный пример
Следующий пример объектов GravatarAccepted
и GravatarDeclined
основан на событиях. Не рекомендуется сопоставлять события или вызовы функций 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}
Дополнительные и обязательные поля
Поля объектов могут быть определены как обязательные или необязательные. Обязательные поля указываются с помощью !
в схеме. Если поле является скалярным, вы получите ошибку при попытке сохранить объект. Если поле ссылается на другой объект, то вы получите следующую ошибку:
1Null value resolved for non-null field 'name'
Каждый объект должен иметь поле id
, которое должно быть типа Bytes!
или String!
. Обычно рекомендуется использовать Bytes!
, если только id
не содержит текст, читаемый человеком, поскольку объекты с id
типа Bytes!
будут быстрее записываться и запрашиваться, чем те, у которых id
типа String!
. Поле id
служит основным ключом и должно быть уникальным среди всех объектов одного типа. По историческим причинам также принимается тип ID!
, который является синонимом String!
.
Для некоторых типов объектов id
для Bytes!
формируется из id
двух других объектов. Это возможно с использованием функции concat
, например, let id = left.id.concat(right.id)
, чтобы сформировать id
из id
объектов left
и right
. Аналогично, чтобы сформировать id
из id
существующего объекта и счетчика count
, можно использовать let id = left.id.concatI32(count)
. Конкатенация гарантирует, что id
будет уникальным, если длина left.id
одинаковая для всех таких объектов, например, если left.id
— это Address
.
Встроенные скалярные типы
Поддерживаемые GraphQL скаляры
В API GraphQL поддерживаются следующие скаляры:
Тип | Описание |
---|---|
Bytes | Массив байтов, представленный в виде шестнадцатеричной строки. Обычно используется для хэшей и адресов Ethereum. |
String | Скаляр для значений типа string . Нулевые символы не поддерживаются и автоматически удаляются. |
Boolean | Скаляр для значений boolean . |
Int | Спецификация GraphQL определяет тип Int как знаковое 32-битное целое число. |
Int8 | 8-байтовое целое число со знаком, также известное как 64-битное целое число со знаком, может хранить значения в диапазоне от -9,223,372,036,854,775,808 до 9,223,372,036,854,775,807. Рекомендуется использовать его для представления типа i64 из ethereum. |
BigInt | Большие целые числа. Используются для типов uint32 , int64 , uint64 , …, uint256 из Ethereum. Примечание: все типы, меньше чем uint32 , такие как int32 , uint24 или int8 , представлены как i32 . |
BigDecimal | BigDecimal Высокоточные десятичные числа, представленные как мантисса и экспонента. Диапазон экспоненты от −6143 до +6144. Округляется до 34 значащих цифр. |
Timestamp | Это значение типа i64 в микросекундах. Обычно используется для полей timestamp в временных рядах и агрегациях. |
Перечисления
Вы также можете создавать перечисления внутри схемы. Перечисления имеют следующий синтаксис:
1enum TokenStatus {2 OriginalOwner3 SecondOwner4 ThirdOwner5}
Как только перечисление определено в схеме, вы можете использовать строковое представление значения перечисления для установки поля перечисления в объекте. Например, вы можете установить tokenStatus
в значение SecondOwner
, сначала определив ваш объект, а затем установив поле с помощью entity.tokenStatus = "SecondOwner"
. Пример ниже демонстрирует, как будет выглядеть объект Token с полем перечисления:
Более подробную информацию о написании перечислений можно найти в документации по GraphQL.
Связи объектов
Объект может иметь связь с одним или несколькими другими объектами в Вашей схеме. Эти связи могут быть использованы в Ваших запросах. Связи в The Graph являются однонаправленными. Можно смоделировать двунаправленные связи, определив однонаправленную связь на любом “конце” связи.
Связи определяются для объектов точно так же, как и для любого другого поля, за исключением того, что в качестве типа указывается тип другого объекта.
Связи “Один к одному”
Определите тип объекта Transaction
с необязательной связью “один к одному” с типом объекта TransactionReceipt
:
1type Transaction @entity(immutable: true) {2 id: Bytes!3 transactionReceipt: TransactionReceipt4}56type TransactionReceipt @entity(immutable: true) {7 id: Bytes!8 transaction: Transaction9}
Связи “Один ко многим”
Определите тип объекта TokenBalance
с обязательной связью “один ко многим” с типом объекта Token
:
1type Token @entity(immutable: true) {2 id: Bytes!3}45type TokenBalance @entity {6 id: Bytes!7 amount: Int!8 token: Token!9}
Обратные запросы
Обратные поисковые запросы можно определить в объекте с помощью поля @derivedFrom
. Это создает виртуальное поле в объекте, которое может быть запрашиваемо, но не может быть установлено вручную через API отображений. Вместо этого оно вычисляется на основе связи, определенной в другом объекте. Для таких отношений редко имеет смысл хранить обе стороны связи, и как производительность индексирования, так и производительность запросов будут лучше, если хранится только одна сторона связи, а другая извлекается.
Для отношений «один ко многим» отношение всегда должно храниться на стороне «один», а сторона «многие» должна быть выведена. Хранение отношений таким образом, а не хранение массива объектов на стороне «многие», приведет к значительному улучшению производительности как при индексировании, так и при запросах к субграфу. В общем, хранение массивов объектов следует избегать, насколько это возможно на практике.
Пример
Мы можем сделать балансы для токена доступными из токена, создав поле 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}
Вот пример того, как написать мэппинг для субграфа с обратными поисковыми запросами:
1let token = new Token(event.address) // Создание токена2token.save() // tokenBalances определяется автоматически34let tokenBalance = new TokenBalance(event.address)5tokenBalance.amount = BigInt.fromI32(0)6tokenBalance.token = token.id // Ссылка на токен сохраняется здесь7tokenBalance.save()
Связи “Многие ко многим”
Для связей “многие ко многим”, таких, например, как пользователи, каждый из которых может принадлежать к любому числу организаций, наиболее простым, но, как правило, не самым производительным способом моделирования связей является создание массива в каждом из двух задействованных объектов. Если связь симметрична, то необходимо сохранить только одну сторону связи, а другая сторона может быть выведена.
Пример
Определите обратный поиск от объекта User
к объекту Organization
. В примере ниже это достигается через поиск атрибута members
внутри объекта Organization
. В запросах поле organizations
на объекте User
будет разрешаться путем поиска всех объектов Organization
, которые включают идентификатор пользователя.
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}
Более эффективный способ хранения этих отношений — это использование таблицы отображений, которая содержит одну запись для каждой пары User
/ Organization
с такой схемой
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}
Этот подход требует, чтобы запросы опускались на один дополнительный уровень для получения, например, сведений об организациях для пользователей:
1query usersWithOrganizations {2 users {3 organizations {4 # this is a UserOrganization entity5 organization {6 name7 }8 }9 }10}
Этот более сложный способ хранения отношений многие ко многим приведет к меньшему объему данных, хранимых для субграфа, что, в свою очередь, сделает субграф значительно быстрее как при индексировании, так и при запросах.
Добавление комментариев к схеме
Согласно спецификации GraphQL, комментарии могут быть добавлены над атрибутами объектов схемы с использованием символа решетки #
. Это показано в следующем примере:
1type MyFirstEntity @entity {2 #уникальный идентификатор и первичный ключ объекта3 id: Bytes!4 address: Bytes!5}
Определение полей полнотекстового поиска
Полнотекстовые поисковые запросы фильтруют и ранжируют объекты на основе введенных данных текстового запроса. Полнотекстовые запросы способны возвращать совпадения по схожим словам путем обработки текста запроса в виде строк перед сравнением с индексированными текстовыми данными.
Определение полнотекстового запроса включает в себя название запроса, словарь языка, используемый для обработки текстовых полей, алгоритм ранжирования, используемый для упорядочивания результатов, и поля, включенные в поиск. Каждый полнотекстовый запрос может охватывать несколько полей, но все включенные поля должны относиться к одному типу объекта.
Чтобы добавить полнотекстовый запрос, включите тип _Schema_
с директивой fulltext
в схему 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}
Пример поля bandSearch
может быть использован в запросах для фильтрации объектов Band
на основе текстовых документов в полях name
, description
и bio
. Перейдите к GraphQL API - Запросы для описания API полнотекстового поиска и других примеров использования.
1query {2 bandSearch(text: "breaks & electro & detroit") {3 id4 name5 description6 wallet7 }8}
Управление функциями: Начиная с specVersion
0.0.4
и далее, fullTextSearch
должен быть объявлен в разделе features
манифеста субграфа.
Поддерживаемые языки
Выбор другого языка окажет решающее, хотя иногда и неуловимое влияние на API полнотекстового поиска. Поля, охватываемые полем полнотекстового запроса, рассматриваются в контексте выбранного языка, поэтому лексемы, полученные в результате анализа и поисковых запросов, варьируются от языка к языку. Например: при использовании поддерживаемого турецкого словаря “token” переводится как “toke”, в то время как, конечно, словарь английского языка переводит его в “token”.
Поддерживаемые языковые словари:
Код | Словарь |
---|---|
простой | Общий |
da | Датский |
nl | Голландский |
en | Английский |
fi | Финский |
fr | Французский |
de | Немецкий |
hu | Венгерский |
it | Итальянский |
no | Норвежский |
pt | Португальский |
ro | Румынский |
ru | Русский |
es | Испанский |
sv | Шведский |
tr | Турецкий |
Алгоритмы ранжирования
Поддерживаемые алгоритмы для упорядочивания результатов:
Алгоритм | Описание |
---|---|
rank | Используйте качество соответствия (0-1) полнотекстового запроса, чтобы упорядочить результаты. |
proximityRank | Похоже на рейтинг, но также учитывает близость совпадений. |