views:

255

answers:

1

Hi,

after a child collection with one item in it gets lazy loaded when performing an SQL select on the parent, an update statement is executed for this child afterwards - without explicitly calling update.

Parent mapping:

<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>

Implementation:

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;
    }

}

Child mapping:

<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>

Child implementation:

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;
    }
}

The entitiy timestamp looks like:

public class EntityTimestamp : IValueObject { private DateTime createdOn;

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());
}

}

The timestamp is set within an event listener:

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);
        }

So when performing a SQL select on the parent, an update of the address entity is executed.

By using another method, I already checked if the address, being passed to the event listener before it is updated automatically, if it is dirty. But all props seem to be the same.

What could that be? Do you need more information?

The method I checked if the address is dirty on update:

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);
}

The nhibernate sql debug:

// parent entity select

NHibernate.SQL: 2010-02-17 16:18:39,357 [21] DEBUG NHibernate.SQL [(null)] - SELECT * FROM ( SELECT 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)] - SELECT addresses0_.ParentEntityId as ServiceP8_3_, addresses0_.AddressId as AddressId3_, addresses0_.AddressId as AddressId11_2_, addresses0_.Street as Street11_2_, addresses0_.StreetNumber as StreetNu3_11_2_, addresses0_.PostOfficeBox as PostOffi4_11_2_, addresses0_.IsDeleted as IsDeleted11_2_, addresses0_.CityId as CityId11_2_, addresses0_.AddressTypeId as AddressT7_11_2_, addresses0_.ParentEntityId as ServiceP8_11_2_, addresses0_.ValidityPeriodFrom as Validity9_11_2_, addresses0_.ValidityPeriodTo as Validit10_11_2_, addresses0_.CreatedOn as CreatedOn11_2_, addresses0_.CreatedBy as CreatedBy11_2_, addresses0_.ChangedOn as ChangedOn11_2_, addresses0_.ChangedBy as ChangedBy11_2_, city1_.CityId as CityId9_0_, city1_.IsDeleted as IsDeleted9_0_, city1_.Name as Name9_0_, city1_.ZipCode as ZipCode9_0_, city1_.CountryId as CountryId9_0_, city1_.CreatedOn as CreatedOn9_0_, city1_.CreatedBy as CreatedBy9_0_, city1_.ChangedOn as ChangedOn9_0_, city1_.ChangedBy as ChangedBy9_0_, addresstyp2_.AddressTypeId as AddressT1_6_1_, addresstyp2_.IsDeleted as IsDeleted6_1_, addresstyp2_.IsSystemDefault as IsSystem3_6_1_, addresstyp2_.Name as Name6_1_, addresstyp2_.[Key] as column5_6_1_, addresstyp2_.CreatedOn as CreatedOn6_1_, addresstyp2_.CreatedBy as CreatedBy6_1_, addresstyp2_.ChangedOn as ChangedOn6_1_, addresstyp2_.ChangedBy as ChangedBy6_1_ FROM Address addresses0_ inner join City city1_ on addresses0_.CityId=city1_.CityId inner join AddressType addresstyp2_ on addresses0_.AddressTypeId=addresstyp2_.AddressTypeId WHERE (addresses0_.IsDeleted = 0) and addresses0_.ParentEntityId=@p0;@p0 = 345625 'aspnet_wp.exe' (Managed): Loaded 'CountryProxyAssembly' 'aspnet_wp.exe' (Managed): Loaded 'CountryProxyModule'

// address is updated

NHibernate.SQL:

2010-02-17 16:18:51,607 [21] DEBUG NHibernate.SQL [(null)] - Batch commands: command 0:UPDATE Address 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 WHERE 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

// address is updated

NHibernate.SQL:

2010-02-17 16:19:03,748 [21] DEBUG NHibernate.SQL [(null)] - Batch commands: command 0:UPDATE Address 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 WHERE 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  A: 

I've run into pretty much the same problem. I have created fields that are non null and updated fields that allow nulls. It looks like you have both as non null so you could simply set the updated fields where I'm setting the created fields below.

I use a mixture of event listeners. I cannot use PreInsert event to populate the "created" fields since it occurs to late in processing and I get null check errors before PreInsert ever fires. I use PreUpdate event because I couldn't find a reliable way to tell if the entity is actually dirty and if I set the "last_updated" fields in OnSaveOrUpdate, it would definitely make the entity dirty and force an update to be issued every time. By using PreUpdate, I've let NHibernate check the dirtiness and I simply inject my values right before the update fires.

See this ayende blog for more info on 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;
    }

}

and then be sure to hook up to required eventlisteners...

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() };
dotjoe