Question

I am getting a list of objects separately (and not from NHibernate) and setting the parent objects IEnumerable equal to this returned object. Originally, we only needed to read the objects. Then we had the need to update only specific fields on the parent. Recently, we needed to update fields on the child. So far, all was well with SaveOrUpdate(). Now, I need to update the children even if the collection of children was attached to a detached parent object (not using NHibernate). The following code results in the parent being updated, but not the children. If I do all, then the children would be deleted if the parent does not have a collection. I do not want to do this because I am worried about legacy uses of this that does not account for this behavior.

DESIRED BEHAVIOR:
1. Cascade any changes to the collection (whether in the parent was retrieved by NHibernate or not). 2. Do not delete objects even if the parent does not have a collection of children.

Is this possible?

This is our NHibernate save method:

[Transaction]
public int? Save(DocumentFieldDTO entity, bool autoFlush)
{
    var persisted = CurrentSession.Merge(entity);

    entity.DocumentFieldID = persisted.DocumentFieldID;
    if (autoFlush) { CurrentSession.Flush(); }
    return entity.DocumentFieldID;
}

The DocumentFieldDTOMap looked like this:

public class DocumentFieldDTOMap : EntityMapBase
{

    public DocumentFieldDTOMap()
    {    
        Table("DocumentField");

        Id(m => m.DocumentFieldID).GeneratedBy.Increment().UnsavedValue(null);

        Map(x => x.Name);

        Map(x => x.DocumentSectionID).Not.Update();
        // .... Lots of other fields ....//

        HasMany(x => x.DocumentFieldOrgs)
        .Cascade.SaveUpdate()
        .LazyLoad()
        .KeyColumn("DocumentFieldID");
        }
    }

}

If I change Cascade.SaveUpdate() to Cascade.All() the updates work, but will also delete. I want to eliminate the delete capability.

UPDATE (1/27/2014):

I just verified that the deletes were cascading when the mapping was SaveUpdate(), so this isn't as big an issue since I am not changing the existing functionality. I would still like to be able to update all cascading EXCEPT delete. An solution, if possible, would be great for future reference.

UPDATE (2/10/2014)

The following is the tests that verify that the children are deleted when cascade is "SaveUpdate()". The GetDocumentFieldDTOWithADO(DocumentFieldID) uses the same transaction as NHibernate and has 318 DocumentFieldOrgs on the first call (before the save) and 0 when called after the save. Maybe there is an issue with the test? Does it delete the children because I call Merge?

    [Test]
    public void Save_ShouldDeleteDocumentFieldOrgs_WhenSavingDocumentFieldWithoutDocFieldOrgsList()
    {
        //arrange
        var expectedDocField = GetDocumentFieldDTOWithADO(DocumentFieldID);
        expectedDocField.DocumentFieldOrgs = null;

        //act
        Repository.Save(expectedDocField, false);
        SessionFactory.GetCurrentSession().FlushAndEvict(expectedDocField);

        //assert
        var actualDocField = GetDocumentFieldDTOWithADO(DocumentFieldID);

        actualDocField.DocumentFieldOrgs.Should()
            .BeEmpty("DocumentFieldOrgs should be deleted if the parent does not have a child collection");        
    }

UPDATED (2/11/2014) - Radim was correct in his answer below. NHibernate did not delete the children. It disassociated them from the parent.

Était-ce utile?

La solution

UPDATE, reflecting the query changes

The test you've showed in the query update, has proven:

If the parent.Children is set to null and persited, it will have NO children - next time accessed.

Let me explain what happened, let me use some virtual language (Attention I am using Parent and Children to make it simple)

1) mapping of the parent child is cascade="save-update"
This is an information for NHibernate that during creation or amending, children collection should be passed the Save() or Update() calls

2) Let's load parent

var session = ... // get a ISession for our test
var parent = session.Get<Parent>(1); // e.g. DocumentFieldDTO 

// NOT Empty -- is true:  IsNotEmpty()
Assert.IsTrue(parent.Children.IsNotEmpty()); // e.g. DocumentFieldOrgs

3) Now, remove the reference and check what NHibernate will do:

parent.Children = null;

session.Flush();
session.Clear();

Here is the SQL statement executed:

exec sp_executesql N'UPDATE [schema].[Child_Table] 
      SET ParentId = null WHERE ParentId = @p0',N'@p0 int',@p0=1

As we can see, because of the mapping save-update, NHibernate did handle this scenario, by removing the reference. In fact by UPDATing the child table

4) Load Parent again

parent = session.Get<Parent>(1);

// EMPTY -- is true: IsEmpty()
Assert.IsTrue(parent.Children.IsEmpty());

Summary: As we've seen above, NHibernate is doing what was mapped, and expected. No delete. Just an update, removing the reference

PREVIOUS part of this Answer

The answer is: change your public int? Save(...) implementation. NHibernate cascading is working as expected, please read more here Ayende, NHibernate Cascades: the different between all, all-delete-orphans and save-update

First take a look at the statement above:

DESIRED BEHAVIOR:
1) Cascade any changes to the collection (whether in the parent was retrieved by NHibernate or not).
2) Do not delete objects even if the parent does not have a collection of children.

The bold parts are the reason, why the Cascade concept is not working. Because:

Cascade makes sense only if an operation on
the existing parent is cascaded / repeated / passed
the existing/known child(children)

NHiberante cascade implementation realy does work this way: 9.9. Lifecyles and object graphs (an extract)

Mapping ... with cascade="all" marks the association as a parent/child style relationship where save/update/deletion of the parent results in save/update/deletion of the child(ren). ... A child which becomes unreferenced by its parent is not automatically deleted, except in the case of a <one-to-many> association mapped with cascade="all-delete-orphan"...

Not only it is not deleted. If it is not referenced, it does not recieve a trigger to any type of cascading operation.

Suggestion:

Adjust the Save() method, to do two operations:

  1. Update the parent
  2. Find the "children" or better - the somehow related items. Load them adjust them, and call session.Flush(). Any changes on objects referenced by ISession will be persisted.
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top