Re[4]: Аудит доменной модели
От: Буравчик Россия  
Дата: 03.07.09 16:08
Оценка:
Здравствуйте, Alkersan, Вы писали:

B>>Лучше про архитектуру регистров 1С v8 почитать. Паттерн тот же, а концепция на порядок стройнее и понятнее.


A>Регистры восьмой 1С очень мощная вещь, вот только врядли существует документация о том как оно реализовано.


А. Габец, Д. Гончаров, Д. Козырев, Д. Кухлевский, М. Радченко Профессиональная разработка в системе 1С:Предприятие 8 (+ CD-ROM).
В ней есть и про регистры и многое другое.
... << RSDN@Home 1.2.0 alpha 4 rev. 1218>>
Best regards, Буравчик
Re[6]: Аудит доменной модели
От: alex_angel  
Дата: 03.07.09 16:47
Оценка:
Здравствуйте, Alkersan, Вы писали:

B>>Купите электронную книжку СМС-кой, 25 рублей всего:

B>>http://www.online.1c.ru/books/book/3761717/

A>Нашел в офисе такую книгу, пролистал, но ничего по архитектуре регистров не нашел."На примере создания реального прикладного решения показана структура различных объектов системы". Фактически описано программирование мышкой


A>То, что существуют отличные системы аудита, я знаю. Мне не нужна такая функиональность как в 1С. Вопрос изначально был такой: какой эффективный шаблон стот применять для ведения истории изменеий доменной модели, при использовании NHibernate и слоёной архитектуры веб приложения.


A>Ну реверс-инжиниринг 1С предприятия, конечно, тоже решение .



Дело конечно Ваше, но попробовать таки схему, ссылку на которую я давал стоит..она действительно работает и ее имплементация не займет очень много времени, тем более если архитектура действительно у Вас разделена по слоям
Re: Аудит доменной модели
От: Alkersan  
Дата: 07.07.09 19:29
Оценка:
Сегодня попробовал реализовать историю изменений. Вот что получилось:

Есть объект в домене: 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);
        }
Re[2]: Аудит доменной модели
От: gandjustas Россия http://blog.gandjustas.ru/
Дата: 07.07.09 21:17
Оценка:
Здравствуйте, 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
    }
}
Re[3]: Аудит доменной модели
От: Alkersan  
Дата: 07.07.09 21:25
Оценка:
G>Офигеть.. и это для одной сущности.
Для других сущностей, мне кажется, ничего нового не появится. Теперь нужно просто объявлять сущности как AuditableDomainObject<T> и соответствующие члены заменить на AuditableEntity<T>. Все остальное сделают эти базовые классы.
Re: Аудит доменной модели
От: Ziaw Россия  
Дата: 08.07.09 07:03
Оценка:
Здравствуйте, Alkersan, Вы писали:

A>Кто-то мне предлагал перед апдейтом сравнивать первоначальное состояние объекта с текущим при помощи рефлексии, и список изменений заносить в лог. Но прохоить по дереву полей объекта каждый раз при обновлении — это наверно долго. Хотелось бы чтобы изменения заносились в какое-то хранилище, типа транзакции, именно в момент изменеий, но вот как именно такое организовать?


Чем не устроили интерцепоторы NH? Как альтернативный вариант можно организовать аудит средствами СУБД, при открытии сессии задаем сессионную переменную с именем пользователя, изменения полей пишут триггеры, которые очень легко сгенерить.
... << RSDN@Home 1.2.0 alpha 4 rev. 1228>>
Re[2]: Аудит доменной модели
От: Alkersan  
Дата: 08.07.09 07:32
Оценка:
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>Как альтернативный вариант можно организовать аудит средствами СУБД, при открытии сессии задаем сессионную переменную с именем пользователя, изменения полей пишут триггеры, которые очень легко сгенерить.


Средствами СУБД не хотелось бы, потому что: я никогда не работал с триггерами; также продукт получается зависимым от БД; и не уверен насчет возможностей настройки и "кастомизации" аудита в БД, например следить только за некоторыми сущностями, или только за частью полей сущности. Да и не всегда структура БД соотвествует структуре объектной модели.
Re[2]: Аудит доменной модели
От: alex_angel  
Дата: 10.07.09 17:57
Оценка:
Здравствуйте, 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 — закоммитить только трансиентный лог и все, вы получаете историю. Все данные о истории хранятся в доменном объекте.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.