Перейти к содержанию

Авторы и разделы как связи

Оглавление серии

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

После урока статья «Сетка карточек CSS Grid» будет связана с вымышленным автором Анной Лесной и разделом «Вёрстка». Исходная Article из второго урока сохраняет title, slug, summary, body и level. Добавляем два отношения, не меняя смысл уже введённых полей. Полные файлы Author и Category входят в архив, а Article этого шага сохранена в states/lesson-03/.

Один автор у многих статей

В нашей небольшой редакции у статьи один автор, у автора может быть несколько статей. Со стороны Article это manyToOne, со стороны Author — oneToMany. Названия описывают количество документов, а не порядок выбора в панели. Даже если сейчас у Анны одна статья, отношение остаётся рассчитанным на множество будущих статей.

Откройте Content-Type Builder и создайте коллекцию Author: API IDs author и authors. Добавьте обязательный name, необязательный bio и отношение к Article. Для справочника авторов отключим Draft & Publish, чтобы не вводить второй редакционный процесс одновременно с первым. Это учебное решение: оно не означает, что биографию всегда следует сразу показывать читателям.

Полный src/api/author/content-types/author/schema.json выглядит следующим образом.

{
  "kind": "collectionType",
  "collectionName": "authors",
  "info": {
    "singularName": "author",
    "pluralName": "authors",
    "displayName": "Author"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {},
  "attributes": {
    "name": {
      "type": "string",
      "required": true
    },
    "bio": {
      "type": "text"
    },
    "articles": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "api::article.article",
      "mappedBy": "author"
    }
  }
}

Поле articles использует mappedBy: author. Оно указывает на имя противоположного поля в Article. В Article, соответственно, author содержит inversedBy: articles. Это две стороны одного отношения; они должны согласовываться. Если назвать противоположное поле иначе, одна красивая схема автора не исправит ошибку в модели статьи.

В панели создайте Анну Лесную с биографией «Вымышленный автор Редакционного дома». Вернитесь к первой статье и выберите Анну в поле author. Ожидаемое состояние — при открытии статьи редактор видит выбранного автора, а при открытии Author может увидеть относящиеся к нему статьи. Эти результаты предназначены для вашей будущей проверки, проект в подготовке курса не запускался.

Не создавайте автора заново через поле отношения при каждом новом тексте. Прежде найдите существующий документ. Иначе появятся «Анна Лесная» и «Анна Лесная» с разными идентификаторами; совпавшие имена не объединят их автоматически. Ошибка особенно неприятна при подсчёте публикаций, когда визуально одинаковые карточки представляют разных участников модели.

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

Теперь добавим Category с title, slug и обратной связью articles. Используем один основной раздел у каждой статьи. Такой выбор позволяет строить понятный каталог: документ находится в одном месте. Если проекту нужны теги, их следует моделировать отдельно, например отношением many-to-many. Не превращайте основную рубрику в произвольный набор тегов только ради похожего выпадающего списка.

Полный src/api/category/content-types/category/schema.json входит в тот же набор.

{
  "kind": "collectionType",
  "collectionName": "categorys",
  "info": {
    "singularName": "category",
    "pluralName": "categorys",
    "displayName": "Category"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {},
  "attributes": {
    "title": {
      "type": "string",
      "required": true
    },
    "slug": {
      "type": "uid",
      "targetField": "title",
      "required": true
    },
    "articles": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "api::article.article",
      "mappedBy": "category"
    }
  }
}

Создайте категорию «Вёрстка», явно задайте slug layout и свяжите её с «Сеткой карточек CSS Grid». Отсутствие Draft & Publish у Category означает отсутствие отдельной опубликованной версии этого справочника. Однако право прочитать категорию через Content API всё равно определяется разрешениями. Состояние публикации и право доступа остаются разными механизмами.

Название «Вёрстка» предназначено человеку, layout — устойчивым ссылкам и фильтрам. Если редактор переименует раздел в «Макеты и вёрстка», адрес может остаться прежним. При изменении slug потребуется подумать о ссылках на раздел и фильтрах сайта. Простая правка title не должна вынуждать клиента менять все URL.

В Content-Type Builder проверьте обе связи Article после сохранения модели. author направлен на api::author.author, category — на api::category.category. Оба поля находятся в полном snapshot статьи этого урока. Это полезнее случайной вставки фрагмента в неизвестное место JSON: читатель может сопоставить весь набор attributes и увидеть, что прежние значения не исчезли.

Что увидит потребитель

Сайт пока не читает отношения. Даже после корректного заполнения Article REST-запрос без populate обычно не вернёт вложенный Author. Отсутствие объекта автора в ответе не обязательно означает отсутствие отношения в базе. Клиент должен запросить нужные сведения и иметь разрешение читать соответствующий тип. Позже мы ограничим вложенный ответ одним именем автора, а не всеми его полями. Форматы операций связей описаны в REST Relations.

Представим будущий ответ с author: { name: 'Анна Лесная' }. Клиент показывает имя, но не должен предполагать, что автор всегда присутствует. Временно несвязанная статья, удалённый справочник или недостаточные права могут привести к отсутствующему значению. Для учебного сайта допустим текст «Автор не указан». Для редакционного выпуска лучше заранее установить правило, что такие документы не публикуются до проверки.

Отношение также влияет на удаление. Прежде чем удалить Анну, выясните, какие статьи используют её документ и что должно произойти с подписью. Системная возможность удалить запись не равна редакционному решению убрать авторство из истории. Иногда правильнее скрыть биографию, оставить имя или передать ответственность другому автору; конкретное правило зависит от проекта, а не только от схемы данных.

Полезен ещё опыт с совпадающими заголовками статей. У Анны могут быть два текста с одинаковым title, но разными slug и documentId. Связь author остаётся корректной, поскольку она не хранит заголовок как ключ. Точно так же категория выбирается по документу, а не по строке, которую сайт показал пользователю. Если будущий импорт сопоставляет справочники только по name, ему понадобится правило разрешения дублей. Наш импортёр позже работает исключительно в пустой базе и получает идентификаторы от созданных объектов, поэтому не притворяется алгоритмом объединения уже существующей редакции.

Проведите мысленный эксперимент: создайте вторую статью «Форма обратной связи», выберите того же Author и Category, затем измените биографию Анны один раз. Связанные статьи должны продолжать обращаться к одному документу автора. При строковой модели пришлось бы искать две копии. Именно эта разница объясняет результат урока: самостоятельные объекты появляются там, где нужны собственные сведения, управление и повторное использование, а не просто потому, что поле можно сделать отношением.