Перейти к основному содержимому

4. GraphQL - основы

Введение в GraphQL

Что такое GraphQL и зачем он нужен

GraphQL — это язык запросов для API и система выполнения запросов, разработанная компанией Facebook в 2012 году как внутренний проект для оптимизации работы мобильных приложений, которые сталкивались с проблемами производительности и избыточной передачи данных при использовании REST API. Основными авторами GraphQL считаются Ли Байрон (Lee Byron), Ник Шрок (Nick Schrock) и Дан Шафер (Dan Schafer). Первая публичная спецификация и открытый исходный код были опубликованы в 2015 году. С этого момента GraphQL быстро приобрёл популярность в сообществе разработчиков и стал стандартом для построения современных API. Сегодня его поддерживают такие компании, как GitHub, Twitter, Shopify, Pinterest и многие другие. Вокруг GraphQL сформировалась большая экосистема инструментов и библиотек для различных языков программирования.

GraphQL позволяет клиентам запрашивать точно те данные, которые им нужны, не больше и не меньше. GraphQL решает проблемы традиционных REST API, такие как избыточная загрузка данных (over-fetching) и недостаточная загрузка (under-fetching). Это делает приложения более эффективными и быстрыми, особенно на мобильных устройствах с ограниченной пропускной способностью.

GraphQL является целевым протоколом для DataSpace Community Edition и будет использоваться в качестве основного способа взаимодействия с данными. В данном разделе мы рассмотрим основы работы с GraphQL и его синтаксис.

Сравнение с REST API

GraphQL часто сравнивают с REST (Representational State Transfer), который долгое время был стандартом для разработки веб-API. Рассмотрим основные отличия GraphQL от REST

АспектGraphQLREST
Конечные точкиЕдиная конечная точкаМножество специфичных конечных точек
Получение данныхКлиент указывает точную структуру данныхСервер определяет структуру ответа
ВерсионированиеВозможность плавной эволюции APIОбычно требует новой версии API
Over-fetchingМинимизирован — клиент получает только запрошенные данныеРаспространен — клиент получает все данные ресурса
Under-fetchingРедко встречается благодаря возможности агрегировать данныеЧасто требует множества запросов

Основные принципы работы

GraphQL основан на концепции схемы, которая описывает все доступные типы данных и операции. Клиент отправляет запрос, указывая нужные поля, а сервер возвращает данные точно в той структуре, которая была запрошена. Система использует резолверы — функции, которые извлекают данные для каждого поля в запросе. GraphQL поддерживает три типа операций: запросы (queries) для чтения данных, мутации (mutations) для изменения данных и подписки (subscriptions) для получения обновлений в реальном времени.

GraphiQL/GraphQL Playground

Знакомство с инструментами разработки

GraphiQL — это интерактивная среда разработки для работы с GraphQL API, которая позволяет легко тестировать запросы, изучать схему и отлаживать операции. Этот инструмент предоставляет удобный веб-интерфейс с подсветкой синтаксиса, автодополнением и встроенной документацией. В DataSpace Community Edition GraphiQL доступен по адресу http://localhost:8080/graphiql?path=/models/1/graphql и будет нашим основным инструментом для изучения и работы с GraphQL API.

GraphiQL предоставляет несколько ключевых возможностей: панель запросов для написания и выполнения GraphQL операций, панель результатов для просмотра ответов сервера, встроенную документацию схемы с автоматическим описанием всех доступных типов и полей, а также функцию автодополнения, которая помогает писать корректные запросы. Интерфейс разделен на две основные области: редактор запросов слева и область результатов справа.

Практическая работа с запросами

Рассмотрим практические примеры работы с GraphQL API. На предыдущих шагах в DataSpace нами была загружена модель медицинской клиники. Познакомимся с GraphQL API на примере этой модели. Откройте GraphiQL по адресу http://localhost:8080/graphiql?path=/models/1/graphql. Начнем с базовых операций создания, изменения и удаления данных.

Пример 1: Создание новой персоны (Mutation)

mutation createPerson {
packet {
createPerson(input: {
id: "person_001"
firstName: "Иван"
lastName: "Петров"
sex: MALE
birthDate: "1985-03-15"
}) {
id
firstName
lastName
sex
birthDate
}
}
}

Скопируйте и вставьте этот запрос в GraphiQL и нажмите на кнопку "Execute Query" (или нажмите Ctrl+Enter). Этот запрос создаёт новую персону с указанными данными и возвращает созданный объект с выбранными полями.

Пример 2: Поиск персон (Query)

Откройте новую вкладку в GraphiQL и скопируйте и вставьте следующий запрос:

query searchPerson {
searchPerson {
elems {
id
firstName
lastName
sex
birthDate
}
}
}

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

Пример 3: Обновление данных персоны (Mutation)

mutation updatePerson{
packet {
updatePerson(input: {
id: "person_001"
firstName: "Иван"
lastName: "Петров"
sex: MALE
birthDate: "1985-03-15"
}) {
id
firstName
lastName
sex
birthDate
}
}
}

В результате выполнения запроса вы получите обновлённые данные персоны. Обратите внимание на то, что в запросе мы указали id: "person_001", что означает, что мы хотим обновить данные персоны с этим идентификатором. Таким образом GraphQL позволяет нам обновлять данные по идентификатору.

Пример 4: Удаление персоны (Mutation)

mutation deletePerson {
packet {
deletePerson(id: "person_001")
}
}

В результате выполнения запроса вы получите сообщение об успешном удалении персоны:

{
"data": {
"packet": {
"deletePerson": "success"
}
}
}

Теперь если повторно выполнить запрос на поиск персон (searchPerson), то результат будет пустым:

{
"data": {
"searchPerson": {
"elems": []
}
}
}

Таким образом, мы познакомились с работой в GraphiQL и прошли полный цикл работы с GraphQL API: создание, поиск, обновление и удаление данных. Далее мы рассмотрим основы GraphQL, такие как схема и система типов, запросы и мутации, а также дополнительные возможности GraphQL.

Основы GraphQL

GraphQL-cхема и система типов

GraphQL построен на концепции схемы (schema) — строго типизированного описания API, которое определяет, какие данные доступны и как клиенты могут с ними взаимодействовать. Схема является контрактом между клиентом и сервером, гарантируя предсказуемость и надежность API.

Рассмотрим примеро очень простой GraphQL-схемы:

type Query {
fruits: [Fruit!]!
}

type Fruit {
id: ID!
name: String!
quantity: Int!
price: Int!
averageWeight: Float
hasEdibleSeeds: Boolean
nutrients: [String]
}

type Vegetable {
id: ID!
name: String!
quantity: Int!
price: Int!
averageWeight: Float
vegetableFamily: String
isPickled: Boolean
}

Эта схема содержит два типа данных: Fruit и Vegetable. Каждый тип имеет свои поля, которые описывают свойства объектов этого типа. В GraphQL-схеме типы данных определяются с помощью ключевых слов type и query.

  • type - определяет тип данных
  • query - определяет запросы к данным
  • id - поле идентификатора
  • name - поле имени
  • quantity - поле количества
  • price - поле цены
  • averageWeight - поле среднего веса
  • hasEdibleSeeds - поле наличия съедобных семян
  • nutrients - поле питательных веществ
  • vegetableFamily - поле семейства овощей
  • isPickled - поле наличия засолки

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

В DataSpace CE схема создается автоматически на основе загруженных моделей данных, но возможности скачать ее через графический web-интерфейс GraphiQL пока нет. Однако, получить файл схемы можно с помощью сторонних утилит. В разделе 6 мы познакомимся с автоматической кодогенерацией и созданием React-компонентов на основе GraphQL-схемы и запросов. При этом получение самой схемы будет происходить автоматически с помощью npm-пакета get-graphql-schema утилиты кодогенерации.

Изучение схемы через GraphiQL

GraphiQL предоставляет встроенную документацию схемы. Нажмите на кнопку "Docs" в левом верхнем углу интерфейса, чтобы исследовать:

  • Все доступные типы данных
  • Поля каждого типа и их описания
  • Аргументы операций и их типы
  • Взаимосвязи между типами

Пример исследования схемы:

  1. Откройте раздел "_Query" в документации
  2. Найдите операцию searchDoctorSchedule
  3. Изучите её аргументы и возвращаемый тип

Помимо GraphQL-схемы в web-интерфейсе GraphiQL можно исследовать сами GraphQL-запросы и мутации на вкладке GraphExplorer. Интерфейс позволяет удобным образом создавать и тестировать запросы и мутации, а также посмотреть их результаты.

Валидация запросов

Строгая типизация схемы обеспечивает автоматическую валидацию всех запросов:

# Корректный запрос
query {
searchPerson(limit: 10) {
elems {
id
firstName
}
}
}

# Некорректный запрос - поле 'unknownField' не существует
query {
searchPerson {
elems {
unknownField # Ошибка валидации!
}
}
}

GraphiQL подсвечивает ошибки в реальном времени, помогая писать корректные запросы и предотвращая ошибки на этапе разработки.

Запросы (Queries)

В GraphQL запросы используются для чтения данных. Они начинаются с ключевого слова query и содержат список полей, которые мы хотим получить. Рассмотрим уже знакомый нам пример запроса, который возвращает список персон:

Запрос:

query searchPerson {
searchPerson {
elems {
id
firstName
lastName
sex
birthDate
}
}
}

Разберем запрос по частям:

  • query — ключевое слово, обозначающее запрос
  • searchPerson — имя запроса
  • elems { ... } — список полей, которые мы хотим получить
  • id — поле идентификатора персоны
  • firstName — поле имени персоны
  • lastName — поле фамилии персоны
  • sex — поле пола персоны
  • birthDate — поле даты рождения персоны

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

Мутации (Mutations)

Мутации используются для изменения данных на сервере. Если проводить аналогию с SQL, то мутации в GraphQL аналогичны операциям INSERT, UPDATE и DELETE в SQL. А запросы в GraphQL аналогичны операциям SELECT в SQL. При этом мутации выполняются последовательно, а запросы могут выполняться параллельно. Это позволяет нам управлять порядком выполнения операций и обеспечивает целостность данных. Мутации начинаются с ключевого слова mutation и содержат список полей, которые мы хотим изменить. Рассмотрим пример мутации, которая создаёт новую персону:

Мутация:

mutation createPerson {
packet {
createPerson(input: {
id: "person_001"
firstName: "Иван"
lastName: "Петров"
sex: MALE
birthDate: "1985-03-15"
}) {
id
firstName
lastName
sex
birthDate
}
}
}

Подробнее рассмотрим ключевое слово input. Мутация может содержать входные параметры, которые передаются в запрос в виде объекта. В нашем примере мы передаем входные параметры в виде объекта input, который содержит поля id, firstName, lastName, sex и birthDate. При этом входные параметры могут быть обязательными и необязательными. В нашем примере обязательными являются все поля кроме поля birthDate. Обязательность поля в DataSpace CE определяется в GraphQL-схеме наличием символа ! в конце типа поля. Также важно, что обязательное поле не может быть передано в запросе в виде null.

Переменные и аргументы

Кроме передачи входных параметров в виде объекта input, мутации и запросы могут содержать входные параметры в виде переменных. Рассмотрим пример мутации, которая создаёт новую персону с использованием переменных:

mutation createPerson($input: _CreatePersonInput!){
packet {
createPerson(input: $input){
id
firstName
lastName
sex
birthDate
}
}
}

Переменные запроса:

{
"input": {
"id": "person_001",
"firstName": "Иван",
"lastName": "Петров",
"sex": "MALE"
}
}

Переменные запроса передаются в специальном разделе Variables интерфейса GraphiQL, а сами данные передаются в формате JSON.

Возможность передачи данных через переменные даёт возможность использовать один и тот же запрос для разных данных, а также передавать данные из внешнего источника, например, из формы или из другого запроса.

Псевдонимы и фрагменты

Псевдонимы (Алиасы)

Псевдонимы (алиасы) в GraphQL позволяют переименовывать поля в результате запроса. Это особенно полезно, когда нужно:

  • Получить одни и те же данные с разными параметрами в одном запросе
  • Избежать конфликтов имен полей
  • Сделать результат более читаемым для клиентского кода

Синтаксис псевдонимов:

query {
aliasName: fieldName(arguments) {
subfields
}
}

Пример 1: Получение двух разных наборов персон в одном запросе В этом примере мы получаем два отдельных списка - мужчин и женщин - в одном запросе, используя псевдонимы malePersons и femalePersons. Без использования псевдонимов нам бы пришлось писать два отдельных запроса.

query searchMultiplePeople {
malePersons: searchPerson(cond: "it.sex == 'MALE'") {
elems {
id
firstName
lastName
sex
}
}
femalePersons: searchPerson(cond: "it.sex == 'FEMALE'") {
elems {
id
firstName
lastName
sex
}
}
}

В этом примере мы получаем два отдельных списка - мужчин и женщин - в одном запросе, используя псевдонимы malePersons и femalePersons.

Пример 2: Переименование полей для удобства Иногда бывает удобно переименовать поля для удобства чтения результата запроса. В этом примере мы переименовываем поля id, beginDate, endDate и entityId на более понятные имена.

query doctorScheduleWithAliases {
searchDoctorSchedule(limit: 5) {
schedules: elems {
scheduleId: id
startTime: beginDate
endTime: endDate
doctor: clinicDoctor {
doctorEmail: entityId
}
}
}
}

Здесь мы используем псевдонимы для создания более понятных имен полей:

  • schedules вместо elems
  • scheduleId вместо id
  • startTime вместо beginDate
  • endTime вместо endDate
  • doctorEmail вместо entityId

Пример 3: Получение статистики по разным периодам Данный запрос возвращает количество записей в расписании приемов врача за текущую и следующую неделю.

query scheduleStatistics {
thisWeek: searchDoctorSchedule(
cond: "it.beginDate >= D2024-01-15 && it.beginDate < D2024-01-22"
) {
count
}
nextWeek: searchDoctorSchedule(
cond: "it.beginDate >= D2024-01-22 && it.beginDate < D2024-01-29"
) {
count
}
}

Псевдонимы делают GraphQL запросы более гибкими и позволяют получать сложно структурированные данные в одном запросе.

Фрагменты

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

Синтаксис фрагментов:

fragment FragmentName on TypeName {
field1
field2
nestedObject {
nestedField
}
}

Здесь важно понимать смысл ключевого слова on. Оно указывает на тип, для которого будет использоваться фрагмент. В данном примере фрагмент будет использоваться для типа Person.

Основной пример использования фрагментов:

В этом примере мы определяем фрагмент PersonAttributes для типа Person, который содержит поля id, birthDate, firstName, lastName и sex. Затем мы используем этот фрагмент в запросе поиска персон.

fragment PersonAttributes on Person {
id
birthDate
firstName
lastName
sex
}

query searchPerson {
searchPerson {
elems {
...PersonAttributes
}
}
}

При этом нет необходимости в каждом запросе указывать все поля, достаточно указать только фрагмент PersonAttributes. Это делает код более кратким и понятным.

Преимущества использования фрагментов:

  1. Переиспользование кода — один фрагмент можно использовать в нескольких запросах
  2. Читаемость — сложные запросы становятся более структурированными
  3. Поддержка — изменения в структуре данных требуют обновления только фрагмента
  4. Согласованность — гарантирует одинаковую структуру данных во всех местах использования

Фрагменты являются мощным инструментом для организации GraphQL-запросов и особенно важны при разработке крупных приложений с множественными компонентами, которые работают с одними и теми же данными.

Пакеты (packet)

Пакеты в DataSpace CE представляют собой механизм транзакционного выполнения операций в рамках одного агрегата. Все операции внутри пакета выполняются атомарно — либо все операции завершаются успешно, либо все отменяются. Это обеспечивает целостность данных и соответствует принципам предметно-ориентированного проектирования (DDD).

Основные особенности пакетов

Транзакционность:

  • Все мутации внутри пакета выполняются в рамках одной транзакции
  • При ошибке любой операции весь пакет откатывается
  • Гарантируется консистентность данных

Идемпотентность:

  • Поддержка параметра idempotencePacketId для предотвращения дублирования операций
  • Повторное выполнение с тем же ID не создает дубликатов

Синтаксис использования пакетов

mutation {
packet(idempotencePacketId: "unique-id") {
# Операции внутри пакета
}
}

Практический пример

В данном примере мы создаем две персоны врача с использованием пакетов. Обратите внимание, что в каждом пакете мы используем псевдоним p1 и p2 для идентификации персоны. Это позволяет нам получить результаты в виде объекта с ключами p1 и p2.

mutation CreatePersons {
p1: packet(idempotencePacketId: "persons-doctors-001") {
person1: createPerson(input: {
id: "person-doctor-1"
firstName: "Анна"
lastName: "Петрова"
birthDate: "1980-05-15"
sex: FEMALE
}) {
id firstName lastName sex birthDate
}
}

p2: packet(idempotencePacketId: "persons-doctors-002") {
person2: createPerson(input: {
id: "person-doctor-2"
firstName: "Михаил"
lastName: "Сидоров"
birthDate: "1975-09-22"
sex: MALE
}) {
id firstName lastName sex birthDate
}
}
}

Теперь удалим эти две персоны также с использованием пакетов.

mutation DeletePersons {
p1: packet(idempotencePacketId: "persons-doctors-003") {
deletePerson(id: "person-doctor-1")
}
p2: packet(idempotencePacketId: "persons-doctors-004") {
deletePerson(id: "person-doctor-2")
}
}

Классы-справочники (dictionaryPacket)

Классы-справочники в DataSpace CE представляют собой специальный тип сущностей, предназначенных для хранения справочной информации. Они управляются через отдельный механизм dictionaryPacket, который обеспечивает централизованное управление справочными данными независимо от основных бизнес-агрегатов. Также в DataSpace есть особенности хранения справочных данных на уровне базы данных, но в этом курсе мы не будем рассматривать этот аспект.

Особенности справочников

Независимость от агрегатов:

  • Справочники не принадлежат к конкретному агрегату
  • Могут использоваться множественными агрегатами

Типы справочников в медицинской модели:

  • DoctorType - типы врачей (например, терапевт, хирург, педиатр, кардиолог)
  • OfficeType - типы кабинетов (например, консультационный, процедурный, операционная)

Синтаксис работы со справочниками

mutation {
dictionaryPacket {
# Операции со справочниками
}
}

Практические примеры работы со справочниками

Пример 1: Создание типов врачей

mutation CreateDoctorTypes {
dictionaryPacket {
# Создание нового типа врача
neurologist: updateOrCreateDoctorType(
input: {
id: "neurologist"
name: "Невролог"
descr: "Врач-специалист по заболеваниям нервной системы"
}
) {
created
returning {
id
name
descr
}
}

# Обновление существующего типа врача
therapist: updateOrCreateDoctorType(
input: {
id: "therapist"
name: "Врач-терапевт"
descr: "Врач общей практики, обновленное описание"
}
) {
created
returning {
id
name
descr
}
}
}
}

Здесь стоит обратить внимание на название операции updateOrCreateDoctorType. Она позволяет создать новый тип врача, если он не существует, или обновить существующий тип врача, если он уже существует. Также стоит обратить внимание на то, что в результате выполнения этой операции мы получаем объект с ключами created и returning. Ключ created указывает на то, что операция была выполнена успешно, а ключ returning содержит результат операции.

Пример 2: Создание типов кабинетов

mutation CreateOfficeTypes {
dictionaryPacket {
# Рентген-кабинет
xray: updateOrCreateOfficeType(
input: {
id: "office-type-xray"
name: "Рентген-кабинет"
}
) {
created
returning { id name }
}

# Лабораторный кабинет
laboratory: updateOrCreateOfficeType(
input: {
id: "office-type-laboratory"
name: "Лабораторный кабинет"
}
) {
created
returning { id name }
}

# Кабинет УЗИ
ultrasound: updateOrCreateOfficeType(
input: {
id: "office-type-ultrasound"
name: "Кабинет УЗИ"
}
) {
created
returning { id name }
}
}
}

Пример 3: Удаление созданных типов врачей и кабинетов

mutation DeleteCreatedDoctorTypes {
dictionaryPacket {
d1:deleteDoctorType(id: "neurologist")
d2:deleteDoctorType(id: "therapist")
}
}
mutation DeleteCreatedOfficeTypes {
dictionaryPacket {
d1: deleteOfficeType(id: "office-type-xray")
d2: deleteOfficeType(id: "office-type-laboratory")
d3: deleteOfficeType(id: "office-type-ultrasound")
}
}

Пример 4: Запросы справочных данных

query CheckDictionaries {
doctorTypes: searchDoctorType {
elems { id name descr }
}
officeTypes: searchOfficeType {
elems { id name }
}
}

Строковые выражения DataSpace CE

Введение в строковые выражения

Строковые выражения в DataSpace CE — это мощный механизм для создания динамических условий фильтрации, сортировки и вычислений в GraphQL запросах. Они позволяют создавать гибкие и выразительные запросы, которые могут адаптироваться к различным сценариям использования.

Строковые выражения используются в параметрах cond (условие фильтрации), sort (критерии сортировки) и других местах, где требуется динамическое вычисление значений. Они основаны на собственном языке выражений DataSpace, который поддерживает работу с различными типами данных и сложными условиями.

Текущий элемент (it)

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

  • Искомую сущность при поиске
  • Элемент коллекции при фильтрации
  • Текущий объект в условиях

Пример использования в поиске персон:

query searchPersonByFirstName {
searchPerson(cond: "it.firstName == 'Иван'") {
elems {
id
firstName
lastName
}
}
}

В качестве результата выполнения данного запроса будет возвращен список персон, у которых поле firstName равно 'Иван'.

Пример фильтрации по дате рождения:

query searchPersonByAge {
searchPerson(cond: "it.birthDate >= D1980-01-01 && it.birthDate <= D1990-12-31") {
elems {
id
firstName
lastName
birthDate
}
}
}

В качестве результата выполнения данного запроса будет возвращен список персон, у которых поле birthDate находится в диапазоне с 1 января 1980 года по 31 декабря 1990 года.

Также в данном запрос стоит обратить внимание на логическую связку && (И). Помимо нее в строковых выражениях можно использовать логическую связку || (ИЛИ) и ! (НЕ). Это позволяет создавать более сложные условия фильтрации.

Пример фильтрации по нескольким условиям:

query searchPersonByFirstNameAndLastName {
searchPerson(cond: "it.firstName == 'Иван' && it.lastName == 'Петров'") {
elems {
id
firstName
lastName
}
}
}

В качестве результата выполнения данного запроса будет возвращен список персон, у которых поле firstName равно 'Иван' и поле lastName равно 'Петров'.

Типы данных и литералы

DataSpace CE поддерживает различные типы данных в строковых выражениях:

Строки

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

'Иван'

Числа

42          # Целое число
3.14 # Вещественное число

Даты и время

Используется префикс D для дат и времени:

D2024-01-15                   # Дата
D2024-01-15T10:30:00 # Дата и время
D2024-01-15T10:30:00+03:00 # Дата и время со смещением
T10:30:00 # Время

Логические значения

true
false

Специальные значения

null        # Пустое значение
now # Текущее время

Операторы сравнения

Базовые операторы

==          # Равенство
!= # Неравенство
> # Больше
>= # Больше или равно
< # Меньше
<= # Меньше или равно

Специальные операторы

Оператор LIKE ($like) Используется для поиска по шаблону с поддержкой подстановочных символов:

$like       # Поиск по шаблону (% - любая последовательность, _ - любой символ)

Пример поиска персон по имени:

query searchPersonByPattern {
searchPerson(cond: "it.firstName $like 'Ив%'") {
elems {
id
firstName
lastName
}
}
}

Данный запрос вернет список персон, у которых поле firstName начинается с буквы 'Ив'.

Оператор IN ($in) Проверяет принадлежность значения к множеству:

Пример поиска персон по нескольким именам:

query searchPersonByNames {
searchPerson(cond: "it.firstName $in ['Иван', 'Петр', 'Анна']") {
elems {
id
firstName
lastName
}
}
}

Данный запрос вернет список персон, у которых поле firstName равно 'Иван', 'Петр' или 'Анна'.

Оператор BETWEEN ($between) Проверяет попадание значения в диапазон:

Пример поиска расписания в диапазоне дат:

query searchScheduleInRange {
searchDoctorSchedule(
cond: "it.beginDate $between (D2024-01-01T00:00:00, D2024-01-31T23:59:59)"
) {
elems {
id
beginDate
endDate
}
}
}

Данный запрос вернет список расписаний, у которых поле beginDate находится в диапазоне с 1 января 2024 года по 31 января 2024 года.

Логические операторы

&&          # Логическое И
|| # Логическое ИЛИ
! # Логическое НЕ (отрицание)

Пример комбинированного условия 1

query searchActiveSchedule {
searchDoctorSchedule(
cond: "it.beginDate >= now && it.endDate <= now.$addDays(7)"
) {
elems {
id
beginDate
endDate
clinicDoctor {
entity {
doctor {
entity {
person {
entity {
firstName
lastName
}
}
}
}
}
}
}
}
}

Давайте разберем данный запрос подробнее. Выражение it.beginDate >= now проверяет, что дата начала приема больше или равна текущей дате. Выражение it.endDate <= now.$addDays(7) проверяет, что дата окончания приема меньше или равна текущей дате плюс 7 дней. Выражение && означает логическое И, то есть условие будет выполняться только если оба условия будут выполняться одновременно. Выражение now означает текущую дату и время. Выражение $addDays(7) означает добавление 7 дней к текущей дате.

Методы работы с данными

Методы строк

$upper      # Приведение к верхнему регистру
$lower # Приведение к нижнему регистру
$length # Длина строки
$trim # Удаление пробелов

Пример поиска без учета регистра:

query searchPersonCaseInsensitive {
searchPerson(cond: "it.firstName.$lower == 'иван'") {
elems {
id
firstName
lastName
}
}
}

Данный запрос вернет список персон, у которых поле firstName равно 'иван' в нижнем регистре.

Пример запрос с длиной строки:

query searchPersonByLength {
searchPerson(cond: "it.firstName.$length == 5") {
elems {
id
firstName
lastName
}
}
}

Данный запрос вернет список персон, у которых поле firstName имеет длину 5 символов.

Методы работы с датами

$year       # Получить год
$month # Получить месяц
$day # Получить день
$addDays(n) # Добавить дни
$addMonths(n) # Добавить месяцы

Пример поиска записей текущего месяца:

query searchCurrentMonthSchedule {
searchDoctorSchedule(
cond: "it.beginDate.$year == now.$year && it.beginDate.$month == now.$month"
) {
elems {
id
beginDate
endDate
}
}
}

Данный запрос вернет список расписаний, у которых поле beginDate находится в текущем месяце.

Методы работы с коллекциями

$exists     # Проверка существования элементов
$count # Количество элементов
$sum # Сумма элементов
$min # Минимальное значение
$max # Максимальное значение

Пример поиска клиник с врачами:

query searchClinicsWithDoctors {
searchClinic(
cond: "entities{type = ClinicDoctor, cond = it.clinic.entityId == root.id}.$exists"
) {
elems {
id
name
}
}
}

Данный запрос вернет список клиник, у которых есть врачи.

Работа с переменными в строковых выражениях

Одной из мощных возможностей DataSpace CE является использование переменных GraphQL внутри строковых выражений. Это позволяет создавать динамические запросы, которые могут адаптироваться к различным параметрам поиска, передаваемым клиентом.

Синтаксис использования переменных

Переменные в строковых выражениях используются с помощью синтаксиса ${variableName}:

"it.fieldName == ${myVariable}"

Директива @strExpr

При использовании переменных в строковых выражениях необходимо применять директиву @strExpr. Эта директива сообщает парсеру GraphQL, что переменная используется внутри строкового выражения, что позволяет избежать ошибок валидации.

Синтаксис директивы:

query myQuery($variable: String) {
searchEntity(cond: "it.field == ${variable}")
@strExpr(string: $variable) {
elems { id }
}
}

Комплексный пример с переменными 1

Рассмотрим практический пример поиска по медицинской модели с использованием переменных:

query searchPersonsWithVariables(
$namePattern: String
$birthYearFrom: Int
$offset: Int
$limit: Int
) {
searchPerson(
cond: "it.firstName.$lower $like ${namePattern}.$lower + '%' && it.birthDate.$year >= ${birthYearFrom}"
offset: $offset
limit: $limit
sort: {crit: "it.lastName", order: ASC}
)
@strExpr(string: $namePattern, int: $birthYearFrom) {
elems {
id
firstName
lastName
birthDate
}
}
}

Переменные запроса:

{
"namePattern": "Ив",
"birthYearFrom": 1980,
"offset": 0,
"limit": 10
}

Фильтрация и сортировка

GraphQL в DataSpace CE поддерживает мощные возможности фильтрации и сортировки. Следующий запрос возвращает ближайшие 10 расписаний приемов врача, начиная с указанной даты. Записи сортируются от новых к старым, выводятся идентификатор, начало и конец каждого приема.

query myFirstQuery {
searchDoctorSchedule(
cond: "it.clinicDoctor.entityId == 'doctor1@mail.ru' && it.beginDate >= D2024-01-01"
sort: {crit:"it.beginDate", order: DESC}
limit:10
offset:0
) {
elems{
id
beginDate
endDate
}
}
}

Данный запрос вернет ближайшие 10 расписаний приемов врача, начиная с 1 января 2024 года. Записи сортируются от новых к старым.

Комплексный пример с переменными 2

query myFirstQuery(
$clinicDoctorId: String!
$beginDate: _DateTime!
) {
searchDoctorSchedule(
cond: "it.clinicDoctor.entityId == ${clinicDoctorId} && it.beginDate >= ${beginDate}"
sort: {crit:"it.beginDate", order: DESC}
limit:10
offset:0
) @strExpr(string:$clinicDoctorId, dateTime:$beginDate){
elems{
id
beginDate
endDate
}
}
}

Переменные запроса:

{
"clinicDoctorId": "doctor1@mail.ru",
"beginDate": "2024-01-01T00:00:00.000"
}

Данный запрос вернёт ближайшие 10 расписаний приёмов врача, начиная с 1 января 2024 года. Записи сортируются от новых к старым. В запросе используются переменные, которые передаются в запрос в виде параметров. Это позволяет избежать жёсткой привязки к конкретным значениям и делает запрос более гибким и универсальным. Также в запросе используется строковое выражение DataSpace CE, которое позволяет использовать переменные в условии фильтрации.

Unit of work

Unit of work - это концепция, которая позволяет группировать несколько операций над данными в одну транзакцию. Это позволяет обеспечить атомарность и согласованность данных.

Формирование расписания врача без проверки на пересечение графиков по врачам и кабинетам:

   mutation createDoctorSchedule($input: _CreateDoctorScheduleInput!) {
packet {
createDoctorSchedule(input: $input) {
id
beginDate
}
}
}
{
"input": {
"beginDate": "2025-06-23T09:00:00Z",
"endDate": "2025-06-23T13:00:00Z",
"clinicSchedule": "clinic-schedule-3",
"clinicDoctor": {
"entityId": "clinic-doctor-1"
},
"clinicOffice": {
"entityId": "office-1"
}
}
}

Как не допустить пересечений графиков по врачам и кабинетам?

mutation createDoctorSchedule($input: _CreateDoctorScheduleInput!) {
packet {

lockClinicSchedule: getClinicSchedule(
id:`${input.clinicSchedule}`
lock:WAIT
) {id}
 
checkDoctorAndOfficeFreeSlot: getClinicSchedule(
id:`find:
it.id == ${input.clinicSchedule} &&
!it.doctorScheduleList{cond = (
it.clinicDoctor.entityId == ${input.clinicDoctor.entityId}
|| it.clinicOffice.entityId == ${input.clinicOffice.entityId}
) && it.beginDate <= ${input.endDate} && it.endDate >= ${input.beginDate}
}.$exists`
failOnEmpty:true
) {id}

createDoctorSchedule(input: $input) {id}
}
}
{
"input": {
"beginDate": "2025-06-23T09:00:00Z",
"endDate": "2025-06-23T13:00:00Z",
"clinicSchedule": "clinic-schedule-3",
"clinicDoctor": {
"entityId": "clinic-doctor-1"
},
"clinicOffice": {
"entityId": "office-1"
}
}
}

Рассмаотрим этот код по шагам:

  1. Заблокировать расписание клиники — предотвращается одновременная запись разными пользователями:
lockClinicSchedule: getClinicSchedule(id:`${input.clinicSchedule}`, lock:WAIT) {id}
  1. Проверка доступности врача и кабинета — убедиться, что врач и кабинет свободны в указанный промежуток времени:
checkDoctorAndOfficeFreeSlot: getClinicSchedule(id:`find: 
it.id == ${input.clinicSchedule} &&
!it.doctorScheduleList{cond = (
it.clinicDoctor.entityId == ${input.clinicDoctor.entityId}
|| it.clinicOffice.entityId == ${input.clinicOffice.entityId}
) && it.beginDate <= ${input.endDate} && it.endDate >= ${input.beginDate}
}.$exists`
failOnEmpty:true) {id}
  1. Создать запись врача — фактически создать новую запись врача в расписании клиники:
createDoctorSchedule(input: $input) {id}

Таким образом, этот фрагмент гарантирует согласованность расписания врачей и кабинетов, предотвращает конфликты записей и обеспечивает создание новой записи врача лишь при наличии свободных слотов.

В данном примере packet = Unit Of Work. Это означает, что все операции, которые выполняются в рамках этого пакета, будут выполняться как одна транзакция. Если в процессе выполнения одной из операций возникнет ошибка, то все изменения, которые были сделаны в рамках этого пакета, будут отменены. Таким образом, мы обеспечиваем согласованность данных.

Заключение

В данном разделе мы рассмотрели основы работы с GraphQL и его синтаксис. Мы узнали, как работать с запросами, мутациями, пакетами, переменными и фрагментами. Также мы рассмотрели примеры сложных GraphQL-запросов и фильтрации и сортировки данных, а также работу со строковыми выражениями. В следующих разделах мы наполним базу данных демо-данными и создадим React-приложение с использованием автоматической кодогенерации.

Ссылки