В данный момент разрабатываю систему для Human Resources на ASP.NET. Как платформу использую Web Client Software Factory, для доступа к данным — NHibernate. Появилось требование — реализовать историю изменеий объектов домена. Но не просто кто и когда менял последним запись о сотруднике, а детально, какие поля какого объекта, кем, когда менялись. Много перечитал но к решению так и не пришел.
В каком слое лучше реализовать такой функционал, и как приблизительно должно выглядеть такое решение?
Я выделяю 2 сценария: создание business entity и дальнейшее ее редактирование. Допустим удалять в системе ничего нельзя (информация вечная).
В какой момент лучше делать запись в лог? Фаулер описывал подход когда сами объекты в setter свойствах вызывали Логер. Но это как-то коряво, и не логично — доменным объектам должно быть все равно, есть ли аудит или нет.
Может лучше с помощью какого-то атрибута над свойствами? Но тогда куда передавать сообщение о том что я (свойство) изменилось? Какой шаблон лучше для этого использовать?
Кто-то мне предлагал перед апдейтом сравнивать первоначальное состояние объекта с текущим при помощи рефлексии, и список изменений заносить в лог. Но прохоить по дереву полей объекта каждый раз при обновлении — это наверно долго. Хотелось бы чтобы изменения заносились в какое-то хранилище, типа транзакции, именно в момент изменеий, но вот как именно такое организовать?
Здравствуйте, Alkersan, Вы писали:
A>В каком слое лучше реализовать такой функционал, и как приблизительно должно выглядеть такое решение?
Сейчас модно группировать операции над сущностями в сервисах, а аудит подключать декларативно, задействуя интерсепторы-перехватчики вызовов методов сервисов. См. PostSharp (интерсепторы подключаются на этапе компиляции) или Unity (умеет, например, выполнять перехват виртуальных вызовов)
Здравствуйте, Alkersan, Вы писали:
A>Кто-то мне предлагал перед апдейтом сравнивать первоначальное состояние объекта с текущим при помощи рефлексии, и список изменений заносить в лог. Но прохоить по дереву полей объекта каждый раз при обновлении — это наверно долго. Хотелось бы чтобы изменения заносились в какое-то хранилище, типа транзакции, именно в момент изменеий, но вот как именно такое организовать?
Это в принципе правильный вариант. Незнаю как NHibernate, а из EF можно вытащить список измененных полей объекта. Вероятне всего и в хибернейте есть такая возможность
соответсвенно перед коммитом изменений можно записывать куда-нибудь эти изменения. отслеживать изменения в свойствах категорически не стоит.
Кстати имеет смысл посмотреть в сторону sharepoint, там есть как аудит, так и версионирование записей.
Здравствуйте, gandjustas, Вы писали:
G>Кстати имеет смысл посмотреть в сторону sharepoint, там есть как аудит, так и версионирование записей.
На шерпоинт смотреть не стоит, т.к. система строится как раз для того, чтобы отказаться от шерпоинта
B> Ааудит подключать декларативно, задействуя интерсепторы-перехватчики вызовов методов сервисов
Можно детальнее описать такой подход? Я смотрел быстрый старт PostSharp, они создали там атрибут, который пишет в трейс информацию том какой метод выполняется. Т.е. можно также написать атрибут, который будет вызывать Логер, когда вызывается setter какой-то проперти?
Здравствуйте, Alkersan, Вы писали:
A>выполняется. Т.е. можно также написать атрибут, который будет вызывать Логер, когда вызывается setter какой-то проперти?
Сорри, недочитал. С пропертями сложнее, да и нужно ли на каждый чих вызывать логгер? Ведь изменение свойств сущностей обычно накапливают в модели, а потом производят транзакционное обновление.
B>Изменение свойств сущностей накапливают в модели, а потом производят транзакционное обновление.
Накопление изменений реализовано в NHibernate, он точно реализует шаблон UnitOfWork, но я не знаю как извлечь список измененний в текущей сессии. Может кто-то знает?
K> NIH-синдром? Или есть более веские причины?
Нет, никакого синдрома. Просто, как мне кажется, даже на мощном шерпоинте трудно реализовать весь функционал бизнесс системы. Шерпоинт — это по сути портал с контентом. В этом плане он очень удобен, и как центральный информационный ресурс компании он и используется. Но когда доменная модель со своими приколами — то тут надо писать все руками.
Здравствуйте, Alkersan, Вы писали:
B>>Изменение свойств сущностей накапливают в модели, а потом производят транзакционное обновление.
A>Накопление изменений реализовано в NHibernate, он точно реализует шаблон UnitOfWork, но я не знаю как извлечь список измененний в текущей сессии. Может кто-то знает?
Здравствуйте, Alkersan, Вы писали:
K>> NIH-синдром? Или есть более веские причины? A>Нет, никакого синдрома. Просто, как мне кажется, даже на мощном шерпоинте трудно реализовать весь функционал бизнесс системы. Шерпоинт — это по сути портал с контентом. В этом плане он очень удобен, и как центральный информационный ресурс компании он и используется. Но когда доменная модель со своими приколами — то тут надо писать все руками.
Ну если изначально взялись за "доменную модель", то SP вам будет только мешать. Но не факт что такая модель нужна в HR.
Здравствуйте, Alkersan, Вы писали:
A>В данный момент разрабатываю систему для Human Resources на ASP.NET. Как платформу использую Web Client Software Factory, для доступа к данным — NHibernate. Появилось требование — реализовать историю изменеий объектов домена. Но не просто кто и когда менял последним запись о сотруднике, а детально, какие поля какого объекта, кем, когда менялись. Много перечитал но к решению так и не пришел. A>В каком слое лучше реализовать такой функционал, и как приблизительно должно выглядеть такое решение?
Если вам не нужно потом будет использовать эту накопленную историю как-то хитро в предметной области, то решение такое:
— реализовать в отдельном, отвязанном от логики, facility (history & audit facility, H&aF );
— к NHibernate подключить interceptor'ы (штатная возможность), которые и будут оповещать H&aF об изменении данных.
Если история будет вплетена в домен, то и H&aF нужно размещать тесно с остальной логикой; вероятно, можно обойтись и без перехватчиков уровня ORM.
Здравствуйте, Alkersan, Вы писали:
A>В данный момент разрабатываю систему для Human Resources на ASP.NET. Как платформу использую Web Client Software Factory, для доступа к данным — NHibernate. Появилось требование — реализовать историю изменеий объектов домена. Но не просто кто и когда менял последним запись о сотруднике, а детально, какие поля какого объекта, кем, когда менялись. Много перечитал но к решению так и не пришел. A>В каком слое лучше реализовать такой функционал, и как приблизительно должно выглядеть такое решение?
Здравствуйте. Насколько я понял — вопрос о реализации версионности. По этой ссылке описан очень понятный механизм. В конце есть ссылка на пдфку с более детальным описанием. http://software.intel.com/ru-ru/blogs/2009/03/09/2000727/
Если будут вопросы — обращайся, объясню детали если что
_>вопрос о реализации версионности. По этой ссылке описан очень понятный механизм
В моем случае скорее не версионность, а аудит изменений. Нужен самый простой механизм, когда не нужны откаты. Просто чтобы велся лог какие поля каких доменных объектов были изменены, кем и когда, желательно с возможностью гибкой настройки полей поддающихся аудиту(но это уже не столь важно, возможно в будущих версиях). Есть корневой объект — Сотрудник. В нем есть множество всязанных классов — контакты, телефоны, адреса, отпуска и т.д.
Re[3]: Аудит доменной модели
От:
Аноним
Дата:
02.07.09 19:06
Оценка:
Здравствуйте, Alkersan, Вы писали:
_>>вопрос о реализации версионности. По этой ссылке описан очень понятный механизм
A>В моем случае скорее не версионность, а аудит изменений. Нужен самый простой механизм, когда не нужны откаты. Просто чтобы велся лог какие поля каких доменных объектов были изменены, кем и когда, желательно с возможностью гибкой настройки полей поддающихся аудиту(но это уже не столь важно, возможно в будущих версиях). Есть корневой объект — Сотрудник. В нем есть множество всязанных классов — контакты, телефоны, адреса, отпуска и т.д.
Да-да, именно аудит в данной схеме и описан.
Возможность откатить изменения — это вовсе не главное и имплементация заняла буквально пару строк кода и один юнит-тест
Нашел в офисе такую книгу, пролистал, но ничего по архитектуре регистров не нашел."На примере создания реального прикладного решения показана структура различных объектов системы". Фактически описано программирование мышкой
То, что существуют отличные системы аудита, я знаю. Мне не нужна такая функиональность как в 1С. Вопрос изначально был такой: какой эффективный шаблон стот применять для ведения истории изменеий доменной модели, при использовании NHibernate и слоёной архитектуры веб приложения.
Ну реверс-инжиниринг 1С предприятия, конечно, тоже решение .
Здравствуйте, Alkersan, Вы писали:
B>>Лучше про архитектуру регистров 1С v8 почитать. Паттерн тот же, а концепция на порядок стройнее и понятнее.
A>Регистры восьмой 1С очень мощная вещь, вот только врядли существует документация о том как оно реализовано.
Здравствуйте, Alkersan, Вы писали:
B>>Купите электронную книжку СМС-кой, 25 рублей всего: B>>http://www.online.1c.ru/books/book/3761717/
A>Нашел в офисе такую книгу, пролистал, но ничего по архитектуре регистров не нашел."На примере создания реального прикладного решения показана структура различных объектов системы". Фактически описано программирование мышкой
A>То, что существуют отличные системы аудита, я знаю. Мне не нужна такая функиональность как в 1С. Вопрос изначально был такой: какой эффективный шаблон стот применять для ведения истории изменеий доменной модели, при использовании NHibernate и слоёной архитектуры веб приложения.
A>Ну реверс-инжиниринг 1С предприятия, конечно, тоже решение .
Дело конечно Ваше, но попробовать таки схему, ссылку на которую я давал стоит..она действительно работает и ее имплементация не займет очень много времени, тем более если архитектура действительно у Вас разделена по слоям
Сегодня попробовал реализовать историю изменений. Вот что получилось:
Есть объект в домене: Resume. Представляет собой сущность, описывающую соискателя работы, содержит — фамилию, имя, отчество, дату рождения ( и еще несколько полей и коллекцию прикрепленных файлов к резюме, которые я опустил для экономии). Объект унаследован от базового шаблонного класса AuditableDomainObject<T>, который в свою очередь расширяет базовый класс DomainObject<T>.
DomainObject<T> — взят из статьи на codeproject.com — здесь; он отвечает за управление идентификатором (Id) сущности и позволяет отличать сохраненные (Persistent) объекты, новосозданных (Transient) исходя из значения идентификатора.
Этот класс расширяется классом AuditableDomainObject<T>, которым я "помечаю" те объекты, подлежат аудиту. Он содержит такой функционал: коллекцию AuditLog и обертку для нее (собственно сам AuditLog и есть обертка, а сама коллекция это список записей IList<AuditLogEntry>) — в этой коллекции накапливаются изменеия с момента последней транзакции. Также существует коллекция auditableEntitys, в которой хрянятся свойства данного объекта, которые "поддаются" аудиту. Т.е. возможна ситуация, что не за всеми полями объекта Resume нужно следить, тогда такое поле просто не попадает в эту коллекцию, и его измения игнорируются. Метод LogChanges() проходится по этой коллекции полей, выявляет изменившиеся или же новосозданные значения ( поле считается созданным — если объект Transient,т.е. если у него идентификатор имеет значение по умолчанию, в данном случае 0, или же поле считается изменённым, если первоначальное значение не равно текущему, см. далее ).
Собственно поля полежащие аудиту — это члены класса AuditableEntity<T>. Содержат 2 члена — Original и Current, начальное значение и текущее соответсвенно. На основе их принимается решение — писать ли данное состояние в лог или нет. Также есть строковый член FieldName с "человеческим" названием текущего поля (_FName — это "Фамилия" например). Несколько конструкторов... и метод LogChanges(), который пишет в переданный при создании экземпляр AuditLog ткущее состояние. Этот метод вызывается извне, при опросе коллекции auditableEntitys. В эти моменты и происходит запись в лог.
Вся эта иерархия пока-что нормально уживается с NHibernate. Маппинг Resume.hbm.xml приведен дальше. Все AuditableEntity<T> скрыты от чужих, и создаюся при вызове set соотсвествующей Property. NHibernate был настроен на доступ к членам класса через Propert`я, а не через private члены, как было ранее (свойство маппинга access не указано, поэтому по умолчанию для доступа использутся Property). Таким образом когда объект берется из базы с помошью NHibernate, создаются заодно объекты и коллекции для аудита.
Написал несколько простых тестов (я пока несилен в юнит тестах), довел их до работоспособности.
public class Resume : AuditableDomainObject<int>
{
#region Constructors
/// <summary>
/// Needed by ORM for reflective creation.
/// </summary>private Resume() { }
public Resume(string fname, string lname, DateTime birth)
{
FName = fname;
LName = lname;
SName = sname;
if (!birth.Equals(DateTime.MinValue)) BirthDate = birth;
else BirthDate = null;
}
#endregion
#region Properties
public string FName
{
get { return _FirstName.Current; }
set
{
Check.Require(!string.IsNullOrEmpty(value), "A valid FirstName name must be provided in Resume");
//_FName = value;if (_FirstName == null)
{
_FirstName = new AuditableEntity<string>(value, "Имя", base.AuditLog);
base.AddEntityToAuditCollection(_FirstName);
}
else _FirstName.Current = value;
}
}
public string LName
{
get { return _LastName.Current; }
set
{
Check.Require(!string.IsNullOrEmpty(value), "A valid LastName name must be provided in Resume");
//_LName = value;if (_LastName == null)
{
_LastName = new AuditableEntity<string>(value, "Фамилия", base.AuditLog);
base.AddEntityToAuditCollection(_LastName);
}
else _LastName.Current = value;
}
}
public string SName
{
get { return _SurName.Current; }
set
{
//_SName = value;if (_SurName == null)
{
_SurName = new AuditableEntity<string>(value, "Отчество", base.AuditLog);
base.AddEntityToAuditCollection(_SurName);
}
else _SurName.Current = value;
}
}
public DateTime? BirthDate
{
get { return _DateOfBirth.Current; }
set
{
//_BirthDate = value;if (_DateOfBirth == null)
{
_DateOfBirth = new AuditableEntity<DateTime?>(value, "Дата рождения", base.AuditLog);
base.AddEntityToAuditCollection(_DateOfBirth);
}
else _DateOfBirth.Current = value;
}
}
#endregion
#region Methods
public override int GetHashCode()
{
return (GetType().FullName + "|" +
FName + "|" +
LName + "|" +
SName + "|" +
((BirthDate.HasValue) ? BirthDate.Value.ToString(): ""))
.GetHashCode();
}
#endregion
#region Members
//private string _FName;
//private string _LName;
//private string _SName;
//private DateTime? _BirthDate;private AuditableEntity<string> _FirstName;
private AuditableEntity<string> _LastName;
private AuditableEntity<string> _SurName;
private AuditableEntity<DateTime?> _DateOfBirth;
#endregion
}
public abstract class AuditableDomainObject <IdT> : DomainObject<IdT>, IAuditableObject
{
/// <summary>
/// AuditLog collection contains a historical data of current Business Entity
/// </summary>public AuditLog AuditLog
{
get
{
if (_auditLogWrapper == null)
_auditLogWrapper = new AuditLog(_auditLog);
return _auditLogWrapper;
}
}
private AuditLog _auditLogWrapper;
private IList<AuditLogEntry> _auditLog = new List<AuditLogEntry>();
/// <summary>
/// AuditableEntitys collection contains a list of auditable properties
/// </summary>public ReadOnlyCollection<IAuditableEntity> AuditableEntitys
{
get { return new ReadOnlyCollection<IAuditableEntity>(auditableEntitys); }
}
private IList<IAuditableEntity> auditableEntitys = new List<IAuditableEntity>();
/// <summary>
/// Adds an IAuditableEntity to the list of auditable fields.
/// </summary>
/// <param name="entity"></param>protected void AddEntityToAuditCollection(IAuditableEntity entity)
{
auditableEntitys.Add(entity);
}
/// <summary>
/// Review the AuditableEntitys collection and record changed to the AuditLog collection
/// </summary>public void LogChanges()
{
if (auditableEntitys.Count != 0)
{
bool isTransient = base.IsTransient();
foreach (IAuditableEntity entity in auditableEntitys)
{
//Log only those properties, who are created or modifiedif (isTransient || entity.Modified)
entity.LogChanges(isTransient);
}
}
}
// These two methods are not used anywhere... They can be deleted.....public void LogChanges(AuditLogEntry entry)
{
_auditLog.Add(entry);
}
public void LogChanges(string aspect, string oldValue, string newValue, DateTime timestamp, string user, bool onCreating)
{
AuditLogEntry newEntry = new AuditLogEntry(aspect, oldValue, newValue, timestamp, user, onCreating);
LogChanges(newEntry);
}
}
public abstract class DomainObject<IdT>
{
/// <summary>
/// ID may be of type string, int, custom type, etc.
/// Setter is protected to allow unit tests to set this property via reflection and to allow
/// domain objects more flexibility in setting this for those objects with assigned IDs.
/// </summary>public IdT ID
{
get { return id; }
protected set { id = value; }
}
public override sealed bool Equals(object obj)
{
DomainObject<IdT> compareTo = obj as DomainObject<IdT>;
return (compareTo != null) &&
(HasSameNonDefaultIdAs(compareTo) ||
// Since the IDs aren't the same, either of them must be transient to
// compare business value signatures
(((IsTransient()) || compareTo.IsTransient()) &&
HasSameBusinessSignatureAs(compareTo)));
}
/// <summary>
/// Transient objects are not associated with an item already in storage. For instance,
/// a <see cref="Customer" /> is transient if its ID is 0.
/// </summary>public bool IsTransient()
{
return ID == null || ID.Equals(default(IdT));
}
/// <summary>
/// Must be provided to properly compare two objects
/// </summary>public abstract override int GetHashCode();
private bool HasSameBusinessSignatureAs(DomainObject<IdT> compareTo)
{
Check.Require(compareTo != null, "compareTo may not be null");
return GetHashCode().Equals(compareTo.GetHashCode());
}
/// <summary>
/// Returns true if self and the provided persistent object have the same ID values
/// and the IDs are not of the default ID value
/// </summary>private bool HasSameNonDefaultIdAs(DomainObject<IdT> compareTo)
{
Check.Require(compareTo != null, "compareTo may not be null");
return (ID != null && !ID.Equals(default(IdT))) &&
(compareTo.ID != null && !compareTo.ID.Equals(default(IdT))) &&
ID.Equals(compareTo.ID);
}
private IdT id = default(IdT);
}
public class AuditableEntity <T> : IAuditableEntity
{
private AuditLog _auditLog;
public T Current {get; set;}
public T Original { get; set; }
public string FieldName { get; set; }
public bool Modified
{
get { return !(Current.Equals(Original)); }
}
#region Constructors
public AuditableEntity(T value)
{
Current = Original = value;
}
public AuditableEntity(T value, AuditLog auditLog)
{
_auditLog = auditLog;
Current = Original = value;
}
public AuditableEntity(T value, string fieldName)
{
Current = Original = value;
FieldName = fieldName;
}
public AuditableEntity(T value, string fieldName, AuditLog auditLog)
{
_auditLog = auditLog;
Current = Original = value;
FieldName = fieldName;
}
#endregion
public void LogChanges(bool onCreating)
{
if (Modified) onCreating = false;
_auditLog.Add(new AuditLogEntry(FieldName, Original.ToString(), Current.ToString(), DateTime.Now, "", onCreating));
}
public void LogChanges()
{
LogChanges(false);
}
}
[TestMethod]
public void ResumeSaveTest()
{
NHibernateDaoFactory factory = new NHibernateDaoFactory(TestGlobals.SessionFactoryConfigPath);
IResumeDao resumeDao = factory.GetResumeDao();
int id = 520;
Terrier.Core.Domain.Resume resume = resumeDao.GetById(id, true);
Assert.IsNotNull(resume, "Resume with ID {0} was not returned", id);
string newName = "NOT " + resume.FName;
resume.FName = newName;
resume.LogChanges();
Assert.IsTrue(resume.AuditLog.Count == 1);
resumeDao.Save(resume);
resumeDao.CommitChanges();
resume = resumeDao.GetById(id, true);
Assert.IsTrue(resume.FName == newName);
}
[TestMethod]
public void AuditLogCreating()
{
Resume resume = new Resume("Alex", "Alex", "Alex", DateTime.Now, "alkersan@gmail.com", "---", "8(050)", "AM");
resume.LogChanges();
Persist(resume);
Assert.IsTrue(resume is AuditableDomainObject<int>);
Assert.IsTrue(resume.AuditLog.Count == (resume as AuditableDomainObject<int>).AuditableEntitys.Count, "After creation of new resume not all Auditale Properties were recorded to log");
// Make some changes and record them to AuditLog
resume.FName = "Not Alex";
resume.LogChanges();
// To AuditLog must be added 1 new value
Assert.IsTrue(resume.AuditLog.Count == (resume as AuditableDomainObject<int>).AuditableEntitys.Count + 1);
Assert.IsTrue(resume.AuditLog[resume.AuditLog.Count - 1].NewValue == "Not Alex", "Change of FName was not recorded to AuditLog");
}
private void Persist(DomainObject<int> transientEntity)
{
var t = transientEntity.GetType().BaseType;
var f = t.GetProperty("ID");
int v = new Random().Next(1,9999);
f.SetValue(transientEntity, v, null);
}
Здравствуйте, Alkersan, Вы писали:
A>Сегодня попробовал реализовать историю изменений. Вот что получилось:...
Офигеть.. и это для одной сущности.
Я тут код аудита для EF набросал. Не проверял работает или нет, но общая идея именно такая:
public partial class SomeContext
{
partial void OnContextCreated()
{
this.SavingChanges += new EventHandler(SomeContext_SavingChanges);
}
void SomeContext_SavingChanges(object sender, EventArgs e)
{
var changeSet = this.ObjectStateManager
.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified);
var logEntries = from se in changeSet
from modifiedProperty in se.GetModifiedProperties()
let ordinal = se.OriginalValues.GetOrdinal(modifiedProperty)
select new
{
Key = se.EntityKey,
Old = se.OriginalValues.GetValue(ordinal),
New = se.CurrentValues.GetValue(ordinal)
};
//write entries to log
}
}
G>Офигеть.. и это для одной сущности.
Для других сущностей, мне кажется, ничего нового не появится. Теперь нужно просто объявлять сущности как AuditableDomainObject<T> и соответствующие члены заменить на AuditableEntity<T>. Все остальное сделают эти базовые классы.
Здравствуйте, Alkersan, Вы писали:
A>Кто-то мне предлагал перед апдейтом сравнивать первоначальное состояние объекта с текущим при помощи рефлексии, и список изменений заносить в лог. Но прохоить по дереву полей объекта каждый раз при обновлении — это наверно долго. Хотелось бы чтобы изменения заносились в какое-то хранилище, типа транзакции, именно в момент изменеий, но вот как именно такое организовать?
Чем не устроили интерцепоторы NH? Как альтернативный вариант можно организовать аудит средствами СУБД, при открытии сессии задаем сессионную переменную с именем пользователя, изменения полей пишут триггеры, которые очень легко сгенерить.
Z>Чем не устроили интерцепоторы NH?
Собственно против интерцепоторов я ничего против не имею. В моем посте я не указал в какой момент я собираюсь писать лог. Думаю возможно такой вариант: подписать EventListener для событий PostUpdate, PostInsert, и в них делать запись в лог.
public class AuditEventListener : IPostUpdateEventListener
{
public void OnPostUpdate(PostUpdateEvent e)
{
if (e.Entity is IAuditableObject)
{
IAuditLogDao auditLogDao = NHibernateSessionManager.Instance.GetDaoFactory().GetAuditLogDao();
auditLogDao.Save(e.Entity.AuditLog);
}
}
}
К тому моменту как произойдет событие PostUpdate или PostInsert, коллекция AuditLog уже будет содержать в себе все изменения с момента последней транзакции.
Z>Как альтернативный вариант можно организовать аудит средствами СУБД, при открытии сессии задаем сессионную переменную с именем пользователя, изменения полей пишут триггеры, которые очень легко сгенерить.
Средствами СУБД не хотелось бы, потому что: я никогда не работал с триггерами; также продукт получается зависимым от БД; и не уверен насчет возможностей настройки и "кастомизации" аудита в БД, например следить только за некоторыми сущностями, или только за частью полей сущности. Да и не всегда структура БД соотвествует структуре объектной модели.
Привет, позволю себе вставить несколько коментариев и отзывов
A>Сегодня попробовал реализовать историю изменений. Вот что получилось:
A>DomainObject<T> — взят из статьи на codeproject.com — здесь; он отвечает за управление идентификатором (Id) сущности и позволяет отличать сохраненные (Persistent) объекты, новосозданных (Transient) исходя из значения идентификатора.
Скажи, у тебя заказчик просил идентификатор? Прямо так и сказал, что нужен id ему для каких-то целей?? Я в этом сильно сомневаюсь. Раз так — идентификатор ни при каких обстоятельствах не должен быть в доменном объекте. Домен — это твой бизнес. Если в бизнесе нет понятий идентификатора, следовательно, не должно его быть и у тебя. Для таких вещей, как разделение persistent объектов, есть паттерн UnitOfWork. (а я раньше codeproject.com считал неплохим сайтом..жуть)
A>Этот класс расширяется классом AuditableDomainObject<T>, которым я "помечаю" те объекты, подлежат аудиту. Он содержит такой функционал: коллекцию AuditLog и обертку для нее (собственно сам AuditLog и есть обертка, а сама коллекция это список записей IList<AuditLogEntry>) — в этой коллекции накапливаются изменеия с момента последней транзакции. Также существует коллекция auditableEntitys, в которой хрянятся свойства данного объекта, которые "поддаются" аудиту. Т.е. возможна ситуация, что не за всеми полями объекта Resume нужно следить, тогда такое поле просто не попадает в эту коллекцию, и его измения игнорируются. Метод LogChanges() проходится по этой коллекции полей, выявляет изменившиеся или же новосозданные значения ( поле считается созданным — если объект Transient,т.е. если у него идентификатор имеет значение по умолчанию, в данном случае 0, или же поле считается изменённым, если первоначальное значение не равно текущему, см. далее ).
Вот тут более-менее в тему, однако есть излишки. Вовсе ни к чему хранить множество логов и энтитей. Главное ты уловил — разделить изменяемую часть и не изменяемую. Приведу код попозже, как лучше это сделать
A>Собственно поля полежащие аудиту — это члены класса AuditableEntity<T>. Содержат 2 члена — Original и Current, начальное значение и текущее соответсвенно. На основе их принимается решение — писать ли данное состояние в лог или нет. Также есть строковый член FieldName с "человеческим" названием текущего поля (_FName — это "Фамилия" например). Несколько конструкторов... и метод LogChanges(), который пишет в переданный при создании экземпляр AuditLog ткущее состояние. Этот метод вызывается извне, при опросе коллекции auditableEntitys. В эти моменты и происходит запись в лог.
держать Original и Current не имеет смысла. Что если было много изменений, какое считать Original? Если последнее, то можно просто хранить коллекцию и сортировать их при выгрузке по дате создания, например.
Ты вроде говорил, что у тебя есть разделение по слоям!!! тогда что значем "человеческое" название? то, что хочет видеть на экране пользователь? так пусть это лежит не в домене, а в презентационной логике. Опять же, если это не бизнес — нечего ему тут делать.
ну а теперь немного кода:
public class Resume2 : AuditableObject<AuditableEntity2> {
//non-changed fields
}
public class AuditableEntity2 : ICloneable<AuditableEntity2> {
protected string FName { get; set; }
protected string LName { get; set; }
protected string SName { get; set; }
protected DateTime? BirthDate { get; set; }
public AuditableEntity2 Clone() {
//just clonereturn null;
}
}
public abstract class AuditableObject<TAuditable> where TAuditable: ICloneable<TAuditable>, new() {
private readonly List<AuditLog<TAuditable>> history = new List<AuditLog<TAuditable>>() ;
private AuditLog<TAuditable> transient;
public TAuditable this[DateTime timePoint] {
get {
return transient != null
?
transient.Version
:
history.Find(
transaction =>
timePoint >= transaction.EffectiveFrom && timePoint < transaction.EffectiveTo).
Version.Clone();
}
}
public void AddVersion(DateTime effectiveFrom, DateTime effectiveTo) {
transient = new AuditLog<TAuditable>(effectiveFrom, effectiveTo, new TAuditable());
}
public void Commit() {
if (transient != null) {
history.Add(transient);
}
transient = null;
}
}
public interface ICloneable<T> {
T Clone();
}
internal class AuditLog<TAuditable> {
public AuditLog(DateTime effectiveFrom, DateTime effectiveTo, TAuditable version) {
EffectiveFrom = effectiveFrom;
EffectiveTo = effectiveTo;
Version = version;
}
public TAuditable Version { get; private set; }
public DateTime EffectiveFrom { get; private set; }
public DateTime EffectiveTo { get; private set; }
}
и естественно, вопросы коммита в базу доменного объекта можно легко делать с помощью NHibernate — закоммитить только трансиентный лог и все, вы получаете историю. Все данные о истории хранятся в доменном объекте.