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

178
18 апреля 2022, 14:20

При реализации проекта/схемы БД небольшой/тривиальной бухгалтерской программы возникла следующая проблема. Представим себе несколько таблиц в БД (пока не обращайте внимание на типы, можно взять за пример БД 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.

Answer 1

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

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

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

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

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

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

READ ALSO
Как вывести элементы из Listbox

Как вывести элементы из Listbox

Имеется ComboBox, из которого пользователь выбирает строки, которые после этого отображаются на ListBoxХочу эти выбранные элементы вывести через...

235
Mysql + C# перенос информации

Mysql + C# перенос информации

Как из MySql phpmyadmin извлечь информацию из таблицы и перенести в Label?

131
Как убирать символы в тексте?

Как убирать символы в тексте?

Кароче, у меня есть панель с паролем и я хочу в случае чего убирать вбитые цифры, но не знаю как это реализовать, пробовал через Remove, не выходило

151
Ошибка CS0161 C#

Ошибка CS0161 C#

В коде возникает ошибка CS0161(не все пути кода возвращают значение)

150