3.1.3. Запросы в формате GraphQL | Tdg

Версия:

1.6 / 1.7

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 делятся на следующие два типа:

Частным случаем запроса на получение данных является запрос на выполнение сервиса. В этом случае также используется тип 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.

Нашли ответ на свой вопрос?
Обратная связь