3.1.3. Запросы в формате GraphQL¶
Рассмотрим обработку запросов в формате GraphQL на основе базового примера.
Для отправки запросов используется веб-интерфейс TDG и программа curl
.
Для приёма и обработки запросов вам понадобится кластер TDG,
настроенный ранее.
Основной язык запросов TDG основан на GraphQL.
GraphQL запросы можно выполнить с использованием веб-интерфейса на вкладке Graphql
или отправив их по протоколу HTTP (с корректным заголовком для
авторизации) на адрес вида http://172.19.0.2:8080/graphql
, где
172.19.0.2 — адрес экземпляра TDG с ролью
connector
;8080 — порт, указанный в параметре
http_port
для данного экземпляра.
Запросы GraphQL делятся на следующие два типа:
запрос на изменение данных (mutation, далее — мутация) — добавление, обновление и удаление.
Частным случаем запроса на получение данных является
запрос на выполнение сервиса.
В этом случае также используется тип query
, но обращение идет не напрямую к
хранящимся в системе данным, а к сервису, в котором можно задать
произвольную логику для выборки объектов.
3.1.3.1. Адаптация конфигурации из базового примера¶
Для выполнения примеров в данном разделе используйте кластер TDG, установка и настройка которого проводились ранее.
Для повторения примеров из данного раздела вам потребуется внести определённые исправления в конфигурацию системы, загруженную ранее.
Прежде всего, внесите изменения в модель данных. Для этого отредактируйте файл
model.avsc
из архива с конфигурацией системы. Измените описание объекта с
именем User
так, чтобы оно стало следующим:
{
"name": "User",
"type": "record",
"logicalType": "Aggregate",
"doc": "читатель",
"fields": [
{
"name": "id",
"type": "long"
},
{
"name": "username",
"type": "string"
},
{
"name": "phones",
"type": {
"type": "array",
"items": "string"
}
}
],
"indexes": [
"id",
"phones"
],
"relations": [
{
"name": "subscription",
"to": "Subscription",
"count": "many",
"from_fields": "id",
"to_fields": "user_id"
}
]
}
Также внесите изменение в описание объекта Book
так, чтобы оно стало следующим:
{
"name": "Book",
"type": "record",
"logicalType": "Aggregate",
"doc": "книга",
"fields": [
{
"name": "id",
"type": "long"
},
{
"name": "book_name",
"type": "string"
},
{
"name": "author",
"type": "string"
},
{
"name": "year",
"type": "int"
}
],
"indexes": [
"id",
"year"
],
"relations": [
{
"name": "subscription",
"to": "Subscription",
"count": "many",
"from_fields": "id",
"to_fields": "book_id"
}
]
}
Затем повторно заархивируйте новую модель с остальными файлами конфигурации и повторите загрузку конфигурации системы согласно инструкции для обновления модели данных.
3.1.3.2. Запрос на получение данных¶
Все агрегаты (объекты логического типа «Aggregate» — см. подробнее раздел о модели данных) доступны для запроса по имени типа. Общий вид GraphQL-запроса на получение данных следующий:
query {
aggregate_name {
fields
}
}
где
aggregate_name
— имя агрегата;fields
— список запрашиваемых полей.
Для возвращаемого объекта можно указывать произвольный набор полей, который вы хотите получить, т.е. необязательно указывать все поля, описанные для данного агрегата в модели данных. Поля могут разделяться запятой, пробелом или переносом строки.
Примечание
В запросах на получение данных ключевое слово query
можно опускать, так как
если оно не указано, GraphQL по умолчанию трактует данную операцию как query.
В дальнейших примерах так и будет сделано для простоты синтаксиса.
3.1.3.2.1. Пример выполнения запроса через веб-интерфейс¶
Для выполнения GraphQL-запросов проще всего использовать встроенный веб-клиент на вкладке Graphql в веб-интерфейсе TDG. Для выполнения простого запроса на получение данных для нашего базового примера введите следующий запрос:
{
User {
id
username
}
}
Обратите внимание, что в фигурных скобках указан тип объекта (Агрегата) User
.
Далее в отдельных фигурных скобках указаны его поля id
и username
для их
отображения в получаемом ответе.
3.1.3.2.2. Пример выполнения запроса через HTTP-запрос¶
Также вы можете выполнить запрос, отправив его в input_processor
TDG
по протоколу HTTP. Для выполнения запроса при помощи утилиты curl
используйте
следующую команду в консоли.
curl --request POST \
--url http://172.19.0.2:8080/graphql \
--header 'Authorization: Bearer ee7fbd80-a9ac-4dcf-8e43-7c98a969c34c' \
--data '{"query":"{User{id,username}}"}'
Примечание
Используйте в качестве значения для параметра Authorization: Bearer
токен приложений,
сгенерированный заранее.
В качестве ответа возвращается объект JSON, содержащий массив со всеми записями
типа User
, при этом для каждой записи будут указаны поля id
и username
.
{
"data": {
"User": [
{
"id": 1,
"username": "John Smith"
},
{
"id": 2,
"username": "Adam Sanders"
}
]
}
}
Если в качестве fields
указать пустой список, то система также вернет пустой
объект, т.к. GraphQL возвращает именно то, что было указано в запросе.
3.1.3.3. Выборка агрегатов¶
Отдельно взятый агрегат можно выбрать по индексу. Для этого
индексированное поле нужно указать в качестве аргумента запроса.
В примере ниже агрегат User
выбирается по индексу id
(см. описание модели).
Проверка условия выполняется на полное совпадение.
{
User(id: 1) {
id
username
}
}
Примечание
Для запросов такого рода доступны только индексированные поля. Фильтровать по обычным (неиндексированным) полям таким способом не получится.
В одном запросе можно указывать несколько индексов. В таком случае запрос будет искать объекты, удовлетворяющие всем условиям одновременно (логическая операция конъюнкция или логическое И).
query {
Subscription(user_id: 1, book_id: 3) {
id
user_id
book_id
}
}
Для запроса по составному (мультиколоночному) индексу используется массив
значений. В нашем примере у агрегата типа Subscription
есть составной индекс pkey
, включающий в себя поля id
и user_id
. Пример
запроса по этому индексу выглядит так:
{
Subscription(pkey: [2, 1]) {
id
book_id
user_id
}
}
3.1.3.4. Мультиключевые индексы¶
Отдельно нужно отметить запросы по так называемому мультиключевому индексу (multikey index), т.е.
индексу по полю, содержащему массив. Для иллюстрации возьмем за основу
наш пример, в который test
такое поле (phones
) и соответствующий индекс к агрегату User
.
Поле phones
будет содержать массив, в котором хранятся все телефонные номера
читателя.
Примечание
Мультиключевой индекс не может быть первичным.
Внешне запрос по мультиключевому индексу будет выглядеть так же, как и запрос по обычному индексу. Разница может быть в возвращаемом результате, т.к. возможен случай, когда сразу несколько элементов массива будут попадать под условие запроса и, соответственно, у нас будет несколько ключей, указывающих на один и тот же объект. В таком случае этот объект может быть возвращен несколько раз, и это нужно учитывать при составлении запросов и при обработке полученных результатов.
3.1.3.4.1. Пример запроса с мультиключевым индексом¶
Допустим, необходимо сделать выборку читателей (User
), телефоны которых начинаются
с определенного кода страны, например, «+7». У читателя с id = 1
таких
телефонов два:
{
"id": 1,
"username": "John Smith",
"phones": [
"+74951234567",
"+79997654321",
"+19001234567"
]
}
Соответственно запрос по индексу phones
для такого случая вернет агрегат
типа User
c id = 1
дважды
Выполните следующий запрос на вкладке Graphql веб-интерфейса TDG.
{
User(phones_like: "+7%") {
id
username
phones
}
}
В результате вы получите следующий ответ.
{
"data": {
"User": [
{
"username": "John Smith",
"phone": [
"+74951234567",
"+79997654321",
"+19001234567"
],
"id": 1
},
{
"username": "John Smith",
"phone": [
"+74951234567",
"+79997654321",
"+19001234567"
],
"id": 1
}
]
}
}
3.1.3.5. Выборка со сравнением¶
Выборки поддерживают операции сравнения в виде суффиксов в именах индексов.
Поддерживаются следующие операторы сравнения:
_gt
(Greater Than) — строго больше;_ge
(Greater Than or Equal) — больше либо равно;_lt
(Less Than) — строго меньше;_le
(Less Than or Equal) — меньше либо равно.
Формат запросов:
{
aggregate_name(index_gt: value) {
fields
}
}
где
index_gt
— наименование индекса, по которому производится выборка с индексом для сравнения;value
— значение для сравнения.
3.1.3.5.1. Примеры выборки со сравнением¶
Например, для получения всех книг, выпущенных после 2008 года, используйте следующий запрос.
{
Book(year_gt: 2008) {
book_name
year
author
}
}
Данные суффиксы также поддерживаются для составных индексов и multikey индексов.
Предположим, что есть индекс, содержащий в себе такие части, как year
и month
.
Тогда для получения книг, выпущенных после июля 2008 года, выполните следующий запрос.
{
Book(year_month_gt: [2008, 7]) {
book_name
year
author
}
}
Примечание
Используемая в нашем примере модель данных не предполагает поля month
,
но вы можете попробовать добавить его сами (по аналогии с этим
разделом) и выполнить вышеуказанный запрос для закрепления практических навыков.
Операторы сравнения для индексов по строковым полям работают в соответствии с правилами сортировки (collation), принятыми в Tarantool.
Также для индексов по строковым полям поддерживается оператор _like
— для
поиска заданного шаблона в строке. В шаблоне можно использовать подстановочный
знак (wildcard) %
, который представляет любое количество любых символов.
Для примера выполните следующий запрос.
{
User(phones_like: "+7%") {
id
username
phones
}
}
3.1.3.6. Выборка агрегатов по связям¶
Для фильтрации связанных агрегатов используется тот же синтаксис, что и для выборки обычных агрегатов.
В используемом базовом примере агрегаты User
и Subscription
связаны отношением «один ко многим». Соответственно, в одном
запросе можно получить одновременно и данные читателя, и информацию о его
подписках (или об отдельно взятой подписке, как в примере ниже).
{
User(id: 1) {
id
username
phones
subscription(pkey: [2, 1]) {
id
book_id
}
}
}
Данный вид запроса аналогичен SQL LEFT OUTER JOIN.
Как объясняется в разделе про отношения между объектами,
отношения могут быть односторонними или полными (двусторонними). В нашем примере
в модели данных заданы двусторонние отношения (например, поле relations
определено
и у User
, и у Subscription
), поэтому также возможен запрос «с другой
стороны» — т.е. запрашивая данные по какому-то из абонементов/подписок, в этом же
запросе можно получить и данные читателя. В качестве примера выполните следующий
запрос.
{
Subscription(pkey: [2, 1]) {
id
book_id
user {
id
username
phones
}
}
}
3.1.3.7. Пагинация¶
Для пагинации используется метод с непрозрачными курсорами, аналогичный
описанному в документации по GraphQL (https://graphql.org/learn/pagination/#pagination-and-edges
).
В общем виде запрос выглядит так:
{
aggregate_name(first: 2, after: $cursor)
}
где:
first
указывает максимальное количество возвращаемых элементов (по умолчанию 10);after
указывает, с какого элемента продолжить выполнение запроса.
В after
как раз и передается «непрозрачный курсор». Непрозрачный курсор —
это строка, о смысле которой пользователь не должен задумываться. Все, что нужно
знать — это то, что, используя эту строку, сервер может продолжить выполнение
запроса с нужного места.
Каждый агрегат имеет специальное синтетическое поле cursor
, доступное через
GraphQL. В дизайне пагинации TDG было решено перенести cursor
на уровень агрегата, что позволяет не вводить промежуточных уровней запроса
edges
и node
, как это предлагается
делать в руководстве по GraphQL (https://graphql.org/learn/pagination/#pagination-and-edges
).
3.1.3.7.1. Примеры использования пагинации¶
В качестве примера первого запроса с пагинацией выполните следующий запрос.
{
User(first: 2) {
id
username
cursor
}
}
Полученный ответ будет напоминать следующий.
{
"data": {
"User": [
{
"cursor": "gaRzY2Fuk6ABzxYAPuJNoseB",
"username": "John Smith",
"id": 1
},
{
"cursor": "gaRzY2Fuk6ACzxYAEQtuI8e5",
"username": "Adam Sanders",
"id": 2
}
]
}
}
Теперь, чтобы продолжить получение следующей порции данных, возьмите
поле cursor
из последнего полученного объекта (в данном случае —
"gaRzY2Fuk6ACzxYAEQtuI8e5"
) и передайте его в аргумент after
,
выполнив следующий запрос.
{
User(first: 2, after: "gaRzY2Fuk6ACzxYAEQtuI8e5") {
id
username
cursor
}
}
Для обратной пагинации необходимо использовать отрицательное число first
.
В этом случае система вернет предыдущие объекты относительно after
.
Обратная пагинация некольцевая: с помощью неё нет возможности получить
последний объект множества выборки, смещаясь от первого.
Пагинация также доступна для запроса по связям. Например:
{
User(id: 1) {
id
username
phones
subscription(first: 2) {
id
book_id
}
}
}
3.1.3.8. Версионирование и запрос исторических данных¶
Основные принципы версионирования данных в TDG указаны тут.
Для передачи и запроса версии используется служебное поле version
.
Важно отметить, что если в запросе не указывается версия, то возвращается последняя
(наибольшая) версия.
Количество хранимых версий можно ограничить через конфигурацию системы. По умолчанию количество не ограничено.
3.1.3.8.1. Пример запроса с использованием исторических данных¶
Допустим в результате одного из запросов у записи типа User
с id
равным
1 был удалён один из номеров телефона (записана новая версия данных без этого
номера).
Нужно получить предыдущую версию, где есть удаленный номер телефона.
В таком случае сделайте запрос на получение последней версии агрегата (не указывая,
какую версию хотите получить), и добавьте в вывод значение поля version
:
{
User(id: 1) {
id
username
phones
version
}
}
В ответ будет получен следующий или подобный JSON:
{
"data": {
"User": [
{
"version": "1585393144680150551",
"username": "John Smith",
"phones": [
"+74951234567",
"+79997654321"
],
"id": 1
}
]
}
}
Берем значение «version»: «1585393144680150551», вычитаем из него 1, получаем «version»: «1585393144680150550», и указываем это значение как аргумент в повторном запросе.
{
User(id: 1, version: 1585393144680150550) {
id
username
phones
version
}
}
TDG будет искать или версию, равную указанной, или, если не найдет равную, то ближайшую младшую версию. Таким образом в любом случае в ответе будет получена предыдущая версия агрегата:
{
"data": {
"User": [
{
"version": "1585392369718590708",
"username": "John Smith",
"phones": [
"+74951234567",
"+79997654321",
"+19001234567"
],
"id": 1
}
]
}
}
Таким образом можно итерационно пройти по предыдущим версиям. При запросе версии меньше, чем минимально существующая, в ответ вернётся пустой объект.
Также возможно запросить все версии агрегата через аргумент all_versions
:
{
User(id: 1, all_versions: true) {
id
username
version
}
}
При использовании аргумента all_versions
можно, конечно же, ограничить количество
возвращаемых версий агрегата при помощи пагинации.
{
User(id: 1, all_versions: true, first: 3) {
id
username
version
}
}
3.1.3.9. Ограничения запросов¶
Для контроля нагрузки на сервер сделаны следующие ограничения запроса:
запрос не должен проходить больше
scanned
строк;запрос не должен возвращать больше
returned
строк.
Данные ограничения превентивно требуют качественного написания запросов, являясь по сути аналогом профилирования медленных запросов.
Настройка данных параметров возможна при конфигурации системы или с помощью специальных запросов и мутаций GraphQL.
3.1.3.10. Изменение данных¶
TDG поддерживает следующие запросы на изменение данных (мутации):
вставка объекта (insert);
обновление объекта (update);
удаление объекта (delete).
3.1.3.10.1. Вставка объекта¶
Общий вид запроса на вставку (добавление или обновление) объекта:
mutation {
aggregate_name(insert: {JSON_input_object}) {
fields
}
}
где:
mutation
— объявление типа запроса как запрос на изменение данных (мутация);aggregate_name
— имя агрегата;JSON_input_object
— объект для вставки в формате JSON. Необходимо указать все обязательные поля и их значения. Обязательными являются поля, описанные в модели данных для этого агрегата, тип которых отличен от «null»;fields
— (опционально) список полей возвращаемого объекта.
3.1.3.10.1.1. Пример запроса на вставку объекта¶
Для добавления нового или обновления существующего (если объект с таким первичным ключом уже есть) объекта перейдите на вкладку Graphql и выполните следующий запрос:
mutation {
User(
insert: {
id: 1
username: "John Smith"
phones: ["+74951234567", "+79997654321"]
}
) {
id
username
phones
}
}
В качестве альтернативы вы можете отправить HTTP-запрос, например при помощи программы
curl
. Для этого выполните следующую команду.
curl --request POST \
--url 'http://172.19.0.2:8080/graphql?=' \
--header 'Authorization: Bearer ee7fbd80-a9ac-4dcf-8e43-7c98a969c34c' \
--data '{"query":" mutation {User(insert: {id: 1, username: \"John Smith\", phones:[\"+74951234567\", \"+79997654321\"]}) {id username phones}}"}'
Примечание
Символ \"
нужен при работе из командной строки для корректной обработки
символов кавычек в команде.
Примечание
Используйте в качестве значения для параметра Authorization: Bearer
токен приложений,
сгенерированный заранее.
Какой бы способ выполнения запроса вы ни выбрали, в результате вы получите следующий ответ:
{
"data": {
"User": [
{
"username": "John Smith",
"phones": [
"+74951234567",
"+79997654321"
],
"id": 1
}
]
}
}
3.1.3.10.1.2. Пример запроса на вставку с оптимистичной блокировкой¶
В запросах могут использоваться оптимистичные блокировки.
Для этого используйте параметр only_if_version
, как это показано в следующем
примере:
mutation {
User(
insert: {
id: 1
username: "John Smith"
phones: ["+74951234567", "+79997654321"]
}
only_if_version: 1585392369718590708
) {
id
username
phones
}
}
3.1.3.10.2. Обновление объекта¶
Общий вид запроса на обновление объекта:
mutation {
aggregate_name(update: filter [[mutator, path, new_value], ...]) {
fields
}
}
где:
mutation
— объявление типа запроса как запроса на изменение данных (мутация);aggregate_name
— имя агрегата;filter
— список условий-предикатов для выбора объектов указанного типа;[[mutator, path, new_value], ...]
— список мутаторов:mutator
— имя мутатора. Возможные значения:set
(устанавливает значение),add
(увеличивает значение на указанное число),sub
(уменьшает значение на указанное число);path
— строковый путь до поля объекта с точкой-разделителем (.
). Путь до объекта массива должен включать индекс массива или символ*
для захвата всех дочерних объектов;new_value
— новое значение.
fields
— список полей возвращаемого объекта.
3.1.3.10.2.1. Пример запроса на обновление объекта¶
Обновим ранее добавленный объект типа User
— изменим значение поля username
.
Нужный объект данного типа выберем по первичному ключу id
.
Перейдите на вкладку Graphql и выполните следующий запрос:
mutation {
User(id:1 update:[["set", "username", "John D. Smith"]]) {
id
username
phones
}
}
В качестве альтернативы вы можете отправить HTTP-запрос, например, при помощи программы curl
.
Для этого выполните следующую команду:
curl --request POST \
--url 'http://172.19.0.2:8080/graphql?=' \
--header 'Authentication: Bearer ee7fbd80-a9ac-4dcf-8e43-7c98a969c34c' \
--data '{"query":"mutation {User(update: id: 1 [[\"set\", \"username\", \"John D. Smith\"]]) {id username phones}}"}'
Примечание
Символ
\"
нужен при работе из командной строки для корректной обработки символов кавычек в команде.Для параметра
Authentication: Bearer
используйте в качестве значения токен приложений, сгенерированный заранее.
Какой бы способ выполнения запроса вы ни выбрали, в результате вы получите следующий ответ:
{
"data": {
"User": [
{
"username": "John D. Smith",
"phones": [
"+74951234567",
"+79997654321"
],
"id": 1
}
]
}
}
3.1.3.10.3. Удаление объектов¶
Общий вид запроса на удаление объекта:
mutation {
aggregate_name(delete: true) {
fields
}
}
где:
aggregate_name
— имя агрегата. В качестве аргумента указываетсяdelete: true
. Остальные аргументы опциональны: их можно использовать, чтобы задавать логику того, что нужно удалить (например значение одного из ключей илиonly_if_version
);fields
— (опционально) возвращаемые поля.
3.1.3.10.3.1. Примеры запросов на удаление объектов¶
Если не указаны другие аргументы помимо delete: true
, будут удалены все
объекты данного типа, например:
mutation {
User(delete: true) {
id
username
}
}
В результате вы получите следующий или подобный ответ, а все перечисленные объекты будут удалены.
{
"data": {
"User": [
{
"username": "John Smith",
"id": 1
},
{
"username": "Adam Sanders",
"id": 2
}
]
}
}
Для выборочного удаления можно использовать все те же аргументы, которые используются для фильтрации в запросах на получение данных.
Например, удаление выборочного объекта по первичному ключу:
mutation {
User(delete: true, id: 1) {
id
username
}
}
3.1.3.10.3.1.1. Полное удаление¶
При обычном удалении информация об объекте помечается признаком удаления и перестаёт учитываться при выборках. Однако существует способ полного удаления информации из TDG без сохранения каких-либо данных, включая различные версии.
Для этого в запросе на удаление укажите параметр permanent_delete: True
.
Логика его работы совпадает с описанной для такого параметра в
данном разделе.
3.1.3.11. Выполнение сервисов¶
GraphQL-запросы дают возможность вызова сервисов в TDG.
Общий вида запроса на выполнение сервиса:
query {
service_name(arg1: value1, arg2: value2, ...)
}
3.1.3.11.1. Пример запроса на выполнение сервиса¶
В базовом примере имеется сервис, описанный в файле конфигурации следующими строками:
services:
select_user_books:
doc: "select_user_books"
function: select_user_books
return_type: string
args:
user_id: long
Для вызова этого сервиса используйте следующий запрос:
{
select_user_books(user_id: 1)
}
В ответе на данный запрос вернется результат вызова сервиса:
{
"data": {
"select_user_books": "[1, 3]"
}
}
Как видно, в ответе действительно имеется массив с идентификаторами (id
)
всех книг, которые взял пользователь с user_id
= 1.