История моментальных снимков с Entity Framework

Я рассматривал некоторые крючки аудита с Entity Framework. Многие из них показывают сравнение старых/новых значений. Это отлично подходит для контрольного журнала, но я ищу снимки объектов.

Например... Допустим, у меня есть приложение, которое управляет продуктами. Продукт имеет несколько атрибутов и связанных с ними других объектов. Допустим, я меняю объект 10 раз. Давайте также скажем, что важно, чтобы я мог просматривать экраны этих изменений объекта (не контрольный журнал, а то, как на самом деле выглядел экран в формате только для чтения). Что меня интересует, так это возможность получить исходный объект продукта EF (со всеми связанными данными) для всех 10 этих изменений (в зависимости от того, что я хочу видеть) и использовать его для привязки к моему экрану.

Если я использую SQL Server, какой тип я должен использовать для сериализованного объекта в настоящее время (XML, blob и т. д.)? Есть ли смысл это делать?


person RailRhoad    schedule 21.09.2009    source источник
comment
Я добавлю награду, чтобы получить более подробный ответ, пожалуйста :)   -  person Anyname Donotcare    schedule 24.05.2016
comment
@AnynameDonotcare для каждой таблицы, для которой вы хотите сохранить историю: 1) добавьте столбец «Версия» (может быть время, может быть увеличивающийся счетчик). 2) добавить еще одну таблицу со всеми теми же свойствами, что и основная таблица (но лучше расширить внешние ключи). 3) В триггере перед обновлением, если столбец «Версия» изменился — скопируйте старые значения в эту таблицу версий. 4) Прибыль.   -  person Evk    schedule 25.05.2016
comment
@Evk: я буду очень признателен, если вы добавите подробный ответ с простым примером (используя EF), который можно использовать в качестве основы для корпоративного приложения (относительно действия удаления и отношений m-m)   -  person Anyname Donotcare    schedule 25.05.2016


Ответы (4)


Посмотрим. У вас есть требование взять граф объектов и сериализовать его в базу данных в формате, который позволит вам материализовать его позже. Я думаю, что есть инструменты, которые делают именно это. Один из них, как мне кажется, это Entity Framework.

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

Я бы предложил разрешить управление версиями всех типов сущностей. Когда вы редактируете тип объекта, вы редактируете ревизию подсказки и сохраняете предыдущую ревизию, содержащую предыдущие значения. (Причина, по которой вы редактируете редакцию подсказки вместо вставки новой подсказки, заключается в том, что другие объекты, которые в настоящее время не материализованы в ObjectContext, могут содержать ссылки на подсказку, которые вы хотели бы сохранить как ссылки на подсказку, а не ссылки на задняя ревизия)

При необходимости вы можете разбить свои таблицы SQL Server таким образом, чтобы старые версии хранились в другой файловой группе. Это позволит вам делать резервные копии последних и предыдущих версий отдельно.

person Craig Stuntz    schedule 21.09.2009
comment
I would propose that you allow all of your entity types to be versioned. Не могли бы вы уточнить, что вы имеете в виду? - person Anyname Donotcare; 24.05.2016

Сначала вам нужно добавить набор свойств к вашим таблицам:

  • Версия - время последней модификации (также может быть автоинкрементный счетчик вместо времени).
  • LastModifiedBy - ссылка на пользователя, сделавшего последнее изменение (если вы его сохранили).

Тогда у вас есть несколько вариантов, как сохранить историю версий. Ты сможешь

  1. Создайте новую таблицу для каждой из основных таблиц, для которых вы хотите сохранить историю. Эти таблицы истории будут иметь все те же поля, что и основная таблица, но первичные и внешние ключи не будут применяться. Для каждого внешнего ключа также сохраните версию записи, на которую ссылаются, на момент создания версии.

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

Как вы заполняете свои таблицы истории? Через триггеры обновления и удаления.

  • В триггере обновления для вашей сущности скопируйте все предыдущие значения в таблицу истории. Для каждого внешнего ключа также скопируйте текущую версию объекта, на который делается ссылка.
  • В триггере удаления - в основном сделайте то же самое.

Обратите внимание, что все больше и больше современных систем ничего НЕ удаляют. Они просто отмечают вещи как удаленные. Если вы хотите следовать этому шаблону (у которого есть несколько преимуществ) - вместо удаления добавьте флаг IsDeleted к своим объектам (конечно, вам придется везде отфильтровывать удаленные объекты).

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

Помимо всего вышеперечисленного - вы также можете восстановить состояние вашей сущности до любой предыдущей версии.

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

person Evk    schedule 27.05.2016
comment
Почему вы предпочитаете первый подход второму, хотя он включает в себя больше избыточных данных и требует больше места для хранения в БД? Триггеры вызывают проблемы с производительностью в дополнение к тому, что я хочу обрабатывать все эти операции через «EF». Не могли бы вы привести простой пример (две таблицы) - person Anyname Donotcare; 27.05.2016
comment
Я впервые слышу, что триггеры вызывают проблемы с производительностью. Триггеры сами по себе не могут их вызвать, только если вы проделаете внутри какую-то тяжелую логику. В общем, вы можете сделать это в EF точно так же - просто в вашем обработчике обновлений сначала скопируйте все текущие значения в таблицу истории (не уверен, какой пример я могу привести здесь - я полагаю, вы знаете, как копировать значения из одного объекта в Другая). Я предпочитаю первый подход, потому что он более простой и понятный, а также позволяет легко восстановить любую предыдущую версию. В дополнение к этому - он позволяет искать в истории (что может быть сложнее сделать, если вы сериализуете). - person Evk; 27.05.2016
comment
если я хочу обрабатывать логику триггера через «EF», должен ли он быть в транзакции? - person Anyname Donotcare; 27.05.2016
comment
Да, конечно (но помните, что SaveChanges уже выполняется внутри транзакции, поэтому, если вы добавите свою запись истории в контекст, вместе будет применено обновление - вы уже делаете это в транзакции). - person Evk; 27.05.2016
comment
Большое спасибо, пожалуйста. 1-Каковы преимущества двух свойств для каждой таблицы, если я добавлю эти свойства в эквивалентную таблицу, которая будет использоваться для управления версиями. 2-Что насчет отношений mm?3-Вы сказали, что современные системы ничего не удаляют, но я читал много соображений относительно мягкого удаления, в основном об ограничениях. - person Anyname Donotcare; 27.05.2016
comment
1. Не уверен, что вы подразумеваете под двумя свойствами для каждой таблицы. Если речь идет о внешних ключах, я имею в виду, что в таблице history хранятся версии ключей, на которые ссылаются, потому что объекты, на которые ссылаются, также могут меняться. Предположим, у вас есть таблица Person, которая ссылается на PersonInfo, которая, в свою очередь, имеет поле Address. Когда вы сохраняете историю для таблицы Person, вам нужно (в таблице истории) сохранить как PersonInfoID (внешний ключ для PersonInfo), так и PersonInfo.Version, потому что позже PersonInfo может измениться, и вам нужно знать его состояние в момент сохранения его состояния. - person Evk; 31.05.2016
comment
Предположим, что вместо этого вы просто сериализуете все свойства из таблицы Person и сохраняете их как большой двоичный объект. Затем вы не сможете вернуться к этой версии позже, потому что за это время в любой из связанных таблиц могло что-то измениться. Опять же - этот подход только в том случае, если вы хотите иметь возможность восстановить обратно, в противном случае вы хотите просто сериализовать измененные столбцы и показать это пользователю, если это необходимо. - person Evk; 31.05.2016
comment
Я имею в виду Version ,LastModifiedBy в каждой таблице? в дополнение к эквивалентной таблице для каждой таблицы - person Anyname Donotcare; 31.05.2016
comment
2. О отношениях m-m - я сделал это только с упомянутым столбцом IsDeleted. Там ничего не удаляется и можно восстановить состояние на любой момент времени. Надеюсь, очевидно, как отношения m-n работают со столбцом IsDeleted (в основном так же, как и со всеми остальными). Я не буду советовать, как с ними обращаться без мягкого удаления, так как сам этого не делал. 3. Да, это приносит осложнения, поэтому у вас должны быть веские причины для этого. Если вы можете обойтись без - конечно, не используйте мягкое удаление. Однако я не могу сказать, что это всегда плохая практика. - person Evk; 31.05.2016
comment
Столбец версии, который вы скопируете в свою таблицу истории при изменении. Скажем, у вас есть версия = вчера. Вы обновляете свою строку, теперь вам нужно скопировать все столбцы (включая версию) в таблицу истории. Итак, после этого у вас будет строка в таблице «История» с «Версия = вчера», а в вашей основной таблице будет «Версия = сейчас». - person Evk; 31.05.2016
comment
Кстати, если хотите что-то попроще и с EF — загляните сюда github.com/loresoft/EntityFramework. Расширенный, в функциональности AuditLog. - person Evk; 31.05.2016

В проекте, который я недавно создал, мы использовали подключение к методу SaveChanges в классе DbContext. Это дало нам доступ к экземпляру класса ChangeTracker. Позвонив ChangeTracker.Entries(), вы получите доступ к списку DbEntityEntry. DbEntityEntry имеет следующие интересные свойства и методы:

  • State - объект создается заново, модифицируется или удаляется
  • Entity - копия объекта в его нынешнем виде
  • CurrentValues - перечисление редактируемых значений
  • OriginalValues - перечисление исходных значений

Мы создали набор POCO для наборов изменений и изменений, к которым затем можно было получить доступ через EF. Это позволило нашим пользователям просматривать изменения на уровне поля вместе с датами и ответственными пользователями.

person Bradley Stacey    schedule 27.05.2016

Взгляните на временные таблицы (темпоральные таблицы с системным управлением версиями), если вы используете SQL Server 2016‹ или Azure SQL.

https://docs.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables?view=sql-server-ver15

Из документации:

Функция базы данных, которая предоставляет встроенную поддержку для предоставления информации о данных, хранящихся в таблице, в любой момент времени, а не только данных, которые являются правильными в текущий момент времени. Temporal — это функция базы данных, представленная в ANSI SQL 2011.

Я написал полное руководство, как реализовать его с Entity Framework Core без каких-либо сторонних библиотек здесь:

https://stackoverflow.com/a/64244548/3850405

person Ogglas    schedule 07.10.2020