Адаптеры баз данных
Изучите контракт хранения данных совместной работы и выберите Memory, SQLite или собственный Database Adapter.
Database Adapter сохраняет основные данные совместной работы, используемые Collaboration Service. Разработчик может реализовать IDatabaseAdapter для любой базы, способной выполнить полный контракт поведения. Service отвечает за семантику Unit, OT и ревизий. Adapter превращает её в атомарные операции хранения, безопасные при повторе.
Проверка сохранения после перезапуска
Остановите Quick Start и выполните pnpm example:database-adapter из корня. Откройте http://127.0.0.1:3010/?unit=persistent-sheet, измените ячейку и проверьте её в другом браузере. Остановите сервер, повторите команду и откройте тот же URL: значение должно сохраниться. Файл — examples/database-adapter/.data/collaboration.sqlite; не удаляйте его между запусками. Сервер читает существующий Unit и вызывает createUnitFromData() только при UNIT_NOT_FOUND. См. server/main.ts.
Что хранит Adapter
Основной Adapter совместной работы хранит:
- запись Unit и текущую головную ревизию;
- снимки с ревизиями;
- подтверждённые наборы изменений;
- блоки, на которые ссылаются снимки Sheet;
- записи идемпотентности1 отправок с ключом
(unitID, sid, reqId); - состояние логического удаления, восстановления и окончательного удаления Unit.
По умолчанию он не хранит пользователей, роли, ACL, каталоги, файлы и метаданные продукта. History, Thread Comment и Worktree используют собственные независимые контракты Adapter.
Обязательная семантика
IDatabaseAdapter — не просто CRUD-интерфейс. Реализация должна сохранять гарантии корректности, ожидаемые Collaboration Service:
| Контракт | Значение |
|---|---|
| Атомарное создание | Начальный снимок и блоки Sheet становятся видимыми вместе либо не становятся вовсе |
| CAS2 ревизии | Следующая отправка подтверждается лишь при совпадении головной ревизии с ожидаемой |
| Идемпотентность отправки | Повтор одного (unitID, sid, reqId) возвращает тот же определённый результат |
| Непрерывные изменения | Подтверждённые наборы изменений и ревизия Unit остаются непрерывными |
| Видимость снимков | Снимок доступен для чтения лишь после сохранения всех зависимостей |
| Атомарный жизненный цикл | Все Units пакета удаления или восстановления обрабатываются успешно либо ни одна |
| Метка окончательного удаления | Окончательно удалённый ID Unit нельзя создать заново |
Простое сопоставление методов обычным операциям чтения и записи без этой семантики конкурентности и атомарности нарушит OT и восстановление.
Memory Adapter
@univerjs-pro/collaboration-database-memory — реализация в памяти процесса для:
- модульных и интеграционных тестов;
- официальных примеров;
- временных сред разработки;
- быстрой проверки собственных Middleware или сборки клиента.
import { MemoryDatabaseAdapter } from "@univerjs-pro/collaboration-database-memory";import { UniverCollabService } from "@univerjs-pro/collaboration-service";const database = new MemoryDatabaseAdapter();const service = new UniverCollabService({ dbAdapter: database });Memory Adapter соблюдает те же контракты ревизий, CAS и идемпотентности, что и постоянные реализации, но все данные находятся в текущем процессе Node.js и теряются при его завершении.
SQLite Adapter
@univerjs-pro/collaboration-database-sqlite — постоянное хранилище SQLite для разработки, тестов, локальных приложений и небольших нагрузок.
import { mkdir } from "node:fs/promises";import { SQLiteDatabaseAdapter } from "@univerjs-pro/collaboration-database-sqlite";import { UniverCollabService } from "@univerjs-pro/collaboration-service";await mkdir("./data", { recursive: true });const database = new SQLiteDatabaseAdapter({ filename: "./data/collaboration.sqlite", busyTimeoutMs: 5_000,});const service = new UniverCollabService({ dbAdapter: database });SQLite Adapter использует внешние ключи и транзакции записи BEGIN IMMEDIATE для атомарных коммитов. В пустой базе он создаёт актуальную схему. Неполные или неподдерживаемые схемы он отказывается открывать, а не применяет неизвестные миграции автоматически.
Он не меняет journal_mode и не определяет расположение файла, политику резервного копирования и другие настройки SQLite. SQLite предназначен для разработки, локальных приложений и небольших масштабов.
Собственные Database Adapters
Собственный Adapter обычно подходит, когда:
- приложение уже использует PostgreSQL, MySQL или другую бизнес-базу;
- отправки совместной работы должны интегрироваться со специфическими механизмами базы приложения;
- эксплуатационные или нормативные требования выходят за предполагаемый масштаб SQLite.
Реализуйте IDatabaseAdapter и внедрите его в Service так же, как встроенный Adapter:
const database = new ApplicationCollaborationDatabase({ pool });const service = new UniverCollabService({ dbAdapter: database });Service не зависит от конкретной базы, но и не может добавить транзакции, CAS или дедупликацию в вашу реализацию. Adapter должен выполнить полный контракт средствами своей базы.
Context и граница приложения
Каждый вызов Service передаёт customData в Middleware, Database Adapter и связанные Events, но SDK не сохраняет его автоматически. Решения об авторизации обычно остаются в Middleware Service, а не повторяются в методах Adapter:
Middleware Service → разрешает или отклоняет бизнес-операцию → Database Adapter → атомарно сохраняет эталонное состояние совместной работыТак Memory, SQLite и собственные Adapters используют общие политики приложения.
Если бизнес-данным нужна строгая согласованность с данными совместной работы, собственный Database Adapter может прочитать значения из customData и записать оба набора в своей транзакции базы данных.
Если общая транзакция не нужна, Middleware или Events могут записать бизнес-данные в дополнительные таблицы или внешние системы. По умолчанию они не разделяют внутреннюю транзакцию Adapter. Events — локальные для процесса уведомления с доставкой по возможности, испускаемые после успешного коммита. Необходимую идемпотентность, повторы и компенсации реализует приложение.
Хранение расширений
History, Thread Comment и Worktree определяют независимые интерфейсы Database Adapter. Их Adapters могут использовать одну базу или разные хранилища. Одна собственная реализация также может предоставлять несколько интерфейсов Adapter и внедряться в соответствующие Services.
Из-за независимости интерфейсов SDK по умолчанию не предоставляет межмодульных транзакций. Даже при общей базе стратегию межмодульной согласованности определяет приложение.
Выбор Adapter
| Adapter | Подходящие задачи | Основное ограничение |
|---|---|---|
| Memory | Тесты, примеры, временная разработка | Данные исчезают при завершении процесса |
| SQLite | Разработка, тесты, локальные приложения, небольшой масштаб | Однофайловая база с настройками под контролем приложения |
| Собственный | Существующие базы или особая интеграция бизнес-данных | Приложение реализует и проверяет полный контракт |
Приложение владеет всеми внедрёнными Adapters. Освободите Service раньше Adapter:
await service.dispose();await database.dispose();Сравнить работающие реализации можно в примере Database Adapter.
Footnotes
-
Идемпотентность позволяет повторить отправку без повторного применения изменений. Сохраняйте идентификатор отправки: те же данные с новым идентификатором считаются новой отправкой. ↩
-
CAS обновляет значение, только если оно совпадает с ожидаемым. Проверка текущей ревизии и запись следующего состояния должны быть атомарными, чтобы два параллельных запроса не подтвердили одну ревизию. ↩
Насколько полезно это руководство?