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