MongoDB или PostgreSQL для Payload CMS: когда сложные блоки начинают влиять на выбор базы
Payload CMS официально поддерживает и PostgreSQL, и MongoDB, причём на уровне самой CMS разница может быть почти незаметной: коллекции, поля, relationships, локализация и Blocks работают с обеими базами. Но чем сложнее становится структура контента, тем сильнее различается то, что происходит под Payload.
Свежая дискуссия в сообществе началась как раз с такого случая: разработчик, который обычно использует PostgreSQL, перевёл часть проектов Payload на MongoDB. Причины — постоянные изменения схемы в начале разработки и большое количество вложенных Blocks. После перехода на blocksAsJSON проблемы с миграциями, по его словам, тоже никуда не делись.
Обсуждение в r/PayloadCMS: https://www.reddit.com/r/PayloadCMS/comments/1xftitc/database_choice_mongo_vs_postgres/
Интересно здесь не выяснять, какая база «лучше». Сам Payload рекомендует выбирать базу исходя из структуры данных конкретного проекта. И сложные блочные страницы — один из случаев, когда этот выбор действительно начинает иметь значение.
Почему Blocks хорошо ложатся на MongoDB
Представим обычную страницу на Payload:
Page
├── title
├── slug
└── layout
├── Hero
│ ├── heading
│ ├── image
│ └── buttons[]
├── Text
│ └── content
├── Gallery
│ └── images[]
└── Products
└── products[]Для Payload это один документ Page.
MongoDB примерно так его и хранит: документ остаётся вложенной структурой, близкой к той, которую разработчик описал в Payload Config. Количество массивов, Blocks и локализованных полей не требует превращать каждый уровень вложенности в отдельную реляционную структуру.
Именно поэтому официальная документация Payload прямо перечисляет аргументы в пользу MongoDB: много динамических полей, много Arrays и Blocks, локализованный контент, а также нежелание синхронизировать production- и staging-базы через DDL-изменения.
Для CMS это довольно естественная модель: страница вместе со своими блоками во многих случаях действительно является единым объектом.
В PostgreSQL картина другая
PostgreSQL хранит данные реляционно. Payload использует Drizzle ORM и строит схему базы на основе конфигурации CMS.
На относительно плоской модели это удобно:
posts
authors
categories
products
ordersОсобенно когда между сущностями много отношений и хочется получить ограничения целостности непосредственно на уровне базы.
Но сложный конструктор страниц приводит к заметно более разветвлённой реляционной схеме: Blocks, массивы, локализованные значения, relationships и версии должны каким-то образом отображаться на таблицы и связи. Большая Payload Config может породить гораздо более сложную физическую схему, чем кажется при взгляде на TypeScript.
Само по себе это не проблема: с этими таблицами напрямую разработчик вообще может никогда не работать.
Заметной сложность становится тогда, когда структура контента постоянно меняется.
Откуда берутся миграции
Допустим, сегодня блок выглядит так:
{
slug: 'hero',
fields: [
{ name: 'title', type: 'text' },
],
}Через неделю в него добавили изображение, кнопки и варианты оформления. Потом разделили один блок на два. Потом удалили старое поле.
В PostgreSQL изменение Payload Config означает изменение схемы базы.
Во время локальной разработки Payload облегчает этот процесс: Drizzle push автоматически синхронизирует development-базу с конфигурацией, так что после каждого добавленного поля создавать миграцию не нужно. Документация прямо советует такой workflow: работать в development-режиме с push, а миграцию создавать после завершения очередного набора изменений.
Но production уже обновляется через миграции:
Payload Config
↓
migration
↓
PostgreSQL schemaИ чем активнее меняется большая блочная схема, тем больше внимания требует этот процесс.
В MongoDB добавление поля в конфигурацию само по себе не требует DDL-миграции: старый документ может просто не содержать нового поля. Документация Payload формулирует это так: в реляционных базах миграции обязательны для окружений вне development, а с MongoDB миграции могут понадобиться изредка — или никогда.
Миграция всё же потребуется, когда существующие данные необходимо преобразовать из старой структуры в новую. Это принципиально другая модель разработки.
Но у MongoDB есть обратная сторона
Именно это стало главным возражением в обсуждении.
Если база не заставляет все документы соответствовать жёсткой схеме, со временем в ней могут одновременно жить документы разных поколений:
{
"title": "Page",
"hero": {
"image": "..."
}
}и:
{
"title": "Page",
"hero": {
"desktopImage": "...",
"mobileImage": "..."
}
}Payload контролирует структуру данных на уровне приложения, но MongoDB не превращает изменение Payload Config в обязательную перестройку всех существующих документов.
Поэтому отсутствие DDL-миграций не означает отсутствия миграций как инженерной задачи. Если старые данные нужно привести к новой форме, это всё равно придётся сделать — просто в MongoDB разработчик сам решает, когда такое преобразование действительно необходимо. Именно так описывает миграции для MongoDB и официальная документация.
Получается компромисс: PostgreSQL заставляет заниматься изменениями схемы раньше, MongoDB позволяет отложить часть этой работы — но повышает риск накопить документы разной формы. Один из комментаторов дискуссии формулирует ровно это: с Mongo легко получить «кучу искажённых объектов», которые либо ломаются, либо требуют специальной обработки в коде.
Что такое blocksAsJSON
У PostgreSQL-адаптера Payload есть промежуточный вариант — blocksAsJSON:
postgresAdapter({
blocksAsJSON: true,
})Вместо реляционного представления Blocks Payload хранит их в JSON-колонке PostgreSQL. Документация прямо указывает, что такой режим может улучшить производительность проектов с большим количеством блоков.
Архитектурно это своего рода компромисс:
PostgreSQL
pages
├── id
├── title
├── slug
└── layout JSON
├── Hero
├── Text
├── Gallery
└── ...Основные сущности проекта остаются реляционными, relationships продолжают использовать преимущества PostgreSQL, но большое дерево блоков не раскладывается на множество таблиц.
Именно этот режим особенно интересен для крупных сайтов на Payload.
У blocksAsJSON тоже есть цена
JSON внутри PostgreSQL уже не обладает всеми свойствами обычной реляционной схемы.
Например, в текущей документации Payload отдельно предупреждают: запросы к JSON, Rich Text и blocksAsJSON не проходят через пользовательские query.operatorHandlers PostgreSQL-адаптера. Готовый пресет postgresUnaccent, делающий поиск без учёта диакритики, на такие поля не действует.
Кроме того, перевод существующего проекта на blocksAsJSON сам по себе требует преобразования данных.
Для этого Payload ещё в версии 3.73.0 добавил готовую предопределённую миграцию:
pnpm payload migrate:create \
--file @payloadcms/db-postgres/blocks-as-jsonОна предназначена именно для перевода существующей реляционной структуры Blocks в JSON-представление, а механизм --file в CLI Payload загружает такие готовые миграции прямо из пакетов @payloadcms/db-*.
Поэтому жалобу из свежего обсуждения на проблемы миграций с blocksAsJSON не стоит трактовать как доказанную проблему Payload: автор не привёл воспроизводимого примера, версии и деталей в треде нет. Но сам выбор между реляционным и JSON-хранением Blocks — реальный, и цена у этого выбора есть.
Когда MongoDB выглядит логичнее
Условный редакционный сайт может состоять почти целиком из документов:
Pages
Articles
Authors
MediaПри этом Pages содержат большой page builder из десятков типов Blocks, много вложенных массивов и локализованного контента.
Для такого проекта MongoDB хорошо соответствует самой модели данных — и документация Payload прямо называет перечисленные признаки аргументами в её пользу.
Особенно это удобно на раннем этапе проекта, когда структура страниц ещё активно меняется.
А когда PostgreSQL выглядит естественнее
Теперь другая система:
Users
Customers
Orders
OrderItems
Products
Warehouses
Payments
InvoicesЗдесь гораздо важнее отношения между сущностями: заказ принадлежит клиенту, позиция заказа — существующему товару, платёж — заказу. Нужна строгая целостность данных, а структура сущностей относительно стабильна.
Это естественная территория реляционной базы, и рекомендация Payload примерно такая же: PostgreSQL (или SQLite) стоит предпочитать, когда нужна целостность на уровне базы и в проекте много отношений между коллекциями.
При этом наличие нескольких Blocks на страницах совсем не означает, что от PostgreSQL нужно отказываться.
Третий вариант для многих проектов
Интереснее всего выглядит комбинация PostgreSQL + blocksAsJSON.
Основные сущности остаются реляционными:
users
products
categories
orders
pagesа внутри pages гибкая структура конструктора хранится как JSON. Это позволяет не превращать каждую деталь редакционной страницы в часть большой реляционной схемы, сохраняя PostgreSQL там, где его свойства действительно полезны.
По сути, разработчик разделяет два типа данных, которые часто живут внутри одного проекта Payload:
- структурированные бизнес-данные — relational;
- гибкий редакционный контент — JSON.
Для многих крупных проектов это может оказаться практичнее, чем выбирать базу исключительно по принципу «MongoDB или PostgreSQL».
Что выбрать для нового проекта
У Payload нет универсальной рекомендации использовать одну конкретную базу, и это принципиальная позиция документации: обе модели официально поддерживаются, а выбор должен зависеть от характера данных.
Если проект преимущественно контентный, схема часто меняется, много локализации, массивов и сложных Blocks — MongoDB действительно заслуживает рассмотрения.
Если Payload используется как основа приложения с большим количеством связанных сущностей и важна целостность на уровне базы — аргументы сильнее у PostgreSQL.
А если бизнес-данные реляционные, но страницы представляют собой огромные деревья Blocks, стоит отдельно посмотреть на blocksAsJSON.
Свежая дискуссия интересна именно этим. Она показывает: выбор базы для Payload определяется не размером проекта и даже не производительностью PostgreSQL или MongoDB в вакууме.
Гораздо важнее посмотреть на Payload Config и понять, какие данные вы на самом деле собираетесь в этой базе хранить.
Правило: выбор базы для Payload начинайте не с «что моднее», а с Payload Config: контентные деревья Blocks и локализация тянут к MongoDB (или blocksAsJSON в PostgreSQL), связанные бизнес-сущности и целостность — к PostgreSQL.
Источники
Обсуждение «Database choice: Mongo vs Postgres» в r/PayloadCMS: https://www.reddit.com/r/PayloadCMS/comments/1xftitc/database_choice_mongo_vs_postgres/
Документация Payload — Database Overview (когда предпочитать MongoDB, когда реляционную базу): https://payloadcms.com/docs/database/overview
Документация Payload — Postgres Adapter (blocksAsJSON, ограничения query.operatorHandlers): https://payloadcms.com/docs/database/postgres
Документация Payload — Migrations (MongoDB против реляционных баз, Drizzle push в dev): https://payloadcms.com/docs/database/migrations
Релиз Payload v3.73.0 с предопределённой миграцией blocksAsJSON: https://github.com/payloadcms/payload/releases/tag/v3.73.0