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