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 | Похоже на рейтинг, но также учитывает близость совпадений. |