3
votes

When using cascade="all-delete-orphan" NHibernate is deleting a child row that is not orphaned - it has simply been moved to a new parent.

using (var session = sessionFactory.OpenSession())
{
    using (var transaction = session.BeginTransaction())
    {
        // Get the store we're moving him to 
        Store newStore = session.QueryOver<Store>().Where(...).SingleOrDefault();

        // Get existing employee 
        Employee jack = session.QueryOver<Employee>().Where(...).SingleOrDefault();

        // Do the move
        jack.Store.Staff.Remove(jack);
        jack.Store = newStore;
        jack.Store.Staff.Add(jack);

        transaction.Commit(); 
    }
}

When the commit occurs, a single DELETE statement is generated to delete 'jack' from the database. This behavior would make sense if 'jack' had, in fact, been orphaned but he should now be happily assigned to his new store.

If I change the cascade to "all", the expected UPDATE statement is generated and 'jack' is happily reassigned as expected. However, this causes genuine orphaned rows to remain in the database which is not acceptable.

Is this a bug or is there something I am doing wrong?

Here are the classes:

public class Store
{
    public virtual Guid Id { get; private set; }
    public virtual string Name { get; set; }
    public virtual IList<Employee> Staff { get; set; }
}

public class Employee
{
    public virtual Guid Id { get; private set; }
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual Store Store { get; set; }
}

and the FluentNHibernate mappings:

public class EmployeeMap : ClassMap<Employee>
{
    public EmployeeMap()
    {
        Id(x => x.Id).GeneratedBy.Guid();
        Map(x => x.FirstName);
        Map(x => x.LastName);
        References(x => x.Store);
    }
}

public class StoreMap : ClassMap<Store>
{
    public StoreMap()
    {
        Id(x => x.Id).GeneratedBy.Guid();
        Map(x => x.Name);
        HasMany(x => x.Staff)
          .Inverse()
          .Cascade.AllDeleteOrphan();
    }
}

UPDATE

After some more testing on this it does seem to be a bug, in that the order of loading the objects alters the outcomes (note the Move Employee is done as above in both cases):

1) Load Store B, Load Store A, Find Employee in A.Staff, Move Employee to B ==> Employee is deleted (No exception is thrown, Employee is not reinserted)

2) Load Store A, Load Store B, Find Employee in A.Staff, Move Employee to B ==> ObjectDeletedException (deleted object would be re-saved by cascade (remove deleted object from associations))

The second case appears to be the scenario referred to by @Cole W and discussed here and here and a known limitation of orphan handling in NHibernate.

However, the first case appears to be a bug. In my understanding there should not be a scenario where the order in which objects are loaded alters the database changes made by NHibernate. It appears to be a bug that potentially causes data loss.

UPDATE 2

Given the inconsistent behavior that is dependent on load order and the potential for data loss I have logged this as a bug in NHibernate JIRA. The bug report has full code and mappings to demonstrate the problem.

2
Your example class file for Store does not have the Staff property mentioned in the example code, please could you update?Alex Norcliffe
Is it a problem that it deletes it and re-inserts it? I realize that it would be more efficient just to do an update but I'm not sure it's that big of a deal if it's all done within a transaction. Also I would expect that this is the expected behavior of NH.Cole W
@Cole W, unfortunately the row is not reinserted either. The row is simply deleted. In any case, this example is simplified test case of the real problem. In the real scenario there are many cascading operations that have to occur as a result of delete. In the real app delete/insert is about 10,000 times slower than update.Phil Degenhardt

2 Answers

5
votes

This actually seems like a common problem among others using NHibernate. Check these articles out to see if they help you.

Check out this SO article: Fluent NHibernate exception moving objects between collections

Also linked in that article is this post by Fabio Maulo:
http://fabiomaulo.blogspot.com/2009/09/nhibernate-tree-re-parenting.html

0
votes

The issues are likely caused by the overwriting of multiple references without telling NHibernate what's going on

Quick stab at it whilst we're waiting for the updated Q (see my comment re the missing Staff property) I would recommend changing your query so that you ask directly for the Store rather than using the Store navigator on your Employee object. Remove jack from that Store.Staff collection, issue a session.SaveOrUpdate(store), and then try overwriting the jack.Store reference to the new one (issuing another SaveOrUpdate before then committing the transaction)