14

I have two entities in a bi-directional one-to-many relationship:

public class Storage
{
    public IList<Box> Boxes { get; set; }
}

public class Box
{
    public Storage CurrentStorage { get; set; }
}

And the mapping:

<class name="Storage">
    <bag name="Boxes" cascade="all-delete-orphan" inverse="true">
        <key column="Storage_Id" />
        <one-to-many class="Box" />
    </bag>
</class>

<class name="Box">
    <many-to-one name="CurrentStorage" column="Storage_Id" />
</class>

A Storage can have many Boxes, but a Box can only belong to one Storage. I have them mapped so that the one-to-many has a cascade of all-delete-orphan.

My problem arises when I try to change a Box's Storage. Assuming I already ran this code:

var storage1 = new Storage();
var storage2 = new Storage();
storage1.Boxes.Add(new Box());

Session.Create(storage1);
Session.Create(storage2);

The following code will give me an exception:

// get the first and only box in the DB
var existingBox = Database.GetBox().First();

// remove the box from storage1
existingBox.CurrentStorage.Boxes.Remove(existingBox);

// add the box to storage2 after it's been removed from storage1
var storage2 = Database.GetStorage().Second();
storage2.Boxes.Add(existingBox);

Session.Flush(); // commit changes to DB

I get the following exception:

NHibernate.ObjectDeletedException : deleted object would be re-saved by cascade (remove deleted object from associations)

This exception occurs because I have the cascade set to all-delete-orphan. The first Storage detected that I removed the Box from its collection and marks it for deletion. However, when I added it to the second Storage (in the same session), it attempts to save the box again and the ObjectDeletedException is thrown.

My question is, how do I get the Box to change its parent Storage without encountering this exception? I know one possible solution is to change the cascade to just all, but then I lose the ability to have NHibernate automatically delete a Box by simply removing it from a Storage and not re-associating it with another one. Or is this the only way to do it and I have to manually call Session.Delete on the box in order to remove it?

Daniel T.
  • 37,212
  • 36
  • 139
  • 206
  • What happens if you never remove the box from storage1? If just you move it into storage2, wouldn't the CurrentStorage get overwritten? I'm not sure if that works if storage1 is already loaded in the session. – dotjoe May 04 '10 at 13:52
  • It works, but until I refresh my entities, I'll have a copy of the box in both storages. I'd rather have my data model be correct rather than relying on NHib to do the right thing when I retrieve the entity. – Daniel T. May 04 '10 at 18:26
  • Ah yes, the orphan will always be deleted if you remove from the collection. I think in this case you'd want to do as you said at the end, you'd want to `cascade="all"` and delete a box by removing from the collection and calling `session.Delete(box)`. I don't think you can have the best of both worlds :( – dotjoe May 04 '10 at 20:56
  • Thanks dotjoe, can you add your post as an answer so I can mark it as the solution? – Daniel T. May 05 '10 at 00:03

1 Answers1

9

See http://fabiomaulo.blogspot.com/2009/09/nhibernate-tree-re-parenting.html

Basically, it boils down to this... You need to define a custom collection type for NHibernate that re-defines what it means to be an orphan. NHibernate's default behavior is to do just as you discovered - to consider a child to be orphaned if it has been removed from the parent. Instead, you need NHibernate to test the child to see if it has been assigned to a new parent. NHibernate does not do this by default because it would require additional information on the one-to-many mapping - it would need to know the name of the corresponding many-to-one property on the child.

Change your Storage mapping to look like this:

<class name="Storage">
    <bag name="Boxes" cascade="all-delete-orphan" inverse="true" collection-type="StorageBoxBag">
        <key column="Storage_Id" />
        <one-to-many class="Box" />
    </bag>
</class>

Define a new type named StorageBoxBag (note - this code is written against NHibernate 2.1 - if you are using NH3 you may have to tweak this a bit):

public class StorageBoxBag : IUserCollectionType
{
    public object Instantiate(int anticipatedSize)
    {
        return new List<Box>();
    }

    public IPersistentCollection Instantiate(ISessionImplementor session, ICollectionPersister persister)
    {
        return new PersistentStorageBoxBag(session);
    }

    public IPersistentCollection Wrap(ISessionImplementor session, object collection)
    {
        return new PersistentStorageBoxBag(session, (IList<Box>)collection);
    }

    public IEnumerable GetElements(object collection)
    {
        return (IEnumerable)collection;
    }

    public bool Contains(object collection, object entity)
    {
        return ((IList<Box>)collection).Contains((Box)entity);
    }

    public object IndexOf(object collection, object entity)
    {
        return ((IList<Box>) collection).IndexOf((Box) entity);
    }

    public object ReplaceElements(object original, object target, ICollectionPersister persister, object owner, IDictionary copyCache, ISessionImplementor session)
    {
        var result = (IList<Box>)target;
        result.Clear();

        foreach (var box in (IEnumerable)original)
            result.Add((Box)box);

        return result;
    }
}

... and a new type named PersistentStorageBoxBag:

public class PersistentStorageBoxBag: PersistentGenericBag<Box>
{
    public PersistentStorageBoxBag(ISessionImplementor session)
        : base(session)
    {
    }

    public PersistentStorageBoxBag(ISessionImplementor session, ICollection<Box> original)
        : base(session, original)
    {
    }

    public override ICollection GetOrphans(object snapshot, string entityName)
    {
        var orphans = base.GetOrphans(snapshot, entityName)
            .Cast<Box>()
            .Where(b => ReferenceEquals(null, b.CurrentStorage))
            .ToArray();

        return orphans;
    }
}

The GetOrphans method is where the magic happens. We ask NHibernate for the list of Boxes that it thinks are orphans, and then filter that down to only the set of Boxes that actually are orphans.

Daniel Schilling
  • 4,829
  • 28
  • 60
  • Nice answer, Daniel! Is there a way to specify the custom collection-type using Fluent NHibernate? – MylesRip Apr 12 '11 at 22:28
  • Google led me to this answer: http://stackoverflow.com/questions/2899576/automapping-custom-collections-with-fluentnhibernate/2900889#2900889 ...though I've never tried it. If all else fails, you can set up your NHibernate initialization code to first auto map certain classes, then ClassMap the ones that can't be automapped, and finally write *.hbm.xml files for the ones that can't be ClassMapped. – Daniel Schilling Apr 25 '11 at 20:11