Здравствуйте, 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 modified
if (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);
}
}
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<class name="Terrier.Core.Domain.Resume, Core" table="Resume" lazy="false">
<id name="ID" column="ResumeId">
<generator class="native" />
</id>
<property name="FName" column="FName" type="string" />
<property name="LName" column="LName" type="string" />
<property name="SName" column="SName" type="string" />
<property name="BirthDate" column="BirthDate" type="datetime"/>
</class>
</hibernate-mapping>
[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
}
}
Здравствуйте, Alkersan, Вы писали:
A>Кто-то мне предлагал перед апдейтом сравнивать первоначальное состояние объекта с текущим при помощи рефлексии, и список изменений заносить в лог. Но прохоить по дереву полей объекта каждый раз при обновлении — это наверно долго. Хотелось бы чтобы изменения заносились в какое-то хранилище, типа транзакции, именно в момент изменеий, но вот как именно такое организовать?
Чем не устроили интерцепоторы NH? Как альтернативный вариант можно организовать аудит средствами СУБД, при открытии сессии задаем сессионную переменную с именем пользователя, изменения полей пишут триггеры, которые очень легко сгенерить.
... << RSDN@Home 1.2.0 alpha 4 rev. 1228>>
Здравствуйте, Alkersan, Вы писали:
Привет, позволю себе вставить несколько коментариев и отзывов
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 clone
return 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 — закоммитить только трансиентный лог и все, вы получаете историю. Все данные о истории хранятся в доменном объекте.