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

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

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

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