Реализация системы аудита изменений данных

Задача: необходимо отслеживать, кто и когда создаёт, изменяет или удаляет сущности в системе. Текущее решение, при котором поля для аудита (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-объектов непосредственно в поле основной записи.
  • Преимущества: История всегда "привязана" к записи, простота выборки.
  • Недостатки: Раздувание основной таблицы, сложность выборки по данным внутри истории, ограничения на размер записи.

Вопрос к сообществу: какой подход является наиболее распространённым и оптимальным на практике для подобных задач?

Контекст: Решение ищется на уровне архитектуры базы данных.