Файлы вместо базы данных: хранилище для сайта, который правит один человек
Контент сайта лежит в JSON-файлах, а не в PostgreSQL. Разбор решения: чем платишь, что получаешь и где эта схема ломается.
Обычный ответ на вопрос «где хранить контент сайта» — база данных. Я выбрал каталог JSON-файлов и до сих пор считаю это правильным решением. Ниже — из чего складывался выбор и в какой момент он перестанет работать.
Что именно решалось
Сайт правит один человек — я. Правок в день — единицы. Объём — десятки документов. При таких числах база данных приносит не производительность, а зависимость: драйвер, который надо собрать и положить в релиз; миграции, которые надо писать и откатывать; процесс, который надо мониторить и бэкапить отдельно от файлов.
Релиз, который крутится на сервере, не тянет ни одной внешней зависимости — только собранный бандл. Добавить туда драйвер базы означало сломать это свойство ради задачи, которой нет.
Как это устроено
Один документ — один файл. Страница, кейс, пост — каждый лежит отдельно, назван своим адресом. Создать сущность значит записать файл, удалить — удалить файл. Списка, который надо держать в согласии с каталогом, не существует: каталог и есть список.
Чтение кэшируется в памяти процесса. Сайт — один процесс Node, поэтому кэш это обычный Map, а сбрасывает его только запись через тот же модуль.
1export async function writePost(slug: string, post: Post) {
2 if (!looksLikeSlug(slug)) throw new Error("bad slug");
3 await mkdir(postsDir(), { recursive: true });
4
5 // Что пост говорит сейчас — сохранить перед тем, как записать поверх.
6 const previous = await readPostFile(slug);
7 if (previous) await keepVersion("posts", slug, previous);
8
9 const file = postFile(slug);
10 const temporary = `${file}.${process.pid}.tmp`;
11 await writeFile(temporary, `${JSON.stringify(post, null, 2)}\n`, "utf8");
12 await rename(temporary, file);
13
14 cache.set(`post:${slug}` as DocumentId, post);
15}
Самая важная деталь: слияние с кодом
Файл на диске накладывается на копию из репозитория, а не заменяет её. Код обрастает полями — это нормальный способ развития сайта, — и документ, сохранённый до появления поля, должен получить это поле пустым, а не остаться без него.
Списки при этом берутся целиком. Слитый список не был бы ни старым, ни новым: представьте меню, где к четырём сохранённым пунктам добавились два из кода. Поэтому меню, однажды сохранённое в панели, живёт своей жизнью — и об этом надо помнить, добавляя пункт в код.
| Что теряешь | Что получаешь |
|---|---|
| Запросы и выборки | Бэкап копированием каталога |
| Одновременная запись | Откат — вернуть предыдущий файл |
| Поиск по контенту | Ноль зависимостей в релизе |
| Много авторов | Diff читается глазами |
Когда схема сломается
Три условия, каждого достаточно. Появится второй редактор — две одновременные записи в один файл, и последний выигрывает молча. Понадобится поиск по контенту — обходить каталог станет дорого. Документов станет тысячи — чтение каталога на каждый запрос перестанет быть бесплатным.
Ни одно из трёх не наступило, а переехать на базу из файлов проще, чем обратно.
