Try fast search NHibernate

11 September 2009

NHibernate: Tree Re-parenting

Did you heard about NHibernate and Hibernate re-parenting issue?

I heard it many times in these years including in our JIRA.

Depending on how you are managing your system there are various solutions to manage the re-parenting issue. The most common is set the cascade style to “all” and manage orphaned nodes in some way.

The last time the user said:

Nhibernate should handle cases where child objects are moved from one parent to another by updating the foreign key column in the child record.
Nhibernate should handle cases where child objects are removed from a parent by deleting the orphan objects.

From my understanding I would like cascade=all-delete-orphan to be intelligent enough to handle orphans as orphans and children with new parents not as orphans (they have parents, just not the same ones)
Make sense?

NHibernate is a persistent-layer and rules about how manage a tree are out from persistent-layer scope. The fact that NHibernate offer you the cascade-feature does not mean that it can work in any of your cases especially when the logic is your logic.

Words as “intelligent”/”smart” or “silly” are subjective opinions. In my opinion what is really important is not if a framework is silly or not but if the framework give me the opportunity to define what is intelligent for me.

That said let go to see if NHibernate is the framework I’m looking for.

The repository

Domain

private void FillDb()
{
var rep = new List<Node>();
for (int i = 0; i < 2; i++)
{
var gp = new Node {Description = "N" + i.ToString("00")};
rep.Add(gp);
for (int j = 0; j < 5; j++)
{
var c = new Node { Description = gp.Description + "-" + j.ToString("00") };
gp.AddChild(c);
for (int k = 0; k < 4; k++)
{
var cc = new Node { Description = c.Description + "-" + k.ToString("00") };
c.AddChild(cc);
}
}
}
using (ISession s = sessions.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
foreach (var node in rep)
{
s.Save(node);
}
tx.Commit();
}
}

Two roots, each with five children and each child with four children.

The target

Usage of delete-orphan cascade-style, making even your DAO/Repository persistence ignorant (no overhead to manage orphans delete).

The test

[Test]
public void TryingDisaster()
{
FillDb();

using (ISession s = sessions.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
var tree = s.CreateQuery("from Node n where n.Parent is null").List<Node>();


// Move first 2 children of root-0 to root-1
var children = new List<Node>(tree[0].Children);

tree[1].AddChild(children[0]);
tree[1].AddChild(children[1]);

// remove others children from root-1
children = new List<Node>(tree[0].Children);
foreach (var child in children)
{
tree[0].RemoveChild(child);
}

// move all nodes from actual root-1-child-0 to root-0
ReparentingAllChildren(tree[1].Children.First(), tree[0]);

tx.Commit();
}

using (ISession s = sessions.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
var tree = s.CreateQuery("from Node n where n.Parent is null").List<Node>();

tree.Count.Should("have only two roots").Be.EqualTo(2);

tree.Should("one should have four children")
.Satisfy(a => a.Any(x => x.Children.Count() == 4));

// get the root-1
var root1 = tree.FirstOrDefault(x => x.Children.Count() == 7);

root1.Should("have 7 children").Not.Be.Null();

// one of children should be empty (children was moved to root-0)
root1.Children.Should()
.Satisfy(a => a.Any(x => x.Children.Count() == 0));

// others children, should be intact
root1.Children.Count(c=> c.Children.Count() == 4).Should().Be.EqualTo(6);

tx.Commit();
}
ClearDb();
}

enough ?

The solution

Mapping
<class name="Node">
<
id name="Id">
<
generator class="hilo">
<
param name="max_lo">99</param>
</
generator>
</
id>
<
property name="Description"/>
<
many-to-one name="Parent" access="field.camelcase"/>
<
set name="Children" collection-type="TreeNodesCollectionType"
inverse="true" cascade="all, delete-orphan" access="field.camelcase">
<
key column="Parent"/>
<
one-to-many class="Node"/>
</
set>
</
class>
Custom collection type
public class TreeNodesCollectionType: IUserCollectionType
{
#region Implementation of IUserCollectionType

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

public IPersistentCollection Wrap(ISessionImplementor session, object collection)
{
return new PersistentNodeSet(session, (ISet<Node>)collection);
}

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

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

public object IndexOf(object collection, object entity)
{
throw new NotSupportedException();
}

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

foreach (var o in (IEnumerable)original)
result.Add((Node)o);

return result;
}

public object Instantiate(int anticipatedSize)
{
return new HashedSet<Node>();
}

#endregion
}
Custom persistent collection
public class PersistentNodeSet : PersistentGenericSet<Node>
{
public PersistentNodeSet() {}

public PersistentNodeSet(ISessionImplementor session) : base(session) {}

public PersistentNodeSet(ISessionImplementor session, ISet<Node> original) : base(session, original) {}

public override System.Collections.ICollection GetOrphans(object snapshot, string entityName)
{
return base.GetOrphans(snapshot, entityName)
.Cast<Node>()
.Where(n=> ReferenceEquals(null,n.Parent))
.ToArray();
}
}

The custom type and custom persistent collection can be implemented in a more reusable way; where needed we can publish the implementation in uNhAddIns.

Conclusion

Perhaps NHibernate is silly but give you the opportunity to demonstrate your intelligence.

Code available here.

8 comments:

  1. Nice post, I think many newcomers to NH seem frustrated because reparenting in SQL is so easy, just change the parentId. But from the OO paradigm, which is the correct perspective when working with NH, that doesn't make much sense.

    ReplyDelete
  2. I may be wrong but I would rather create new children by copying value over from the existing children and then delete them. Even though it's tempting to just change the parent, one should first ask oneself some other questions: what is the type of the relation between the parent and its children, a composition or an aggregation ? Are the children reference or value objects ?

    ReplyDelete
  3. @Stiiifff
    Make the question to who want manage the tree in this way.
    This post is specific for those are thinking "I have a problem and NH is not intelligent enough to understand my way... so now I don't have the problem any more because the real problem is NH".
    The more easy way to solve a problem is give the blame to somebody else.

    ReplyDelete
  4. Personally I don't see the problem only the solution :)

    ReplyDelete
  5. Exactly, what should this mean if "magic" happened behind the scene (assuming that many devs would expose relationship as a List).
    Parent.Children[4] = someOtherChildThatHasDifferentParent

    ReplyDelete
  6. @exp2000
    No problem. The solution is the same implementing a List instead a Set. The difference come in play when you want early loading.

    ReplyDelete
  7. I was more thinking that NH should not support re-parenting by default, because maybe in my application I don't want this magic, I want explicit remove from one parent and adding too other. I, for one, also do not expose anything other then IEnumerable for many relationships as pubic api. I want explicitness. Same goes for bi-directional synchronization.

    ReplyDelete
  8. @exp2000
    There are various ways to manage a tree. For example if you are using valueObjects you don't need the impl. of this post... each of us may have a different solution the matter is show it some where in some way.
    The nice thing is that NHibernate is allowing various ways... even your ;)

    ReplyDelete