Как изменить дочерний родительский элемент в NHibernate, когда каскад является удаленным-все-сиротой?

У меня есть два объекта в двунаправленном соотношении один-ко-многим:

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

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

И отображение:

<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 может иметь много Boxes, но a Box может принадлежать только одному Storage. Я их сопоставил так, что один-ко-многим имеет каскад all-delete-orphan.

Моя проблема возникает, когда я пытаюсь изменить Box Storage. Предполагая, что я уже запускал этот код:

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

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

Следующий код даст мне исключение:

// 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 been removed from storage1
var storage2 = Database.GetStorage().Second();
storage2.Boxes.Add(existingBox);

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

Я получаю следующее исключение:

NHibernate.ObjectDeletedException: удаленный объект будет повторно сохранен cascade (удалить удаленный объект из ассоциаций)

Это исключение возникает, потому что у меня есть каскадный набор all-delete-orphan. Первый Storage обнаружил, что я удалил Box из своей коллекции и пометил ее для удаления. Однако, когда я добавил его ко второму Storage (в том же сеансе), он попытается снова сохранить поле и ObjectDeletedException.

Мой вопрос: как мне получить Box, чтобы изменить родительский Storage, не встречая этого исключения? Я знаю, что одним из возможных решений является изменение каскада только на all, но затем я теряю способность NHibernate автоматически удалять Box, просто удаляя его из Storage и не переписывая его другим. Или это единственный способ сделать это, и мне нужно вручную вызвать Session.Delete в поле, чтобы удалить его?

Ответ 1

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

В основном, это сводится к этому... Вам нужно определить тип настраиваемой коллекции для NHibernate, который переопределяет то, что значит быть сиротой. Поведение NHibernate по умолчанию должно выполняться так же, как вы обнаружили, - считать ребенка сиротой, если он был удален из родителя. Вместо этого вам нужно NHibernate для проверки дочернего элемента, чтобы узнать, был ли он назначен новому родителю. NHibernate не делает этого по умолчанию, потому что для этого потребуется дополнительная информация о отображении "один ко многим" - ему нужно знать имя соответствующего свойства "много-к-одному" для дочернего элемента.

Измените сопоставление Storage, чтобы выглядеть так:

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

Определите новый тип с именем StorageBoxBag (обратите внимание - этот код написан на NHibernate 2.1 - если вы используете NH3, вам может потребоваться немного изменить его):

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

... и новый тип с именем 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;
    }
}

Метод GetOrphans - это то, где происходит волшебство. Мы запрашиваем NHibernate для списка Box es, который, по его мнению, является сиротами, а затем фильтрует это только до набора Box es, которые фактически являются сиротами.