Концепции, идеи, стратегии
Концепции, идеи, стратегииМаппинг схемы GraphQL для вашего сайта, темы или плагина WordPress

Маппинг схемы GraphQL для вашего сайта, темы или плагина WordPress

Итак, вы решили начать использовать GraphQL для вашего существующего сайта на WordPress. Отлично! Независимо от того, применяется ли GraphQL для новой или существующей функциональности, ему потребуется взаимодействовать с базовым уровнем данных. Для этого вам нужно будет отобразить модель данных вашего приложения (будь то кастомный PHP-код в вашем сайте WordPress, теме или плагине) в схему GraphQL.

Как следует выполнять маппинг? Нужно ли делать это всё и сразу? Должен ли он быть точной копией существующей модели данных? Что насчёт исправления неподходящих имён в процессе? А что касается технического долга — стоит ли его сохранять или решать?

Давайте рассмотрим несколько стратегий маппинга модели данных существующего приложения WordPress в схему GraphQL.

Создавайте схему в своём собственном темпе

Добавление GraphQL в приложение — это не всё или ничего. Одно и то же приложение может одновременно работать через несколько API, и в этом случае GraphQL будет существовать рядом с другими API столько, сколько потребуется. Например, можно оставить существующую функциональность на REST, а GraphQL использовать только для новой функциональности.

Если вы хотите полностью перейти на GraphQL, это не обязательно делать всё сразу. Существующую функциональность можно медленно, но уверенно переводить на GraphQL, пока однажды GraphQL не станет единственным API в приложении.

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

Не перекладывайте бремя реализации на интерфейс

Сервер GraphQL будет реализовывать логику доступа к данным приложения. Он будет делать это, вызывая функциональность WordPress, например вызывая get_posts для получения данных записей. На этом уровне существует PHP-код для удовлетворения резолверов.

Однако схема GraphQL — это интерфейс: она объявляет контракты для доступа к данным в API. Её не интересуют детали реализации: она ничего не знает о WordPress, функции get_posts, таблице БД wp_posts или SQL-запросах.

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

Это важно, поскольку модель данных зачастую будет запятнана своей реализацией. WordPress даёт яркий пример этого с CPT "attachment", предназначенным для представления медиафайлов, таких как изображения.

Поскольку это Custom Post Type, изображение обрабатывается как запись. Тогда у нас может возникнуть соблазн представить медиафайлы с помощью типа Post, который содержит следующие поля:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
}

Но это может быть неуместно для приложения. Смысл поля «content» ясен для записи, но не для изображения. Скорее всего, оно там не должно быть.

Изображение было смоделировано как CPT в WordPress потому, что это было удобно: так можно повторно использовать существующую логику и хранить данные в существующей таблице wp_posts.

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

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

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

Применим эту идею на практике. Вместо того чтобы тип Post представлял медиафайлы, логичнее иметь тип Media, содержащий только те свойства, которые действительно имеют смысл для сущности «изображение»:

type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Под капотом, на уровне реализации, field resolver по-прежнему будет выполнять функцию get_posts для разрешения записей типа Media, но схему GraphQL это не касается.

Отделяйте схему GraphQL от диаграммы БД

WordPress реализован поверх этой диаграммы сущность-связь БД:

Диаграмма сущность-связь БД в WordPress

Схема GraphQL должна основываться на диаграмме БД, но мы не должны пытаться создать её точную копию один к одному. Это связано с тем, что и схема GraphQL, и диаграмма БД строятся с определёнными предусловиями или ограничениями, которые не применимы к другой стороне.

В предыдущем разделе показан пример, где таблица wp_posts хранит данные CPT изображения, но в GraphQL будет два отдельных типа: Post и Media.

Рассмотрим ещё один пример: категории. В WordPress у записи может быть одна или несколько категорий, и любой CPT также может создать свою собственную категорию. Например, CPT с именем «event» будет иметь «event_category».

Как категории записей, так и категории событий хранятся в таблице wp_terms. Это упрощает для WordPress получение строк из того или иного типа категорий при выполнении SQL-запроса.

Поэтому у нас может возникнуть соблазн отобразить категории через тип Category, на который ссылаются и записи, и события:

type Category {
  id: ID!
  name: String!
}
 
type Post {
  categories: [Category]!
}
 
type Event {
  categories: [Category]!
}

Однако запись всегда будет содержать категории записей, а событие — всегда категории событий. Данные этих двух типов категорий могут храниться в одной и той же таблице БД, но на уровне приложения они не будут смешиваться. Категория записи и категория события — это две разные сущности.

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

В данном случае при маппинге категорий в схему GraphQL следует создать отдельный тип для каждой из них: PostCategory и EventCategory. Тогда тип Post будет ссылаться только на PostCategory, а тип Event — только на EventCategory:

type PostCategory {
  id: ID!
  name: String!
}
 
type Post {
  categories: [PostCategory]!
}
 
type EventCategory {
  id: ID!
  name: String!
}
 
type Event {
  categories: [EventCategory]!
}

Если мы всё же хотим иметь в схеме сущность, объединяющую все категории, это можно сделать через интерфейс Category:

interface Category {
  name: String!
}
 
type PostCategory implements Category {
  id: ID!
  name: String!
}
 
type EventCategory implements Category {
  id: ID!
  name: String!
}

Таким образом, пользователи, обращающиеся к API, будут чётко понимать, какие данные будут получены, независимо от того, как они отображены в диаграмме БД и как хранятся в БД.

После того как у нас есть финальная схема GraphQL, мы можем увидеть, что её структура будет в чём-то напоминать диаграмму БД WordPress, но при этом явно отличаться от неё:

Схема GraphQL

Адаптируйте именование полей, следуя статической типизации

Поля должны, насколько возможно, сохранять то же именование, что и в приложении.

Например, мы можем создать запись с помощью функции wp_insert_post, и запись имеет свойства «title» и «content». Эти имена также подходят для схемы GraphQL (хотя могут потребовать незначительных изменений), поэтому их следует оставить:

type MutationRoot {
  insertPost(title: String, content: String): Post
}
 
type Post {
  id: ID!
  title: String
  content: String
}

Но так бывает не всегда. Как мы видели ранее, кастомные записи должны быть выделены в собственные сущности. Тогда, в то время как функция get_posts получает список любого CPT, эквивалентное поле posts в корневом типе схемы будет получать только сущности типа Post, но не Page (которая тоже является CPT):

type QueryRoot {
  posts: [Post]!
}

Как же тогда получить список всех записей и страниц? Через ещё одно поле — customPosts, которое получает сущности любого CPT, отображённого в union-тип CustomPostUnion:

union CustomPostUnion = Post | Page
 
type QueryRoot {
  customPosts: [CustomPostUnion]!
}

Важный вывод таков: именование, которое мы выбираем для схемы GraphQL, должно соответствовать типу получаемой сущности. И из-за строгой типизации GraphQL этот тип может отличаться на уровнях приложения и API.

В данном случае, хотя в WordPress «post» может означать любой «custom post type», в GraphQL «post» — это обязательно Post. Если поле получает кастомные записи, то поле в схеме GraphQL должно называться customPosts, а не posts. Аналогично, если input получает ID кастомной записи, он должен называться customPostID, а не postID.

Маппинг для поля customPosts

Этот принцип применяется, например, к комментариям. Комментарий можно добавить к любому CPT, а не только к записям. Поэтому тип Comment должен чётко это отражать, содержа поле customPost (а не post):

type Comment {
  id: ID!
  customPost: CustomPostUnion!
}

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

Типы перечислений по соглашению определяются в верхнем регистре. Например, документация graphql.org приводит следующий пример:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

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

Для примера: записи в WordPress имеют свойство «status», которое содержит одно из следующих значений:

  • "publish"
  • "pending"
  • "draft"
  • "trash"

При отображении этого свойства в схеме поле Post.status могло бы возвращать String, вот так:

type Post {
  status: String!
}

Однако, поскольку статус обязательно будет одним из этих заранее определённых значений и никаким другим, лучше отобразить его как enum:

enum Status {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}
 
type Post {
  status: Status!
}

Теперь у нас может возникнуть проблема: enum PUBLISH будет преобразован в строковое значение "PUBLISH" в приложении, а не "publish".

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

// Это получит все записи, а не только опубликованные
$published_posts = get_posts([
  "post_status" => "PUBLISH",
]);

В этом случае можно рассмотреть компромисс между соглашением и удобством: по-прежнему использовать enum для отображения констант, но в нижнем регистре:

enum Status {
  publish
  draft
  pending
  trash
}

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