NHibernate - неожиданное обновление без явного обновления - PullRequest
2 голосов
/ 17 февраля 2010

после того, как дочерняя коллекция с одним элементом загружается лениво при выполнении SQL-выбора для родителя, оператор update выполняется для этого потомка без явного вызова update.

Родительское отображение:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
    namespace="ParentEntity" 
    assembly="ParentEntity">

  <class name="ParentEntity" table="ParentEntity">

    <id name="Id" column="ParentEntityId" unsaved-value="-1">
      <generator class="identity"/>
    </id>

    <bag name="addresses" access="field" inverse="true" cascade="all-delete-orphan" where="IsDeleted = 0">
      <key column="ParentEntityId"/>
      <one-to-many class="Address"/>
    </bag>

  </class>

</hibernate-mapping>

Реализация:

public class ParentEntity : IEntity<ParentEntity>, IAuditableEntity, IDeletableEntity
{

    private ICollection<Address> addresses;


    protected ParentEntity()
    {
        addresses = new List<Address>();

    }



    public virtual ICollection<Address> Addresses
    {
        get
        {
            return new List<Address>(addresses.Where(a => !a.IsDeleted && !a.Validity.IsExpired)).AsReadOnly();
        }
        private set
        {
            addresses = value;
        }
    }

    public virtual ICollection<Address> ExpiredAddresses
    {
        get
        {
            return new List<Address>(addresses.Where(a => !a.IsDeleted && a.Validity.IsExpired)).AsReadOnly();
        }
    }



    #region IAuditableEntity Members

    public virtual EntityTimestamp Timestamp
    {
        get { return timestamp; }
        set { timestamp = value; }
    }

    #endregion


    public virtual bool AddAddress(Address address)
    {
        if (addresses.Contains(address) || ExpiredAddresses.Contains(address) )
            return false;

        address.ParentEntity = this;

        addresses.Add(address);

        return true;
    }

    public virtual bool RemoveAddress(Address address)
    {
        if (!addresses.Contains(address) && !ExpiredAddresses.Contains(address))
            return false;

        address.IsDeleted = true;
        return true;
    }

}

Дочернее отображение:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
    namespace="..." 
    assembly="...">

  <class name="Address" table="Address">

    <id name="Id" column="AddressId" unsaved-value="-1">
      <generator class="identity"/>
    </id>

    <property name="Street" ></property>
    <property name="StreetNumber" ></property>
    <property name="PostOfficeBox" ></property>
    <property name="IsDeleted" not-null="true" ></property>    

    <many-to-one name="City" not-null="true" column="CityId" lazy="false" cascade="none" fetch="join" class="City"></many-to-one>

    <many-to-one name="Type" not-null="true" column="AddressTypeId" lazy="false" cascade="none" fetch="join" class="AddressType"></many-to-one>

    <many-to-one name="ParentEntity" not-null="true" update="false" column="ParentEntityId" lazy="false" cascade="none" fetch="join" class="ParentEntity"></many-to-one>


    <component name="Timestamp" class="EntityTimestamp">
      <property name="CreatedOn" not-null="true" />
      <component name="CreatedBy" class="User">
        <property name="Name" not-null="true" column="CreatedBy" />
      </component>
      <property name="ChangedOn" not-null="true" />
      <component name="ChangedBy" class="User">
        <property name="Name" not-null="true" column="ChangedBy" />
      </component>
    </component>

  </class>

</hibernate-mapping>

Дочерняя реализация:

public class Address : IEntity<Address>, IAuditableEntity, IDeletableEntity
{
    // id etc...

    private EntityTimestamp timestamp;
    private City city;
    private bool isDeleted;
    private string street;
    private string postOfficeBox;
    private string streetNumber;
    private Validity validity;
    private AddressType type;
    private ParentEntity parentEntity;

    public virtual EntityTimestamp Timestamp
    {
        get { return timestamp; }
        set { timestamp = value; }
    }

    public virtual bool IsDeleted
    {
        get { return isDeleted; }
        set { isDeleted = value; }
    }

    public virtual string Street
    {
        get { return street; }
        set { street = value; }
    }

    public virtual string StreetNumber
    {
        get { return streetNumber; }
        set { streetNumber = value; }
    }

    public virtual string PostOfficeBox
    {
        get { return postOfficeBox; }
        set { postOfficeBox = value; }
    }

    public virtual City City
    {
        get { return city; }
        set { city = value; }
    }

    public virtual AddressType Type
    {
        get { return type; }
        set { type = value; }
    }

    public virtual Validity Validity
    {
        get { return validity; }
        set { validity = value; }
    }

    protected internal virtual ParentEntity ParentEntity
    {
        get { return parentEntity; }
        set { parentEntity = value; }
    }

    protected Address()
    {
    }

    public Address(Validity validity)
    {
        this.validity = validity;
    }
}

Правовая отметка времени выглядит следующим образом:

открытый класс EntityTimestamp: IValueObject{private DateTime creationOn;

public virtual DateTime CreatedOn
{
    get { return createdOn; }
    private set { createdOn = value; }
}

private IUser createdBy;


public virtual IUser CreatedBy
{
    get { return createdBy; }
    private set { createdBy = value; }
}

private DateTime changedOn;


public virtual DateTime ChangedOn
{
    get { return changedOn; }
    private set { changedOn = value; }
}

private IUser changedBy;


public virtual IUser ChangedBy
{
    get { return changedBy; }
    private set { changedBy = value; }
}


protected EntityTimestamp()
{
}

private EntityTimestamp(DateTime createdOn, IUser createdBy, DateTime changedOn, IUser changedBy)
{
    if (createdBy == null)
        throw new ArgumentException("Created by user is null.");

    if (changedBy == null)
        throw new ArgumentException("Changed by user is null.");

    this.createdOn = createdOn;
    this.createdBy = createdBy;
    this.changedBy = changedBy;
    this.changedOn = changedOn;
}

public static EntityTimestamp New()
{            
    return new EntityTimestamp(new DateTimePrecise().Now, SecurityService.Current.GetCurrentUser(), new DateTimePrecise().Now, SecurityService.Current.GetCurrentUser());
}

public static EntityTimestamp New(IUser forUser)
{
    return new EntityTimestamp(new DateTimePrecise().Now, forUser, new DateTimePrecise().Now, forUser);
}

public static EntityTimestamp NewUpdated(IUser forUser, EntityTimestamp oldTimestamp)
{
    return new EntityTimestamp(oldTimestamp.CreatedOn, oldTimestamp.CreatedBy, new DateTimePrecise().Now, forUser);
}

public static EntityTimestamp NewUpdated(EntityTimestamp oldTimestamp)
{
    return new EntityTimestamp(oldTimestamp.CreatedOn, oldTimestamp.CreatedBy, new DateTimePrecise().Now, SecurityService.Current.GetCurrentUser());
}

}

Временная метка устанавливается в прослушивателе событий:

public class EntitySaveEventListener : NHibernate.Event.Default.DefaultSaveEventListener
    {
        protected override object PerformSaveOrUpdate(SaveOrUpdateEvent e)
        {
            if (e.Entity is IAuditableEntity)
            {
                var entity = e.Entity as IAuditableEntity;
                //todo: CascadeBeforeSave();
                if (entity != null)
                {             
                        IsDirtyEntity(e.Session, e.Entity);       
                    if (entity.IsNew)
                    {
                        entity.Timestamp = EntityTimestamp.New();
                    }
                    else
                    {
                        entity.Timestamp = EntityTimestamp.NewUpdated(entity.Timestamp);


                    }

                }
            }

            return base.PerformSaveOrUpdate(e);
        }

Поэтому при выполнении SQL-выбора для родителяобновление адреса объекта выполнено.

Используя другой метод, я уже проверил, передается ли адрес прослушивателю событий перед его автоматическим обновлением, если он грязный.Но все реквизиты кажутся одинаковыми.

Что бы это могло быть?Вам нужна дополнительная информация?

Метод, который я проверял, грязный ли адрес при обновлении:

public static Boolean IsDirtyEntity(ISession session, Object entity)
{
    String className = NHibernateProxyHelper.GuessClass(entity).FullName;
    ISessionImplementor sessionImpl = session.GetSessionImplementation();
    IPersistenceContext persistenceContext = sessionImpl.PersistenceContext;
    IEntityPersister persister = sessionImpl.Factory.GetEntityPersister(className);
    EntityEntry oldEntry = sessionImpl.PersistenceContext.GetEntry(entity);


    if ((oldEntry == null) && (entity is INHibernateProxy))
    {
        INHibernateProxy proxy = entity as INHibernateProxy;
        Object obj = sessionImpl.PersistenceContext.Unproxy(proxy);
        oldEntry = sessionImpl.PersistenceContext.GetEntry(obj);
    }

    Object [] oldState = oldEntry.LoadedState;
    Object [] currentState = persister.GetPropertyValues(entity, sessionImpl.EntityMode);
    Int32 [] dirtyProps = persister.FindDirty(currentState, oldState, entity, sessionImpl);

    return (dirtyProps != null);
}

Отладка sql nhibernate:

// выбор родительской сущности

NHibernate.SQL: 2010-02-17 16: 18: 39,357 [21] DEBUG NHibernate.SQL [(null)] - SELECT * FROM(ВЫБРАТЬ spr. *, Spft. [Rank], ROW_NUMBER () OVER (ORDER BY spft. [Rank] DESC) AS RowNum FROM CONTAINSTABLE (ParentEntitySpecialTable, Computed, '"some text"') AS spft INNER JOIN ParentEntity spr ON spr.ParentEntityId = spft. [Key]

              ) AS Results 
                WHERE 
                  RowNum BETWEEN (@p0 - 1) * @p1 + 1 AND @p2 * @p3
              ORDER BY 
             [Rank] DESC;@p0 = 1, @p1 = 20, @p2 = 1, @p3 = 20

NHibernate.SQL: 2010-02-17 16: 18: 39,513 [21] DEBUG NHibernate.SQL [(null)] - ВЫБРАТЬ адрес 0_.ParentEntityId как ServiceP8_3_, address0_.AddressId как AddressId3_, address0_.AddressId как AddressId11_2_, address0_.Street как Street11_2_, address0_.StreetNumber как StreetNu3_11_2_, address0_.PostOfficeBox как PostOffi4_11_2_, address00.Id0.IdressT7_11_2_, addresses0_.ParentEntityId, как ServiceP8_11_2_, addresses0_.ValidityPeriodFrom как Validity9_11_2_, addresses0_.ValidityPeriodTo как Validit10_11_2_, addresses0_.CreatedOn как CreatedOn11_2_, addresses0_.CreatedBy как CreatedBy11_2_, addresses0_.ChangedOn как ChangedOn11_2_, addresses0_.ChangedBy как ChangedBy11_2_, city1_.CityId как CityId9_0_,city1_.IsDeleted, как IsDeleted9_0_, city1_.Name как Name9_0_, city1_.ZipCode как ZipCode9_0_, city1_.CountryId как CountryId9_0_, city1_.CreatedOn как CreatedOn9_0_, city1_.CreatedBy как CreatedBy9_0_, city1_.ChangedOn как ChangedOn9_0_, city1_.ChangedBy как ChangedBy9_0_, addresstyp2_.AddressTypeId в AddressT1_6_1_, addresstyp2_.IsDeleted как IsDeleted6_1_, addresstyp2_.IsSystemDefault как IsSystem3_6_1_, addresstyp2_.Name как Name6_1_, addresstyp2 _. [Key], как column5_6_1_, addresstyp2_.CreatedOn в CreatedOn6_1_, addresstyp2_.CreatedBy как CreatedBy6_1_, addresstyp2_.ChangedOn как ChangedOn6_1_, addresstyp2_.ChangedBy as ChangedBy6_1_ ОТ адресной рекламыdress0_ внутреннее соединение Город city1_ на адресах 0_.CityId = city1_.CityId внутреннее объединение AddressType addresstyp2_ для address0_.AddressTypeId = addresstyp2_.AddressTypeId WHERE (address0_.IsDeleted = 0) и address0__p_p_p_setup_p0_p0_p0_p0_p0_p0_p0_p_0_p0_0(Управляемый): загруженный 'CountryProxyAssembly' 'aspnet_wp.exe' (Управляемый): Загруженный 'CountryProxyModule'

// адрес обновляется

NHibernate.SQL:

2010-02-17 16: 18: 51,607 [21] DEBUG NHibernate.SQL [(null)] - Пакетные команды: команда 0: ОБНОВЛЕНИЕ Адрес SET SET Street = @ p0, StreetNumber = @ p1,PostOfficeBox = @ p2, IsDeleted = @ p3, CityId = @ p4, AddressTypeId = @ p5, ValidityPeriodFrom = @ p6, ValidityPeriodTo = @ p7, CreatedOn = @ p8, CreatedBy = @ p9, ChangedOn = @ p10, ChangedBy = @ p11 ГДЕ AddressId = @ p12; @ p0 = 'fff', @ p1 = '', @ p2 = NULL, @ p3 = False, @ p4 = 116644, @ p5 = 1, @ p6 = 20.01.2010 17:28:15, @ p7 = 31.12.9999 00:00:00, @ p8 = 20.01.2010 17:29:52, @ p9 = 'fff', @ p10 = 17.02.2010 16:18:51, @ p11 = 'fff', @ p12 = 117390

// адрес обновлен

NHibernate.SQL:

2010-02-17 16: 19: 03,748 [21] DEBUG NHibernate.SQL [(null)] - Пакетная обработка команды: команда 0: ОБНОВЛЕНИЕ адреса УСТАНОВКА Street = @ p0, StreetNumber = @ p1, PostOfficeBox = @ p2, IsDeleted = @ p3, CityId = @ p4, AddressTypeId = @ p5, ValidityPeriodFrom = @ p6, ValidityPeriodTo = @ p7, CreatedOn = @ p8, CreatedBy = @ p9, ChangedOn = @ p10, ChangedBy = @ p11 ГДЕ AddressId = @ p12; @ p0 = 'fff', @ p1 = '', @ p2 = NULL, @ p3 = False, @ p4 = 116644, @ p5 = 1, @ p6 = 20.01.2010 17:28:15, @ p7 = 31.12.9999 00:00:00, @ p8 = 20.01.2010 17:29:52, @ p9 = 'fff', @ p10 = 17.02.2010 16:19:03, @ p11 = 'fff', @ p12 = 117390

1 Ответ

3 голосов
/ 17 февраля 2010

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

Я использую смесь слушателей событий. Я не могу использовать событие PreInsert для заполнения «созданных» полей, так как это происходит слишком поздно при обработке, и я получаю нулевые ошибки проверки до того, как PreInsert когда-либо сработает. Я использую событие PreUpdate, потому что я не смог найти надежный способ узнать, действительно ли сущность грязная, и если я установлю поля «last_updated» в OnSaveOrUpdate, это определенно сделает грязную сущность и заставит обновление каждый раз появляться. Используя PreUpdate, я позволил NHibernate проверять «грязь» и просто вставляю свои значения прямо перед запуском обновления.

См. блог ayende для получения дополнительной информации о PreUpdate

public class AuditableEventListener : DefaultSaveOrUpdateEventListener, IPreUpdateEventListener
{
    public override void OnSaveOrUpdate(SaveOrUpdateEvent @event)
    {
        Auditable a = @event.Entity as Auditable;
        if (a != null)
        {
            if (this.GetEntityState(@event.Entity, @event.EntityName, @event.Entry, @event.Session) == EntityState.Transient)
            {
                a.create_dt = DateTime.Now;
                a.create_by = @event.Session.Load<Staff>(CurrentStaff.Id);
            }
        }

        base.OnSaveOrUpdate(@event);
    }

    #region IPreUpdateEventListener Members

    public bool OnPreUpdate(PreUpdateEvent @event)
    {
        var audit = @event.Entity as Auditable;
        if (audit == null) return false;

        var now = DateTime.Now;
        var user = @event.Session.Load<Staff>(CurrentStaff.Id);

        //Very important to keep the State and Entity synced together
        Set(@event.Persister, @event.State, "last_update_dt", now);
        Set(@event.Persister, @event.State, "last_update_by", user);

        audit.last_update_dt = now;
        audit.last_update_by = user;

        return false;
    }

    #endregion


    private void Set(IEntityPersister persister, object[] state, string propertyName, object value)
    {
        var index = Array.IndexOf(persister.PropertyNames, propertyName);
        if (index == -1)
            return;
        state[index] = value;
    }

}

, а затем обязательно подключите требуемые списки событий ...

ISaveOrUpdateEventListener[] saveUpdateListeners = new ISaveOrUpdateEventListener[] { new AuditableEventListener() };
conf.EventListeners.SaveEventListeners = saveUpdateListeners;
conf.EventListeners.SaveOrUpdateEventListeners = saveUpdateListeners;
conf.EventListeners.UpdateEventListeners = saveUpdateListeners;

conf.EventListeners.PreUpdateEventListeners = new IPreUpdateEventListener[] { new AuditableEventListener() };
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...