Saturday, November 26, 2011

Many-to-many mapping: Inverse

In this and few next posts I'll try to go over the rules and guidelines and give some insights into how to map many-to-many relationship in NHibernate properly.

In general, it is good for relationships in object model to follow the relationships at database level, so that querying for objects can be naturally translated into SQL queries without any additional overhead.

This is quite easy and natural for parent-child relationship (aka many-to-one). Child object can have a reference to its parent object as well as child row can have a foreign key to its parent row.

Many-to-many relationships (i.e. Users and Groups) are different. In fact, they can exist only at object model level - at database level it is only a concept implemented using two many-to-one relations with intermediate table.

We can implement it the same way in our NHibernate-based object model, but the intermediate entity will probably have no additional properties and this will lead to quite polluted and ugly model. We don't want User and Group to have collections of some UserInGroup objects that have no real object-oriented meaning. We do want User to have many Groups and Group to have many Users - as simple as it can be.

Fortunately, this is quite common scenario and NHibernate can support it very well with respect to database-level constraints and good practices, but only when mapped and used with care.

Let's begin with the simplest possible mapping using FluentNHibernate:

// in UserMap
HasManyToMany(x => x.Groups);

// in GroupMap
HasManyToMany(x => x.Users);

Now let's add two users to a group. To ensure our objects state is correct and collections both at User and Group side are complete, we need to update the collections on both sides:

using (var sess = sessionFactory.OpenSession())
using (var tx = sess.BeginTransaction())
{
var user1 = new User() { Name = "u1" };
sess.Save(user1);

var user2 = new User() { Name = "u2" };
sess.Save(user2);

var group1 = new Group() { Name = "g" };
sess.Save(group1);

user1.Groups.Add(group1);
group1.Users.Add(user1);

user2.Groups.Add(group1);
group1.Users.Add(user2);

tx.Commit();
}

But NHibernate is not meant to guess that when adding one relationship on User side in the 13. and 16. line and then adding another one on Group side in the next line we are in fact specifying the same single relationship. What is the result for lines 13-17?

Each statement in the code above triggers separate INSERT statement, duplicating each relationship. At database level it means that each user is a group member twice. Moreover, when GroupsToUsers table has a primary key defined on both columns (and it probably should have), the database will complain about primary key violation and we'll end up with an exception.

What we need to do here is to inform NHibernate that we're defining the same relation on both ends and it should be represented as one row in the database. We need to choose which side is responsible to do the database insert and mark the other with inverse attribute. For example, let's choose User as inverse and make group.Users.Add(user) trigger the database call.

// in UserMap
HasManyToMany(x => x.Groups).Inverse();

// in GroupMap
HasManyToMany(x => x.Users);

(We'll cover how to choose the inverse side properly later.)

What is the result for lines 13-17 now?

So far, so good. But it's far from correctness yet. By now, it works without overhead only for entitles created just before so that NHibernate knows the current collections state. But things looks much worse when the entities are loaded from the database. We'll look closer at the problem and its solution in the next post.

No comments:

Post a Comment