From: <jul...@us...> - 2011-01-17 15:04:11
|
Revision: 5361 http://nhibernate.svn.sourceforge.net/nhibernate/?rev=5361&view=rev Author: julian-maughan Date: 2011-01-17 15:04:02 +0000 (Mon, 17 Jan 2011) Log Message: ----------- Fixed merge failure when there is a transient entity reachable by multiple paths and at least one path does not cascade on merge [ref. NH-2481]. Port from Hibernate. Modified Paths: -------------- trunk/nhibernate/releasenotes.txt trunk/nhibernate/src/NHibernate/Event/Default/DefaultMergeEventListener.cs trunk/nhibernate/src/NHibernate/NHibernate.csproj trunk/nhibernate/src/NHibernate/Type/EntityType.cs trunk/nhibernate/src/NHibernate.Test/NHSpecificTest/NH479/Fixture.cs trunk/nhibernate/src/NHibernate.Test/NHibernate.Test.csproj Added Paths: ----------- trunk/nhibernate/src/NHibernate/Event/Default/EventCache.cs trunk/nhibernate/src/NHibernate.Test/Cascade/A.cs trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/CascadeMergeToChildBeforeParent.hbm.xml trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/CascadeMergeToChildBeforeParentTest.cs trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/MultiPathCircleCascade.hbm.xml trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/MultiPathCircleCascadeTest.cs trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Node.cs trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Route.cs trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Tour.cs trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Transport.cs trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Vehicle.cs trunk/nhibernate/src/NHibernate.Test/Cascade/G.cs trunk/nhibernate/src/NHibernate.Test/Cascade/H.cs trunk/nhibernate/src/NHibernate.Test/Cascade/MultiPathCascade.hbm.xml trunk/nhibernate/src/NHibernate.Test/Cascade/MultiPathCascadeTest.cs Modified: trunk/nhibernate/releasenotes.txt =================================================================== --- trunk/nhibernate/releasenotes.txt 2011-01-17 05:55:48 UTC (rev 5360) +++ trunk/nhibernate/releasenotes.txt 2011-01-17 15:04:02 UTC (rev 5361) @@ -1,4 +1,8 @@ ** Known BREAKING CHANGES from NH3.0.0.GA to NH3.0.1.GA + + ##### Run time ##### + * [NH-2481] - An exception will now be thrown when an entity references a transient entity and cascade="merge|all" is not configured on the association + ##### Possible Breaking Changes ##### * [NH-2461] - Signature change for IQuery.SetParameterList @@ -10,7 +14,7 @@ ##### Run time ##### * [NH-2199] - null values in maps/dictionaries are no longer silenty ignored/deleted * [NH-1894] - SybaseAnywhereDialect has been removed, and replaced with SybaseASA9Dialect. - - Sybase Adaptive Server Enterprise (ASE) dialects removed. + - Sybase Adaptive Server Enterprise (ASE) dialects removed. ##### Possible Breaking Changes ##### * [NH-2251] - Signature change for GetLimitString in Dialect Modified: trunk/nhibernate/src/NHibernate/Event/Default/DefaultMergeEventListener.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Event/Default/DefaultMergeEventListener.cs 2011-01-17 05:55:48 UTC (rev 5360) +++ trunk/nhibernate/src/NHibernate/Event/Default/DefaultMergeEventListener.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -1,6 +1,7 @@ using System; using System.Collections; +using Iesi.Collections.Generic; using NHibernate.Classic; using NHibernate.Engine; using NHibernate.Intercept; @@ -11,9 +12,8 @@ namespace NHibernate.Event.Default { - /// <summary> - /// Defines the default copy event listener used by hibernate for copying entities - /// in response to generated copy events. + /// <summary> + /// Defines the default event listener for handling of merge events generated from a session. /// </summary> [Serializable] public class DefaultMergeEventListener : AbstractSaveEventListener, IMergeEventListener @@ -32,16 +32,58 @@ protected override IDictionary GetMergeMap(object anything) { - return IdentityMap.Invert((IDictionary)anything); + return ((EventCache)anything).InvertMap(); } public virtual void OnMerge(MergeEvent @event) { - OnMerge(@event, IdentityMap.Instantiate(10)); + EventCache copyCache = new EventCache(); + + OnMerge(@event, copyCache); + + // TODO: iteratively get transient entities and retry merge until one of the following conditions: + // 1) transientCopyCache.size() == 0 + // 2) transientCopyCache.size() is not decreasing and copyCache.size() is not increasing + + // TODO: find out if retrying can add entities to copyCache (don't think it can...) + // For now, just retry once; throw TransientObjectException if there are still any transient entities + + IDictionary transientCopyCache = this.GetTransientCopyCache(@event, copyCache); + + if (transientCopyCache.Count > 0) + { + RetryMergeTransientEntities(@event, transientCopyCache, copyCache); + + // find any entities that are still transient after retry + transientCopyCache = this.GetTransientCopyCache(@event, copyCache); + + if (transientCopyCache.Count > 0) + { + ISet<string> transientEntityNames = new HashedSet<string>(); + + foreach (object transientEntity in transientCopyCache.Keys) + { + string transientEntityName = @event.Session.GuessEntityName(transientEntity); + + transientEntityNames.Add(transientEntityName); + + log.InfoFormat( + "transient instance could not be processed by merge: {0} [{1}]", + transientEntityName, + transientEntity.ToString()); + } + + throw new TransientObjectException("one or more objects is an unsaved transient instance - save transient instance(s) before merging: " + transientEntityNames); + } + } + + copyCache.Clear(); + copyCache = null; } - - public virtual void OnMerge(MergeEvent @event, IDictionary copyCache) + + public virtual void OnMerge(MergeEvent @event, IDictionary copiedAlready) { + EventCache copyCache = (EventCache)copiedAlready; IEventSource source = @event.Session; object original = @event.Original; @@ -54,7 +96,7 @@ if (li.IsUninitialized) { log.Debug("ignoring uninitialized proxy"); - @event.Result = source.Load(li.PersistentClass, li.Identifier); + @event.Result = source.Load(li.EntityName, li.Identifier); return; //EARLY EXIT! } else @@ -66,14 +108,20 @@ { entity = original; } - - if (copyCache.Contains(entity)) + + if (copyCache.Contains(entity) && copyCache.IsOperatedOn(entity)) { - log.Debug("already merged"); + log.Debug("already in merge process"); @event.Result = entity; } else { + if (copyCache.Contains(entity)) + { + log.Info("already in copyCache; setting in merge process"); + copyCache.SetOperatedOn(entity, true); + } + @event.Entity = entity; EntityState entityState = EntityState.Undefined; @@ -129,11 +177,12 @@ log.Debug("ignoring persistent instance"); //TODO: check that entry.getIdentifier().equals(requestedId) + object entity = @event.Entity; IEventSource source = @event.Session; IEntityPersister persister = source.GetEntityPersister(@event.EntityName, entity); - copyCache[entity] = entity; //before cascade! + ((EventCache)copyCache).Add(entity, entity, true); //before cascade! CascadeOnMerge(source, persister, entity, copyCache); CopyValues(persister, entity, entity, source, copyCache); @@ -143,45 +192,100 @@ protected virtual void EntityIsTransient(MergeEvent @event, IDictionary copyCache) { - log.Debug("merging transient instance"); + log.Info("merging transient instance"); object entity = @event.Entity; IEventSource source = @event.Session; IEntityPersister persister = source.GetEntityPersister(@event.EntityName, entity); string entityName = persister.EntityName; + + @event.Result = this.MergeTransientEntity(entity, entityName, @event.RequestedId, source, copyCache); + } + + private object MergeTransientEntity(object entity, string entityName, object requestedId, IEventSource source, IDictionary copyCache) + { + IEntityPersister persister = source.GetEntityPersister(entityName, entity); object id = persister.HasIdentifierProperty ? persister.GetIdentifier(entity, source.EntityMode) : null; + object copy = null; + + if (copyCache.Contains(entity)) + { + copy = copyCache[entity]; + persister.SetIdentifier(copy, id, source.EntityMode); + } + else + { + copy = source.Instantiate(persister, id); + ((EventCache)copyCache).Add(entity, copy, true); // before cascade! + } - object copy = persister.Instantiate(id, source.EntityMode); // should this be Session.instantiate(Persister, ...)? - copyCache[entity] = copy; //before cascade! - // cascade first, so that all unsaved objects get their // copy created before we actually copy //cascadeOnMerge(event, persister, entity, copyCache, Cascades.CASCADE_BEFORE_MERGE); base.CascadeBeforeSave(source, persister, entity, copyCache); CopyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.ForeignKeyFromParent); - //this bit is only *really* absolutely necessary for handling - //requestedId, but is also good if we merge multiple object - //graphs, since it helps ensure uniqueness - object requestedId = @event.RequestedId; - if (requestedId == null) + try { - SaveWithGeneratedId(copy, entityName, copyCache, source, false); + // try saving; check for non-nullable properties that are null or transient entities before saving + this.SaveTransientEntity(copy, entityName, requestedId, source, copyCache); } - else + catch (PropertyValueException ex) { - SaveWithRequestedId(copy, requestedId, entityName, copyCache, source); + string propertyName = ex.PropertyName; + object propertyFromCopy = persister.GetPropertyValue(copy, propertyName, source.EntityMode); + object propertyFromEntity = persister.GetPropertyValue(entity, propertyName, source.EntityMode); + IType propertyType = persister.GetPropertyType(propertyName); + EntityEntry copyEntry = source.PersistenceContext.GetEntry(copy); + + if (propertyFromCopy == null || !propertyType.IsEntityType) + { + log.InfoFormat("property '{0}.{1}' is null or not an entity; {1} =[{2}]", copyEntry.EntityName, propertyName, propertyFromCopy); + throw; + } + + if (!copyCache.Contains(propertyFromEntity)) + { + log.InfoFormat("property '{0}.{1}' from original entity is not in copyCache; {1} =[{2}]", copyEntry.EntityName, propertyName, propertyFromEntity); + throw; + } + + if (((EventCache)copyCache).IsOperatedOn(propertyFromEntity)) + { + log.InfoFormat("property '{0}.{1}' from original entity is in copyCache and is in the process of being merged; {1} =[{2}]", copyEntry.EntityName, propertyName, propertyFromEntity); + } + else + { + log.InfoFormat("property '{0}.{1}' from original entity is in copyCache and is not in the process of being merged; {1} =[{2}]", copyEntry.EntityName, propertyName, propertyFromEntity); + } + + // continue...; we'll find out if it ends up not getting saved later } - - // cascade first, so that all unsaved objects get their + + // cascade first, so that all unsaved objects get their // copy created before we actually copy base.CascadeAfterSave(source, persister, entity, copyCache); CopyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.ForeignKeyToParent); - @event.Result = copy; + return copy; } + + private void SaveTransientEntity(object entity, string entityName, object requestedId, IEventSource source, IDictionary copyCache) + { + // this bit is only *really* absolutely necessary for handling + // requestedId, but is also good if we merge multiple object + // graphs, since it helps ensure uniqueness + if (requestedId == null) + { + SaveWithGeneratedId(entity, entityName, copyCache, source, false); + } + else + { + SaveWithRequestedId(entity, requestedId, entityName, copyCache, source); + } + } protected virtual void EntityIsDetached(MergeEvent @event, IDictionary copyCache) { @@ -211,7 +315,7 @@ string previousFetchProfile = source.FetchProfile; source.FetchProfile = "merge"; - //we must clone embedded composite identifiers, or + //we must clone embedded composite identifiers, or //we will get back the same instance that we pass in object clonedIdentifier = persister.IdentifierType.DeepCopy(id, source.EntityMode, source.Factory); object result = source.Get(persister.EntityName, clonedIdentifier); @@ -220,7 +324,7 @@ if (result == null) { - //TODO: we should throw an exception if we really *know* for sure + //TODO: we should throw an exception if we really *know* for sure // that this is a detached instance, rather than just assuming //throw new StaleObjectStateException(entityName, id); @@ -237,7 +341,7 @@ return; } - copyCache[entity] = result; //before cascade! + ((EventCache)copyCache).Add(entity, result, true); //before cascade! object target = source.PersistenceContext.Unproxy(result); if (target == entity) @@ -258,7 +362,7 @@ throw new StaleObjectStateException(persister.EntityName, id); } - // cascade first, so that all unsaved objects get their + // cascade first, so that all unsaved objects get their // copy created before we actually copy CascadeOnMerge(source, persister, entity, copyCache); CopyValues(persister, entity, target, source, copyCache); @@ -357,8 +461,7 @@ persister.SetPropertyValues(target, copiedValues, source.EntityMode); } - protected virtual void CopyValues(IEntityPersister persister, object entity, object target, - ISessionImplementor source, IDictionary copyCache, ForeignKeyDirection foreignKeyDirection) + protected virtual void CopyValues(IEntityPersister persister, object entity, object target, ISessionImplementor source, IDictionary copyCache, ForeignKeyDirection foreignKeyDirection) { object[] copiedValues; @@ -383,7 +486,7 @@ persister.SetPropertyValues(target, copiedValues, source.EntityMode); } - /// <summary> + /// <summary> /// Perform any cascades needed as part of this copy event. /// </summary> /// <param name="source">The merge event being processed. </param> @@ -403,6 +506,92 @@ } } + /// <summary> + /// Determine which merged entities in the copyCache are transient. + /// </summary> + /// <param name="event"></param> + /// <param name="copyCache"></param> + /// <returns></returns> + /// <remarks>Should this method be on the EventCache class?</remarks> + protected EventCache GetTransientCopyCache(MergeEvent @event, EventCache copyCache) + { + EventCache transientCopyCache = new EventCache(); + + foreach(object entity in copyCache.Keys) + { + object entityCopy = copyCache[entity]; + + if (entityCopy is INHibernateProxy) + entityCopy = ((INHibernateProxy)entityCopy).HibernateLazyInitializer.GetImplementation(); + + // NH-specific: Disregard entities that implement ILifecycle and manage their own state - they + // don't have an EntityEntry, and we can't determine if they are transient or not + if (entityCopy is ILifecycle) + continue; + + EntityEntry copyEntry = @event.Session.PersistenceContext.GetEntry(entityCopy); + + if (copyEntry == null) + { + // entity name will not be available for non-POJO entities + // TODO: cache the entity name somewhere so that it is available to this exception + log.InfoFormat( + "transient instance could not be processed by merge: {0} [{1}]", + @event.Session.GuessEntityName(entityCopy), + entity); + + // merge did not cascade to this entity; it's in copyCache because a + // different entity has a non-nullable reference to it; + // this entity should not be put in transientCopyCache, because it was + // not included in the merge; + + throw new TransientObjectException( + "object is an unsaved transient instance - save the transient instance before merging: " + @event.Session.GuessEntityName(entityCopy)); + } + else if (copyEntry.Status == Status.Saving) + { + transientCopyCache.Add(entity, entityCopy, copyCache.IsOperatedOn(entity)); + } + else if (copyEntry.Status != Status.Loaded && copyEntry.Status != Status.ReadOnly) + { + throw new AssertionFailure( + String.Format( + "Merged entity does not have status set to MANAGED or READ_ONLY; {0} status = {1}", + entityCopy, + copyEntry.Status)); + } + } + return transientCopyCache; + } + + /// <summary> + /// Retry merging transient entities + /// </summary> + /// <param name="event"></param> + /// <param name="transientCopyCache"></param> + /// <param name="copyCache"></param> + protected void RetryMergeTransientEntities(MergeEvent @event, IDictionary transientCopyCache, EventCache copyCache) + { + // TODO: The order in which entities are saved may matter (e.g., a particular + // transient entity may need to be saved before other transient entities can + // be saved). + // Keep retrying the batch of transient entities until either: + // 1) there are no transient entities left in transientCopyCache + // or 2) no transient entities were saved in the last batch. + // For now, just run through the transient entities and retry the merge + + foreach(object entity in transientCopyCache.Keys) + { + object copy = transientCopyCache[entity]; + EntityEntry copyEntry = @event.Session.PersistenceContext.GetEntry(copy); + + if (entity == @event.Entity) + MergeTransientEntity(entity, copyEntry.EntityName, @event.RequestedId, @event.Session, copyCache); + else + MergeTransientEntity(entity, copyEntry.EntityName, copyEntry.Id, @event.Session, copyCache); + } + } + /// <summary> Cascade behavior is redefined by this subclass, disable superclass behavior</summary> protected override void CascadeAfterSave(IEventSource source, IEntityPersister persister, object entity, object anything) { Added: trunk/nhibernate/src/NHibernate/Event/Default/EventCache.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Event/Default/EventCache.cs (rev 0) +++ trunk/nhibernate/src/NHibernate/Event/Default/EventCache.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,186 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Iesi.Collections; +using NHibernate.Util; + +namespace NHibernate.Event.Default +{ + public class EventCache : IDictionary + { + private IDictionary entityToCopyMap = IdentityMap.Instantiate(10); + // key is an entity involved with the operation performed by the listener; + // value can be either a copy of the entity or the entity itself + + private IDictionary entityToOperatedOnFlagMap = IdentityMap.Instantiate(10); + // key is an entity involved with the operation performed by the listener; + // value is a flag indicating if the listener explicitly operates on the entity + + #region ICollection Implementation + + /// <summary> + /// Returns the number of entity-copy mappings in this EventCache + /// </summary> + public int Count + { + get { return entityToCopyMap.Count; } + } + + public bool IsSynchronized + { + get { return false; } + } + + public object SyncRoot + { + get { return this; } + } + + public void CopyTo(Array array, int index) + { + if (array == null) + throw new ArgumentNullException("array"); + if (index < 0) + throw new ArgumentOutOfRangeException("arrayIndex is less than 0"); + if (entityToCopyMap.Count + index + 1 > array.Length) + throw new ArgumentException("The number of elements in the source ICollection<T> is greater than the available space from arrayIndex to the end of the destination array."); + + entityToCopyMap.CopyTo(array, index); + } + + #endregion + + #region IEnumerable implementation + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)entityToCopyMap).GetEnumerator(); + } + + #endregion + + #region IDictionary implementation + + public object this[object key] + { + get + { + return entityToCopyMap[key]; + } + set + { + this.Add(key, value); + } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool IsFixedSize + { + get { return false; } + } + + public ICollection Keys + { + get { return entityToCopyMap.Keys; } + } + + public ICollection Values + { + get { return entityToCopyMap.Values; } + } + + public void Add(object key, object value) + { + if (key == null) + throw new ArgumentNullException("key"); + if (value == null) + throw new ArgumentNullException("value"); + + entityToCopyMap.Add(key, value); + entityToOperatedOnFlagMap.Add(key, false); + } + + public bool Contains(object key) + { + return entityToCopyMap.Contains(key); + } + + public void Remove(object key) + { + entityToCopyMap.Remove(key); + entityToOperatedOnFlagMap.Remove(key); + } + + public IDictionaryEnumerator GetEnumerator() + { + return entityToCopyMap.GetEnumerator(); + } + + public void Clear() + { + entityToCopyMap.Clear(); + entityToOperatedOnFlagMap.Clear(); + } + + #endregion + + /// <summary> + /// Associates the specified entity with the specified copy in this EventCache; + /// </summary> + /// <param name="entity"></param> + /// <param name="copy"></param> + /// <param name="isOperatedOn">indicates if the operation is performed on the entity</param> + public void Add(object entity, object copy, bool isOperatedOn) + { + if (entity == null) + throw new ArgumentNullException("null entities are not supported", "entity"); + if (copy == null) + throw new ArgumentNullException("null entity copies are not supported", "copy"); + + entityToCopyMap.Add(entity, copy); + entityToOperatedOnFlagMap.Add(entity, isOperatedOn); + } + + /// <summary> + /// Returns copy-entity mappings + /// </summary> + /// <returns></returns> + public IDictionary InvertMap() + { + return IdentityMap.Invert(entityToCopyMap); + } + + /// <summary> + /// Returns true if the listener is performing the operation on the specified entity. + /// </summary> + /// <param name="entity">Must be non-null and this EventCache must contain a mapping for this entity</param> + /// <returns></returns> + public bool IsOperatedOn(object entity) + { + if (entity == null) + throw new ArgumentNullException("null entities are not supported", "entity"); + + return (bool)entityToOperatedOnFlagMap[entity]; + } + + /// <summary> + /// Set flag to indicate if the listener is performing the operation on the specified entity. + /// </summary> + /// <param name="entity"></param> + /// <param name="isOperatedOn"></param> + public void SetOperatedOn(object entity, bool isOperatedOn) + { + if (entity == null) + throw new ArgumentNullException("null entities are not supported", "entity"); + + if (!entityToOperatedOnFlagMap.Contains(entity) || !entityToCopyMap.Contains(entity)) + throw new AssertionFailure("called EventCache.SetOperatedOn() for entity not found in EventCache"); + + entityToOperatedOnFlagMap[entity] = isOperatedOn; + } + } +} \ No newline at end of file Modified: trunk/nhibernate/src/NHibernate/NHibernate.csproj =================================================================== --- trunk/nhibernate/src/NHibernate/NHibernate.csproj 2011-01-17 05:55:48 UTC (rev 5360) +++ trunk/nhibernate/src/NHibernate/NHibernate.csproj 2011-01-17 15:04:02 UTC (rev 5361) @@ -165,6 +165,7 @@ <Compile Include="Engine\TypedValue.cs" /> <Compile Include="Engine\UnsavedValueFactory.cs" /> <Compile Include="Engine\Versioning.cs" /> + <Compile Include="Event\Default\EventCache.cs" /> <Compile Include="Exceptions\ADOExceptionHelper.cs" /> <Compile Include="Criterion\AbstractCriterion.cs" /> <Compile Include="Criterion\AndExpression.cs" /> Modified: trunk/nhibernate/src/NHibernate/Type/EntityType.cs =================================================================== --- trunk/nhibernate/src/NHibernate/Type/EntityType.cs 2011-01-17 05:55:48 UTC (rev 5360) +++ trunk/nhibernate/src/NHibernate/Type/EntityType.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -243,7 +243,6 @@ return value; //special case ... this is the leaf of the containment graph, even though not immutable } - /// <summary></summary> public override bool IsMutable { get { return false; } @@ -251,8 +250,7 @@ public abstract bool IsOneToOne { get; } - public override object Replace(object original, object target, ISessionImplementor session, object owner, - IDictionary copyCache) + public override object Replace(object original, object target, ISessionImplementor session, object owner, IDictionary copyCache) { if (original == null) { @@ -269,17 +267,26 @@ { return target; } - object id = GetIdentifier(original, session); - if (id == null) + if (session.GetContextEntityIdentifier(original) == null && ForeignKeys.IsTransient(associatedEntityName, original, false, session)) { - throw new AssertionFailure("cannot copy a reference to an object with a null id"); + object copy = session.Factory.GetEntityPersister(associatedEntityName).Instantiate(null, session.EntityMode); + //TODO: should this be Session.instantiate(Persister, ...)? + copyCache.Add(original, copy); + return copy; } - id = GetIdentifierOrUniqueKeyType(session.Factory).Replace(id, null, session, owner, copyCache); - return ResolveIdentifier(id, session, owner); + else + { + object id = GetIdentifier(original, session); + if (id == null) + { + throw new AssertionFailure("non-transient entity has a null id"); + } + id = GetIdentifierOrUniqueKeyType(session.Factory).Replace(id, null, session, owner, copyCache); + return ResolveIdentifier(id, session, owner); + } } } - /// <summary></summary> public override bool IsAssociationType { get { return true; } @@ -612,4 +619,4 @@ } } -} +} \ No newline at end of file Added: trunk/nhibernate/src/NHibernate.Test/Cascade/A.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/A.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/A.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,58 @@ +using System; +using Iesi.Collections.Generic; + +namespace NHibernate.Test.Cascade +{ + public class A + { + private long id; + private string data; + private ISet<H> hs; // A 1 - * H + private G g; // A 1 - 1 G + + public A() + { + hs = new HashedSet<H>(); + } + + public A(string data) : this() + { + this.data = data; + } + + public virtual long Id + { + get { return id; } + set { id = value; } + } + + public virtual string Data + { + get { return data; } + set { data = value; } + } + + public virtual G G + { + get { return g; } + set { g = value; } + } + + public virtual ISet<H> Hs + { + get { return hs; } + set { hs = value; } + } + + public virtual void AddH(H h) + { + hs.Add(h); + h.A = this; + } + + public override string ToString() + { + return "A[" + id + ", " + data + "]"; + } + } +} \ No newline at end of file Property changes on: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle ___________________________________________________________________ Added: bugtraq:url + http://jira.nhibernate.org/browse/%BUGID% Added: bugtraq:logregex + NH-\d+ Added: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/CascadeMergeToChildBeforeParent.hbm.xml =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/CascadeMergeToChildBeforeParent.hbm.xml (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/CascadeMergeToChildBeforeParent.hbm.xml 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,139 @@ +<?xml version="1.0"?> +<hibernate-mapping + xmlns="urn:nhibernate-mapping-2.2" + assembly="NHibernate.Test" + namespace="NHibernate.Test.Cascade.Circle"> + + <class name="Route" table="HB2_Route"> + + <id name="RouteId" type="long"> + <generator class="native" /> + </id> + + <version name="Version" column="VERS" type="long" /> + + <property name="Name" type="string" not-null="true" /> + + <set name="Nodes" inverse="true" cascade="persist,merge,refresh"> + <key column="RouteId" /> + <one-to-many class="Node" /> + </set> + + <set name="Vehicles" inverse="true" cascade="persist,merge,refresh"> + <key column="routeId" /> + <one-to-many class="Vehicle" /> + </set> + + </class> + + <class name="Tour" table="HB2_Tour"> + + <id name="TourId" type="long"> + <generator class="native" /> + </id> + + <version name="Version" column="VERS" type="long" /> + + <property name="Name" type="string" not-null="true" /> + + <set name="Nodes" inverse="true" lazy="true" cascade="merge,refresh"> + <key column="TourId" /> + <one-to-many class="Node" /> + </set> + + </class> + + <class name="Transport" table="HB2_Transport"> + + <id name="TransportId" type="long"> + <generator class="native" /> + </id> + + <version name="Version" column="VERS" type="long" /> + + <property name="Name" type="string" not-null="true" /> + + <many-to-one name="PickupNode" + column="PickupNodeId" + unique="true" + not-null="true" + cascade="merge,refresh" + lazy="false" /> + + <many-to-one name="DeliveryNode" + column="DeliveryNodeId" + unique="true" + not-null="true" + cascade="merge,refresh" + lazy="false" /> + + <many-to-one name="Vehicle" + column="VehicleId" + unique="false" + not-null="true" + cascade="none" + lazy="false" /> + + </class> + + <class name="Vehicle" table="HB2_Vehicle"> + + <id name="VehicleId" type="long"> + <generator class="native" /> + </id> + + <version name="Version" column="VERS" type="long" /> + + <property name="Name" /> + + <set name="Transports" inverse="false" lazy="true" cascade="merge,refresh"> + <key column="VehicleId" /> + <one-to-many class="Transport" not-found="exception" /> + </set> + + <many-to-one name="Route" + column="RouteId" + unique="false" + not-null="true" + cascade="none" + lazy="false" /> + + </class> + + <class name="Node" table="HB2_Node"> + + <id name="NodeId" type="long"> + <generator class="native" /> + </id> + + <version name="Version" column="VERS" type="long" /> + + <property name="Name" type="string" not-null="true" /> + + <set name="DeliveryTransports" inverse="true" lazy="true" cascade="merge,refresh"> + <key column="DeliveryNodeId" /> + <one-to-many class="Transport" /> + </set> + + <set name="PickupTransports" inverse="true" lazy="true" cascade="merge,refresh"> + <key column="PickupNodeId" /> + <one-to-many class="Transport" /> + </set> + + <many-to-one name="Route" + column="RouteId" + unique="false" + not-null="true" + cascade="none" + lazy="false" /> + + <many-to-one name="Tour" + column="TourId" + unique="false" + not-null="false" + cascade="merge,refresh" + lazy="false" /> + + </class> + +</hibernate-mapping> \ No newline at end of file Added: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/CascadeMergeToChildBeforeParentTest.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/CascadeMergeToChildBeforeParentTest.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/CascadeMergeToChildBeforeParentTest.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,261 @@ +using System; +using System.Collections; +using System.Linq; +using NHibernate.Engine; +using NHibernate.Test; +using NUnit.Framework; + +namespace NHibernate.Test.Cascade.Circle +{ + /** + * The test case uses the following model: + * + * <- -> + * -- (N : 0,1) -- Tour + * | <- -> + * | -- (1 : N) -- (pickup) ---- + * -> | | | + * Route -- (1 : N) - Node Transport + * | | <- -> | | + * | -- (1 : N) -- (delivery) -- | + * | | + * | -> -> | + * -------- (1 : N) ---- Vehicle--(1 : N)------------ + * + * Arrows indicate the direction of cascade-merge. + */ + + [TestFixture] + public class CascadeMergeToChildBeforeParentTest : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override IList Mappings + { + get { return new[] { "Cascade.Circle.CascadeMergeToChildBeforeParent.hbm.xml" }; } + } + + [Test] + public void Merge() + { + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + Route route = new Route(); + route.Name = "routeA"; + session.Save(route); + transaction.Commit(); + } + + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + Route route = session.Get<Route>(1L); + route.TransientField = "sfnaouisrbn"; + + Tour tour = new Tour(); + tour.Name = "tourB"; + + Node pickupNode = new Node(); + pickupNode.Name = "pickupNodeB"; + + Node deliveryNode = new Node(); + deliveryNode.Name = "deliveryNodeB"; + + pickupNode.Route = route; + pickupNode.Tour = tour; + pickupNode.TransientField = "pickup node aaaaaaaaaaa"; + + deliveryNode.Route = route; + deliveryNode.Tour = tour; + deliveryNode.TransientField = "delivery node aaaaaaaaa"; + + tour.Nodes.Add(pickupNode); + tour.Nodes.Add(deliveryNode); + + route.Nodes.Add(pickupNode); + route.Nodes.Add(deliveryNode); + + Route mergedRoute = (Route)session.Merge(route); + + transaction.Commit(); + } + } + + // This test fails because the merge algorithm tries to save a + // transient child (transport) before cascade-merge gets its + // transient parent (vehicle); merge does not cascade from the + // child to the parent. + [Test] + public void MergeTransientChildBeforeTransientParent() + { + Route route = null; + + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + route = new Route(); + route.Name = "routeA"; + session.Save(route); + transaction.Commit(); + } + + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + route = session.Get<Route>(route.RouteId); + route.TransientField = "sfnaouisrbn"; + + Tour tour = new Tour(); + tour.Name = "tourB"; + + Transport transport = new Transport(); + transport.Name = "transportB"; + + Node pickupNode = new Node(); + pickupNode.Name = "pickupNodeB"; + + Node deliveryNode = new Node(); + deliveryNode.Name = "deliveryNodeB"; + + Vehicle vehicle = new Vehicle(); + vehicle.Name = "vehicleB"; + + pickupNode.Route = route; + pickupNode.Tour = tour; + pickupNode.PickupTransports.Add(transport); + pickupNode.TransientField = "pickup node aaaaaaaaaaa"; + + deliveryNode.Route = route; + deliveryNode.Tour = tour; + deliveryNode.DeliveryTransports.Add(transport); + deliveryNode.TransientField = "delivery node aaaaaaaaa"; + + tour.Nodes.Add(pickupNode); + tour.Nodes.Add(deliveryNode); + + route.Nodes.Add(pickupNode); + route.Nodes.Add(deliveryNode); + route.Vehicles.Add(vehicle); + + transport.PickupNode = pickupNode; + transport.DeliveryNode = deliveryNode; + transport.Vehicle = vehicle; + transport.TransientField = "aaaaaaaaaaaaaa"; + + vehicle.Transports.Add(transport); + vehicle.TransientField = "anewvalue"; + vehicle.Route = route; + + Route mergedRoute = (Route)session.Merge(route); + + transaction.Commit(); + } + } + + [Test] + public void MergeData3Nodes() + { + Route route = null; + + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + route = new Route(); + route.Name = "routeA"; + session.Save(route); + transaction.Commit(); + } + + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + route = session.Get<Route>(route.RouteId); + route.TransientField = "sfnaouisrbn"; + + Tour tour = new Tour(); + tour.Name = "tourB"; + + Transport transport1 = new Transport(); + transport1.Name = "TRANSPORT1"; + + Transport transport2 = new Transport(); + transport2.Name = "TRANSPORT2"; + + Node node1 = new Node(); + node1.Name = "NODE1"; + + Node node2 = new Node(); + node2.Name = "NODE2"; + + Node node3 = new Node(); + node3.Name = "NODE3"; + + Vehicle vehicle = new Vehicle(); + vehicle.Name = "vehicleB"; + + node1.Route = route; + node1.Tour = tour; + node1.PickupTransports.Add(transport1); + node1.TransientField = "node 1"; + + node2.Route = route; + node2.Tour = tour; + node2.DeliveryTransports.Add(transport1); + node2.PickupTransports.Add(transport2); + node2.TransientField = "node 2"; + + node3.Route = route; + node3.Tour = tour; + node3.DeliveryTransports.Add(transport2); + node3.TransientField = "node 3"; + + tour.Nodes.Add(node1); + tour.Nodes.Add(node2); + tour.Nodes.Add(node3); + + route.Nodes.Add(node1); + route.Nodes.Add(node2); + route.Nodes.Add(node3); + route.Vehicles.Add(vehicle); + + transport1.PickupNode = node1; + transport1.DeliveryNode = node2; + transport1.Vehicle = vehicle; + transport1.TransientField = "aaaaaaaaaaaaaa"; + + transport2.PickupNode = node2; + transport2.DeliveryNode = node3; + transport2.Vehicle = vehicle; + transport2.TransientField = "bbbbbbbbbbbbb"; + + vehicle.Transports.Add(transport1); + vehicle.Transports.Add(transport2); + vehicle.TransientField = "anewvalue"; + vehicle.Route = route; + + Route mergedRoute = (Route)session.Merge(route); + + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Transport").ExecuteUpdate(); + session.CreateQuery("delete from Vehicle").ExecuteUpdate(); + session.CreateQuery("delete from Node").ExecuteUpdate(); + session.CreateQuery("delete from Route").ExecuteUpdate(); + session.CreateQuery("delete from Tour").ExecuteUpdate(); + transaction.Commit(); + } + base.OnTearDown(); + } + } +} \ No newline at end of file Added: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/MultiPathCircleCascade.hbm.xml =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/MultiPathCircleCascade.hbm.xml (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/MultiPathCircleCascade.hbm.xml 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<hibernate-mapping + xmlns="urn:nhibernate-mapping-2.2" + assembly="NHibernate.Test" + namespace="NHibernate.Test.Cascade.Circle"> + + <class name="Route" table="HB_Route"> + + <id name="RouteId" type="long"> + <generator class="native" /> + </id> + + <property name="Name" type="string" not-null="true" /> + + <set name="Nodes" inverse="true" cascade="persist,merge,refresh"> + <key column="RouteId" /> + <one-to-many class="Node" /> + </set> + + </class> + + <class name="Tour" table="HB_Tour"> + + <id name="TourId" type="long"> + <generator class="native" /> + </id> + + <property name="Name" type="string" not-null="true" /> + + <set name="Nodes" inverse="true" lazy="true" cascade="merge,refresh"> + <key column="TourId" /> + <one-to-many class="Node" /> + </set> + + </class> + + <class name="Transport" table="HB_Transport"> + + <id name="TransportId" type="long"> + <generator class="native" /> + </id> + + <property name="Name" type="string" not-null="true" /> + + <many-to-one name="PickupNode" + column="PickupNodeId" + unique="true" + not-null="true" + cascade="merge,refresh" + lazy="false" /> + + <many-to-one name="DeliveryNode" + column="DeliveryNodeId" + unique="true" + not-null="true" + cascade="merge,refresh" + lazy="false" /> + + </class> + + <class name="Node" table="HB_Node"> + + <id name="NodeId" type="long"> + <generator class="native" /> + </id> + + <property name="Name" type="string" not-null="true" /> + + <set name="DeliveryTransports" inverse="true" lazy="true" cascade="merge,refresh"> + <key column="DeliveryNodeId" /> + <one-to-many class="Transport" /> + </set> + + <set name="PickupTransports" inverse="true" lazy="true" cascade="merge,refresh"> + <key column="PickupNodeId" /> + <one-to-many class="Transport" /> + </set> + + <many-to-one name="Route" + column="RouteId" + unique="false" + not-null="true" + cascade="none" + lazy="false" /> + + <many-to-one name="Tour" + column="TourId" + unique="false" + not-null="false" + cascade="merge,refresh" + lazy="false" /> + + </class> + +</hibernate-mapping> \ No newline at end of file Added: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/MultiPathCircleCascadeTest.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/MultiPathCircleCascadeTest.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/MultiPathCircleCascadeTest.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,440 @@ +using System; +using System.Collections; +using System.Linq; +using NHibernate.Engine; +using NHibernate.Test; +using NUnit.Framework; + +namespace NHibernate.Test.Cascade.Circle +{ + + /** + * The test case uses the following model: + * + * <- -> + * -- (N : 0,1) -- Tour + * | <- -> + * | -- (1 : N) -- (pickup) ---- + * -> | | | + * Route -- (1 : N) -- Node Transport + * | <- -> | + * -- (1 : N) -- (delivery) -- + * + * Arrows indicate the direction of cascade-merge. + * + * It reproduced the following issues: + * http://opensource.atlassian.com/projects/hibernate/browse/HHH-3046 + * http://opensource.atlassian.com/projects/hibernate/browse/HHH-3810 + * + * This tests that merge is cascaded properly from each entity. + * + * @author Pavol Zibrita, Gail Badner + */ + + [TestFixture] + public class MultiPathCircleCascadeTest : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override IList Mappings + { + get { return new[] { "Cascade.Circle.MultiPathCircleCascade.hbm.xml" }; } + } + + protected override void Configure(NHibernate.Cfg.Configuration configuration) + { + base.Configure(configuration); + configuration.SetProperty(NHibernate.Cfg.Environment.GenerateStatistics, "true"); + configuration.SetProperty(NHibernate.Cfg.Environment.BatchSize, "0"); + } + + [Test] + public void MergeEntityWithNonNullableTransientEntity() + { + Route route = this.GetUpdatedDetachedEntity(); + + Node node = route.Nodes.First(); + route.Nodes.Remove(node); + + Route routeNew = new Route(); + routeNew.Name = "new route"; + routeNew.Nodes.Add(node); + node.Route = routeNew; + + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + try + { + session.Merge(node); + Assert.Fail("should have thrown an exception"); + } + catch (Exception ex) + { + Assert.That(ex, Is.TypeOf(typeof(TransientObjectException))); + +// if (((SessionImplementor)session).Factory.Settings.isCheckNullability() ) { +// assertTrue( ex instanceof TransientObjectException ); +// } +// else { +// assertTrue( ex instanceof JDBCException ); +// } + } + finally + { + transaction.Rollback(); + } + } + } + + [Test] + public void MergeEntityWithNonNullableEntityNull() + { + Route route = GetUpdatedDetachedEntity(); + Node node = route.Nodes.First(); + route.Nodes.Remove(node); + node.Route = null; + + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + try + { + session.Merge(node); + Assert.Fail("should have thrown an exception"); + } + catch (Exception ex) + { + Assert.That(ex, Is.TypeOf(typeof(PropertyValueException))); + +// if ( ( ( SessionImplementor ) s ).getFactory().getSettings().isCheckNullability() ) { +// assertTrue( ex instanceof PropertyValueException ); +// } +// else { +// assertTrue( ex instanceof JDBCException ); +// } + } + finally + { + transaction.Rollback(); + } + } + } + + public void MergeEntityWithNonNullablePropSetToNull() + { + Route route = GetUpdatedDetachedEntity(); + Node node = route.Nodes.First(); + node.Name = null; + + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + try + { + session.Merge(route); + Assert.Fail("should have thrown an exception"); + } + catch (Exception ex) + { + Assert.That(ex, Is.TypeOf(typeof(PropertyValueException))); + +// if ( ( ( SessionImplementor ) s ).getFactory().getSettings().isCheckNullability() ) { +// assertTrue( ex instanceof PropertyValueException ); +// } +// else { +// assertTrue( ex instanceof JDBCException ); +// } + } + finally + { + transaction.Rollback(); + } + } + } + + [Test] + public void MergeRoute() + { + Route route = this.GetUpdatedDetachedEntity(); + + ClearCounts(); + + ISession s = base.OpenSession(); + s.BeginTransaction(); + s.Merge(route); + s.Transaction.Commit(); + s.Close(); + + AssertInsertCount(4); + AssertUpdateCount(1); + + s = base.OpenSession(); + s.BeginTransaction(); + route = s.Get<Route>(route.RouteId); + CheckResults(route, true); + s.Transaction.Commit(); + s.Close(); + } + + [Test] + public void MergePickupNode() + { + Route route = GetUpdatedDetachedEntity(); + + ClearCounts(); + + ISession s = OpenSession(); + s.BeginTransaction(); + + Node pickupNode = route.Nodes.First(n => n.Name == "pickupNodeB"); + pickupNode = (Node)s.Merge(pickupNode); + + s.Transaction.Commit(); + s.Close(); + + AssertInsertCount(4); + AssertUpdateCount(0); + + s = OpenSession(); + s.BeginTransaction(); + route = s.Get<Route>(route.RouteId); + CheckResults(route, false); + s.Transaction.Commit(); + s.Close(); + } + + [Test] + public void MergeDeliveryNode() + { + Route route = GetUpdatedDetachedEntity(); + + ClearCounts(); + + ISession s = OpenSession(); + s.BeginTransaction(); + + Node deliveryNode = route.Nodes.First(n => n.Name == "deliveryNodeB"); + deliveryNode = (Node)s.Merge(deliveryNode); + + s.Transaction.Commit(); + s.Close(); + + AssertInsertCount(4); + AssertUpdateCount(0); + + s = OpenSession(); + s.BeginTransaction(); + route = s.Get<Route>(route.RouteId); + CheckResults(route, false); + s.Transaction.Commit(); + s.Close(); + } + + [Test] + public void MergeTour() + { + Route route = GetUpdatedDetachedEntity(); + + ClearCounts(); + + ISession s = OpenSession(); + s.BeginTransaction(); + Tour tour = (Tour)s.Merge(route.Nodes.First().Tour); + s.Transaction.Commit(); + s.Close(); + + AssertInsertCount(4); + AssertUpdateCount(0); + + s = OpenSession(); + s.BeginTransaction(); + route = s.Get<Route>(route.RouteId); + CheckResults(route, false); + s.Transaction.Commit(); + s.Close(); + } + + [Test] + public void MergeTransport() + { + Route route = GetUpdatedDetachedEntity(); + + ClearCounts(); + + ISession s = OpenSession(); + s.BeginTransaction(); + + Node node = route.Nodes.First(); + Transport transport = null; + + if (node.PickupTransports.Count == 1) + transport = node.PickupTransports.First(); + else + transport = node.DeliveryTransports.First(); + + transport = (Transport)s.Merge(transport); + + s.Transaction.Commit(); + s.Close(); + + AssertInsertCount(4); + AssertUpdateCount(0); + + s = OpenSession(); + s.BeginTransaction(); + route = s.Get<Route>(route.RouteId); + CheckResults(route, false); + s.Transaction.Commit(); + s.Close(); + } + + private Route GetUpdatedDetachedEntity() + { + ISession s = OpenSession(); + s.BeginTransaction(); + + Route route = new Route(); + route.Name = "routeA"; + + s.Save(route); + s.Transaction.Commit(); + s.Close(); + + route.Name = "new routeA"; + route.TransientField = "sfnaouisrbn"; + + Tour tour = new Tour(); + tour.Name = "tourB"; + + Transport transport = new Transport(); + transport.Name = "transportB"; + + Node pickupNode = new Node(); + pickupNode.Name = "pickupNodeB"; + + Node deliveryNode = new Node(); + deliveryNode.Name = "deliveryNodeB"; + + pickupNode.Route = route; + pickupNode.Tour = tour; + pickupNode.PickupTransports.Add(transport); + pickupNode.TransientField = "pickup node aaaaaaaaaaa"; + + deliveryNode.Route = route; + deliveryNode.Tour = tour; + deliveryNode.DeliveryTransports.Add(transport); + deliveryNode.TransientField = "delivery node aaaaaaaaa"; + + tour.Nodes.Add(pickupNode); + tour.Nodes.Add(deliveryNode); + + route.Nodes.Add(pickupNode); + route.Nodes.Add(deliveryNode); + + transport.PickupNode = pickupNode; + transport.DeliveryNode = deliveryNode; + transport.TransientField = "aaaaaaaaaaaaaa"; + + return route; + } + + private void CheckResults(Route route, bool isRouteUpdated) + { + // since merge is not cascaded to route, this method needs to + // know whether route is expected to be updated + if (isRouteUpdated) + { + Assert.That(route.Name, Is.EqualTo("new routeA")); + } + + Assert.That(route.Nodes.Count, Is.EqualTo(2)); + Node deliveryNode = null; + Node pickupNode = null; + + foreach(Node node in route.Nodes) + { + if ("deliveryNodeB".Equals(node.Name)) + { + deliveryNode = node; + } + else if ("pickupNodeB".Equals(node.Name)) + { + pickupNode = node; + } + else + { + Assert.Fail("unknown node"); + } + } + + Assert.That(deliveryNode, Is.Not.Null); + Assert.That(deliveryNode.Route, Is.SameAs(route)); + Assert.That(deliveryNode.DeliveryTransports.Count, Is.EqualTo(1)); + Assert.That(deliveryNode.PickupTransports.Count, Is.EqualTo(0)); + Assert.That(deliveryNode.Tour, Is.Not.Null); + Assert.That(deliveryNode.TransientField, Is.EqualTo("node original value")); + + Assert.That(pickupNode, Is.Not.Null); + Assert.That(pickupNode.Route, Is.SameAs(route)); + Assert.That(pickupNode.DeliveryTransports.Count, Is.EqualTo(0)); + Assert.That(pickupNode.PickupTransports.Count, Is.EqualTo(1)); + Assert.That(pickupNode.Tour, Is.Not.Null); + Assert.That(pickupNode.TransientField, Is.EqualTo("node original value")); + + Assert.That(deliveryNode.NodeId.Equals(pickupNode.NodeId), Is.False); + Assert.That(deliveryNode.Tour, Is.SameAs(pickupNode.Tour)); + Assert.That(deliveryNode.DeliveryTransports.First(), Is.SameAs(pickupNode.PickupTransports.First())); + + Tour tour = deliveryNode.Tour; + Transport transport = deliveryNode.DeliveryTransports.First(); + + Assert.That(tour.Name, Is.EqualTo("tourB")); + Assert.That(tour.Nodes.Count, Is.EqualTo(2)); + Assert.That(tour.Nodes.Contains(deliveryNode), Is.True); + Assert.That(tour.Nodes.Contains(pickupNode), Is.True); + + Assert.That(transport.Name, Is.EqualTo("transportB")); + Assert.That(transport.DeliveryNode, Is.SameAs(deliveryNode)); + Assert.That(transport.PickupNode, Is.SameAs(pickupNode)); + Assert.That(transport.TransientField, Is.EqualTo("transport original value")); + } + + protected override void OnTearDown() + { + using (ISession session = base.OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Transport").ExecuteUpdate(); + session.CreateQuery("delete from Node").ExecuteUpdate(); + session.CreateQuery("delete from Tour").ExecuteUpdate(); + session.CreateQuery("delete from Route").ExecuteUpdate(); + transaction.Commit(); + } + base.OnTearDown(); + } + + protected void ClearCounts() + { + sessions.Statistics.Clear(); + } + + protected void AssertInsertCount(long expected) + { + Assert.That(sessions.Statistics.EntityInsertCount, Is.EqualTo(expected), "unexpected insert count"); + } + + protected void AssertUpdateCount(long expected) + { + Assert.That(sessions.Statistics.EntityUpdateCount, Is.EqualTo(expected), "unexpected update count"); + } + + protected void AssertDeleteCount(long expected) + { + Assert.That(sessions.Statistics.EntityDeleteCount, Is.EqualTo(expected), "unexpected delete count"); + } + } +} \ No newline at end of file Added: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Node.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Node.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Node.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,90 @@ +using System; +using System.Text; +using Iesi.Collections.Generic; + +namespace NHibernate.Test.Cascade.Circle +{ + public class Node + { + private long nodeId; + private long version; + private string name; + private ISet<Transport> deliveryTransports = new HashedSet<Transport>(); + private ISet<Transport> pickupTransports = new HashedSet<Transport>(); + private Route route = null; + private Tour tour; + private string transientField = "node original value"; + + public virtual long NodeId + { + get { return nodeId; } + set { nodeId = value; } + } + + public virtual long Version + { + get { return version; } + set { version = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual ISet<Transport> DeliveryTransports + { + get { return deliveryTransports; } + set { deliveryTransports = value; } + } + + public virtual ISet<Transport> PickupTransports + { + get { return pickupTransports; } + set { pickupTransports = value; } + } + + public virtual Route Route + { + get { return route; } + set { route = value; } + } + + public virtual Tour Tour + { + get { return tour; } + set { tour = value; } + } + + public virtual string TransientField + { + get { return transientField; } + set { transientField = value; } + } + + public override string ToString() + { + var buffer = new StringBuilder(); + + buffer.AppendFormat("{0}, id: {1}", name, nodeId); + + if (route != null) + buffer.AppendFormat(" route name: {0}, tour name: {1}", route.Name, tour == null ? "null" : tour.Name); + + if (deliveryTransports != null) + { + foreach(Transport transport in deliveryTransports) + buffer.AppendFormat("Delivery Transports: {0}", transport.ToString()); + } + + if (route != null) + { + foreach(Transport transport in pickupTransports) + buffer.AppendFormat("Node: {0}", transport.ToString()); + } + + return buffer.ToString(); + } + } +} \ No newline at end of file Added: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Route.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Route.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Route.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,67 @@ +using System; +using System.Text; +using Iesi.Collections.Generic; + +namespace NHibernate.Test.Cascade.Circle +{ + public class Route + { + private long routeId; + private long version; + private ISet<Node> nodes = new HashedSet<Node>(); + private ISet<Vehicle> vehicles = new HashedSet<Vehicle>(); + private string name; + private string transientField = null; + + public virtual long RouteId + { + get { return routeId; } + set { routeId = value; } + } + + public virtual long Version + { + get { return version; } + set { version = value; } + } + + public virtual ISet<Node> Nodes + { + get { return nodes; } + set { nodes = value; } + } + + public virtual ISet<Vehicle> Vehicles + { + get { return vehicles; } + set { vehicles = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual string TransientField + { + get { return transientField; } + set { transientField = value; } + } + + public override string ToString() + { + var buffer = new StringBuilder(); + + buffer.AppendFormat("Route name: {0}, id: {1}, transientField: {2}", name, routeId, transientField); + + foreach(Node node in nodes) + buffer.AppendFormat("Node: {0}", node.ToString()); + + foreach(Vehicle vehicle in vehicles) + buffer.AppendFormat("Vehicle: {0}", vehicle.ToString()); + + return buffer.ToString(); + } + } +} \ No newline at end of file Added: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Tour.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Tour.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Tour.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,37 @@ +using System; +using Iesi.Collections.Generic; + +namespace NHibernate.Test.Cascade.Circle +{ + public class Tour + { + private long tourId; + private long version; + private string name; + private ISet<Node> nodes = new HashedSet<Node>(); + + public virtual long TourId + { + get { return tourId; } + set { tourId = value; } + } + + public virtual long Version + { + get { return version; } + set { version = value; } + } + + public virtual string Name + { + get { return name; } + set { name = value; } + } + + public virtual ISet<Node> Nodes + { + get { return nodes; } + set { nodes = value; } + } + } +} \ No newline at end of file Added: trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Transport.cs =================================================================== --- trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Transport.cs (rev 0) +++ trunk/nhibernate/src/NHibernate.Test/Cascade/Circle/Transport.cs 2011-01-17 15:04:02 UTC (rev 5361) @@ -0,0 +1,63 @@ +using System; +using Iesi.Collections.Generic; + +namespace NHibernate.Test.Cascade.Circle +{ + public class Transport + { + private long transportId; + private long version; + private string name; + private Node pickupNode = null; + private Node deliveryNode = null; + private Vehicle vehicle; + private string transientField = "transport original value"; + + public virtual long TransportId + { + get { return transportId; } + set { transportId = value; } + } + + public virtual long Version + { + get { return version; } + set { version = value; } + } + + publi... [truncated message content] |