Реализация системы аудита изменений данных
Задача: необходимо отслеживать, кто и когда создаёт, изменяет или удаляет сущности в системе. Текущее решение, при котором поля для аудита (createdBy, editedBy, deletedBy и соответствующие временные метки) добавляются в каждую модель, ведёт к значительному дублированию кода и сложностям в поддержке.
Рассмотрим текущую схему Prisma для модели UserAccount в качестве примера проблемы:
model UserAccount {
id Int @id @unique @default(autoincrement()) @map("id")
creatorAccount UserAccount? @relation("CreatorAccount", fields: [createdBy], references: [id])
editorAccount UserAccount? @relation("EditorAccount", fields: [editedBy], references: [id])
deleterAccount UserAccount? @relation("DeleterAccount", fields: [deletedBy], references: [id])
isDeleted Boolean @default(false) @map("is_deleted")
deletedBy Int @map("deleted_by")
deletedAt DateTime? @map("deleted_at") @db.Timestamptz()
deletionReason String? @map("deletion_reason") @db.VarChar(128)
createdUsers UserAccount[] @relation("CreatorAccount")
editedUsers UserAccount[] @relation("EditorAccount")
deletedUsers UserAccount[] @relation("DeleterAccount")
createdBy Int? @map("created_by")
createdAt DateTime @map("created_at") @db.Timestamptz()
editedBy Int? @map("edited_by")
editedAt DateTime? @map("edited_at") @db.Timestamptz()
@@map("user_accounts")
}Перенос логики аудита в отдельную структуру данных выглядит оптимальным решением. Рассмотрим возможные архитектурные варианты на уровне базы данных.
Варианты архитектуры для истории изменений
Вариант 1: Отдельная таблица истории для каждой основной таблицы
- Преимущества: Максимально удобна для запросов и анализа данных, типобезопасность, высокая производительность для операций, связанных с одной сущностью.
- Недостатки: Наименее компактное решение, сложность поддержки (при добавлении новой таблицы необходимо создавать для неё таблицу аудита), дублирование структуры.
Вариант 2: Одна общая таблица для аудита
- Структура: Содержит поля для идентификации сущности (например,
tableName,recordId), типа операции (create, update, delete), данных пользователя и меток времени. Данные до и после изменения хранятся в полях JSON (например,oldData,newData). - Преимущества: Компактность, централизованное управление, лёгкость добавления аудита для новых сущностей без изменения схемы БД.
- Недостатки: Сложнее в написании запросов для извлечения истории конкретной сущности, потеря типобезопасности данных в JSON-полях, потенциально меньшая производительность.
Альтернативный подход: Массив версий внутри записи
- Предполагает хранение истории изменений в виде массива JSON-объектов непосредственно в поле основной записи.
- Преимущества: История всегда "привязана" к записи, простота выборки.
- Недостатки: Раздувание основной таблицы, сложность выборки по данным внутри истории, ограничения на размер записи.
Вопрос к сообществу: какой подход является наиболее распространённым и оптимальным на практике для подобных задач?
Контекст: Решение ищется на уровне архитектуры базы данных.