3. Запросы из внешних систем / 3.3. Язык запросов GraphQL
3. Запросы из внешних систем / 3.3. Язык запросов GraphQL

3.3. Язык запросов GraphQL

3.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.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.3.2. Запрос на получение данных

Все агрегаты (объекты логического типа «Aggregate» —- см. подробнее раздел о модели данных) доступны для запроса по имени типа. Общий вид GraphQL запроса на получение данных следующий:

query {
  aggrerate_name {
    fields
  }
}

где

  • aggrerate_name —- имя агрегата,
  • fields —- список запрашиваемых полей.

Для возвращаемого объекта можно указывать произвольный набор полей, который мы хотим получить, т.е. необязательно указывать все поля, описанные для данного агрегата в модели данных. Поля могут разделяться запятой, пробелом или переносом строки.

Примечание

В запросах на получение данных ключевое слово query можно опускать, так как если оно не указано, GraphQL по умолчанию трактует данную операцию как query. В дальнейших примерах так и будет сделано для простоты синтаксиса.

3.3.2.1. Пример выполнения запроса через веб-интерфейс

Для выполнения GraphQL запросов проще всего использовать встроенный веб-клиент на вкладке Graphql в веб-интерфейсе TDG. Для выполнения простого запроса на получение данных для нашего базового примера введите следующий запрос:

{
 User{
   id
   username
     }
}

Обратите внимание, что в фигурных скобках указан тип объекта (Агрегата) User. Далее в отдельных фигурных скобках указаны его поля id и username для их отображения в получаемом ответе.

3.3.2.2. Пример выполнения запроса через HTTP-запрос

Также вы можете выполнить запрос, отправив его в input_processor TDG по протоколу HTTP. Для выполнения запроса при помощи утилиты curl используйте следующую команду в консоли.

curl --request POST \
  --url http://172.19.0.2:8080/graphql \
  --header 'auth-token: ee7fbd80-a9ac-4dcf-8e43-7c98a969c34c' \
  --data '{"query":"{User{id,username}}"}'

Примечание

Используйте в качестве значения для ключа auth-token токен приложений, сгенерированный заранее.

В качестве ответа возвращается объект JSON, содержащий массив со всеми записями типа User, при этом для каждой записи будут указаны поля id и username.

{
  "data": {
    "User": [
      {
        "id": 1,
        "username": "John Smith"
      },
      {
        "id": 2,
        "username": "Adam Sanders"
      }
    ]
  }
}

Если в качестве fields указать пустой список, то система также вернет пустой объект, т.к. GraphQL возвращает именно то, что было указано в запросе.

3.3.3. Выборка агрегатов

Отдельно взятый агрегат можно выбрать по индексу. Для этого индексированное поле нужно указать в качестве аргумента запроса. В примере ниже агрегат User выбирается по индексу id (см. описание модели). Проверка условия выполняется на полное совпадение.

{
  User(id: 1) {
    id
    username
  }
}

Примечание

Для запросов такого рода доступны только индексированные поля. Фильтровать по обычным (неиндексированным) полям таким способом не получится.

В одном запросе можно указывать несколько индексов. В таком случае запрос будет искать объекты, удовлетворяющие всем условиям одновременно (логическая операция конъюнкция или логическое И).

{
  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.3.4. Мультиключевые индексы

Отдельно нужно отметить запросы по так называемому multikey индексу, т.е. индексу по полю, содержащему массив. Для иллюстрации возьмем за основу наш пример, в который было добавлено такое поле (phones) и соответствующий индекс к агрегату User. Поле phones будет содержать массив, в котором хранятся все телефонные номера читателя.

Примечание

Мультиключевой (multikey) индекс не может быть первичным.

Внешне запрос по мультиключевому (multikey) индексу будет выглядеть так же, как и запрос по обычному индексу. Разница может быть в возвращаемом результате, т.к. возможен случай, когда сразу несколько элементов массива будут попадать под условие запроса и, соответственно, у нас будет несколько ключей, указывающих на один и тот же объект. В таком случае этот объект может быть возвращен несколько раз, и это нужно учитывать при составлении запросов и при обработке полученных результатов.

3.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.3.5. Выборка со сравнением

Выборки поддерживают операции сравнения в виде суффиксов в именах индексов.

Поддерживаются следующие операторы сравнения:

  • _gt (Greater Than) — строго больше,
  • _ge (Greater Than or Equal) — больше либо равно,
  • _lt (Less Than) — строго меньше,
  • _le (Less Than or Equal) — меньше либо равно.

Формат запросов:

{
  aggrerate_name(index_gt: value) {
    fields
  }

где

  • index_gt – наименование индекса, по которому производится выборка с индексом для сравнения;
  • value – значение для сравнения.

3.3.5.1. Примеры выборки со сравнением

Например, для получения всех книг, выпущенных после 2008 года, используйте следующий запрос.

{
  Book(year_gt: 2008) {
    book_name
    year
    author
  }
}

Данные суффиксы также поддерживаются для составных индексов и multikey индексов. Предположим, что есть индекс, содержащий в себе такие части, как year и month. Тогда для получения книг, выпущенных после июля 2008 года, выполните следующий запрос.

{
  Book(year_month_gt: [2008, 07]) {
    book_name
    year
    author
  }
}

Примечание

Используемая в нашем примере модель данных не предполагает поля month, но вы можете попробовать добавить его сами (по аналогии с этим разделом) и выполнить вышеуказанный запрос для закрепления практических навыков.

Операторы сравнения для индексов по строковым полям работают в соответствии с правилами сортировки (collation), принятыми в Tarantool.

Также для индексов по строковым полям поддерживается оператор _like – для поиска заданного шаблона в строке. В шаблоне можно использовать подстановочный знак (wildcard) %, который представляет любое количество любых символов. Для примера выполните следующий запрос.

{
  User(phones_like: "+7%") {
    id
    username
    phones
  }
}

3.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.3.7. Пагинация

Для пагинации мы используем метод с непрозрачными курсорами, похожий на тот, что описан здесь.

В общем виде запрос выглядит так:

{
aggrerate_name(first:2, after:$cursor)
}

где:

  • first указывает максимальное количество возвращаемых элементов (по умолчанию 10),
  • after указывает, с какого элемента продолжить выполнение запроса.

В after как раз и передается «непрозрачный курсор». Непрозрачный курсор —- это строка, о смысле которой пользователь не должен задумываться. Все, что нужно знать —- это то, что, используя эту строку, сервер может продолжить выполнение запроса с нужного места.

Каждый агрегат имеет специальное синтетическое поле cursor, доступное через GraphQL. В дизайне пагинации TDG было решено перенести cursor на уровень агрегата, что позволяет не вводить промежуточных уровней запроса edges и node, как это предлагается делать в руководстве по GraphQL.

3.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.3.8. Версионирование и запрос исторических данных

Основные принципы версионирования данных в TDG указаны тут.

Для передачи и запроса версии используется служебное поле version. Важно отметить, что если в запросе не указывается версия, то возвращается последняя (наибольшая) версия.

Количество хранимых версий можно ограничить через конфигурацию системы. По умолчанию количество не ограничено.

3.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.3.9. Ограничения запросов

Для контроля нагрузки на сервер сделаны следующие ограничения запроса.

  • запрос не должен проходить больше scanned строк;
  • запрос не должен возвращать больше returned строк.

Данные ограничения превентивно требуют качественного написания запросов, являясь по сути аналогом профилирования медленных запросов.

Настройка данных параметров возможна при конфигурации системы или с помощью специальных graphql запросов и мутаций.

3.3.10. Изменение данных

TDG поддерживает следующие запросы на изменение данных (mutations, далее – мутации):

  • вставка объекта (insert),
  • удаление объекта (delete).

3.3.10.1. Вставка объекта

Общий вид запроса на вставку (добавление или обновление) объекта:

mutation {
  aggrerate_name(insert: {JSON_input_object}) {
    fields
}

где:

  • mutation –- объявление типа запроса как запрос на изменение данных (мутация),
  • aggrerate_name -— имя агрегата,
  • JSON_input_object –- объект для вставки в формате JSON. Необходимо указать все обязательные поля и их значения. Обязательными являются поля, описанные в модели данных для этого агрегата, тип которых отличен от «null».
  • fields –- (опционально) список полей возвращаемого объекта.

3.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 'auth-token: ee7fbd80-a9ac-4dcf-8e43-7c98a969c34c' \
--data '{"query":"   mutation {User(insert: {id: 1, username: \"John Smith\", phones:[\"+74951234567\", \"+79997654321\"]}) {id username phones}}"}'

Примечание

Символ \" нужен при работе из командной строки для корректной обработки символов кавычек в команде.

Примечание

Используйте в качестве значения для ключа auth-token токен приложений, сгенерированный заранее.

Какой бы способ выполнения запроса вы не выбрали, в результате вы получите следующий ответ.

{
  "data": {
    "User": [
      {
        "username": "John Smith",
        "phones": [
          "+74951234567",
          "+79997654321"
        ],
        "id": 1
      }
    ]
  }
}

3.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.3.10.2. Удаление объектов

Общий вид запроса на удаление объекта:

mutation {
  aggrerate_name(delete: true) {
    fields
}

где:

  • aggrerate_name —- имя агрегата. В качестве аргумента указывается delete: true. Остальные аргументы опциональны: их можно использовать, чтобы задавать логику того, что нужно удалить (например значение одного из ключей или only_if_version).
  • fields -— (опционально) возвращаемые поля.

3.3.10.2.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.3.10.2.1.1. Полное удаление

При обычном удалении информация об объекте помечается признаком удаления и перестаёт учитываться при выборках. Однако существует способ полного удаления информации из TDG без сохранения каких-либо данных, включая различные версии.

Для этого в запросе на удаление укажите параметр permanent_delete: True. Логика его работы совпадает с описанной для такого параметра в данном разделе.

3.3.11. Выполнение сервисов

При помощи GraphQL запросов имеется возможность вызова сервисов в TDG.

Общий вида запроса на выполнение сервиса:

query {
   <service_name>(arg1:value1, arg2:value2, ...)
}

3.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).