Как реализуются иммутабельные записи в БД?

При реализации проекта/схемы БД небольшой/тривиальной бухгалтерской программы возникла следующая проблема. Представим себе несколько таблиц в БД (пока не обращайте внимание на типы, можно взять за пример БД Northwind от Microsoft):

+----------------+    +------------------+    +-----------+
| Product        |    | OrderDetails     |    | Order     |
+--------+-------+    +------+-----------+    +------+----+
| guid   | Id    |<-+ | guid | OrderId   |--->| guid | Id |
+--------+-------+  | +------+-----------+    +------+----+
| string | Name  |  +-| guid | ProductId |
+--------+-------+    +------+-----------+
| int    | Price |    | int  | Amount    |
+--------+-------+    +------+-----------+

Так как предполагается генерация документов на основе данных из БД, то возникает логичный вопрос: а как повторно сгенерировать документ, если информация о продукте изменилась? В этом случае (как мне представляется) есть пара вариантов:
а) после генерации документа сохранять его в файловой системе (в этом случае теряется вся информация, т.е. мы не можем гарантировать, что данные не изменились со времени генерации документа);
б) сохранять всю информацию непосредственно в БД. Не будет ли в этом случае трудно поддерживать и сопровождать БД?

Как принято поступать в таких случаях?


Добавление

Сейчас в голову пришёл ещё один вариант. Во все сущности, которые должны сохранять своё состояние добавляем свойство указывающее на текущее состояние ItemState (например: Draft, Active, Inactive, Deleted). Также добавляем свойство ItemCode, которое указывает на конкретную сущность (не путать с Id). Далее это можно было бы использовать так:

а) пользователь создаёт счёт-фактуру, которая ссылается на конкретные записи в БД;
б) если пользователь в дальнейшем изменяет цену товара, то в БД выполняется следующая транзакция:

  1. Запись товара копируется в БД и получает новый Guid, но сохраняет свой ItemCode;
  2. ItemState "старой" сущности меняется с Active на Inactive;

В приведённом примере все используемые "старые" ссылки (например в существующих счетах-фактурах) будут ссылаться на "старые" данные. То есть основная идея в том, что сущности не изменяются в БД, а копируются. Можно ли использовать такой подход? Может быть у этого подхода есть своё название (даже не знаю как это "нагуглить")?


P.S.: не знаю поможет ли эта информация, но используется EF Core 3.1, MS SQL.


Ответы (1 шт):

Автор решения: Roman Konoval

Подходы, которые упомянули в комментариях могут сработать, но не все и всегда практичны, когда нужно поддерживать именно доступ к данным в какой-то момент времени в прошлом. Т.е. да, по истории или аудиту, можно восстановить состояние, на тот момент. Но если уже какая-то часть кода реализована, то это потребует дополнительной поддержки по восстановлению этого состояния, нужно все равно идентифицировать версии и т.д.

Event sourcing выглядит хорошо, но его добавление в готовую систему, требует еще больше усилий. Без опыта использования event sourcing внедрить непросто.

Есть еще один подход, который стоит рассмотреть, и вы его частично описали в вопросе. Суть его в том, что вместо записи Order вы храните записи Order_Version (и так для всех сущностей). Каждая запись - неизменяемая, это может проверять триггер и просто не давать этого делать. Каждое изменение - это добавление новой записи для новой версии сущности. Это немного похоже на подход с историей, но тут настоящая неизменность (immutability) записей и все версии одинаковы, т.е. нет различия между текущей и исторической.

Обычно не требуется большого изменения кода, который использует сущности, так как в большинстве кода все равно работаем ли мы с последней или с какой-то другой версией сущности. Мы по сути всегда используем какую-то потенциально устаревшую версию (так как ее могли поменять после того, как ее прочитали). И выделение этого аспекта явно идет на пользу коду. Обычно изменения требует небольшое количество кода, который собственно меняет сущности или которому нужно явно работать с последней версией, т.е. где были какие-то уже блокировки, чтобы синхронизировать конкурентный доступ.

При таком подходе отчет будет явно привязан к конкретной версии и даже если сущность изменилась, то это никак не влияет на отчет.

Конечно есть определенные накладные расходы и плата за гибкость, а именно, при создании отчета для какой-то версии, нужно выбирать связанные сущности по дате, т.е. нужно выбирать правильные версии связанных сущностей.

→ Ссылка