In the previous posts I've described how to teach NHibernate about our bidirectional many-to-many relationships using Inverse attribute and I've gone through collection types used in many-to-many mapping to see how they differ in terms of performance. Time to sum up the topic of many-to-many relationship mappings with a bit of guidance.
1. Map one side of many-to-many relationship with Inverse attribute
NHibernate should trigger database writes from one side only, otherwise you'll end up with duplicated values in intermediate table or primary key violation errors. From database perspective, Inverse side becomes read-only, it is only modified at objects level.
2. Add relationships at both ends
For bidirectional relationships, create a single method responsible for adding the relationship between two entities. The method should modify the collections at both sides of the relationship at once, to ensure the objects state is always correct. When you've added Group to User, always add User to Group, too. NHibernate doesn't reload the entities within single session, so it is not able to figure out the changes at the second side of the relationship automatically.
3. Avoid mapping Inverse side of many-to-many
In some cases you don't really need to map both sides of the relationship. If your application is not going to query the database for data from the Inverse side, you'd better not map this side at all. Less mappings, less bugs. And there is no risk of forgetting to update collections at both sides as there is one side only.
4. Map Inverse side using bag
Inverse side of the relationship from the database perspective is read-only - it doesn't trigger any writes. Read-only collections can be mapped as bag as we don't need to care about write penalties. And accessing the collection will always load all its values, regardless of the collection type, so it's best to use the simplest one here.
5. Map active side using set
Set ensures uniqueness. It's good to have uniqueness in many-to-many relationships as there are almost no use cases for non-unique many-to-many relationship. Moreover, set is much better for updates - it can add/update/remove single rows, contrary to bag, which is always deleting and re-creating all relations.
6. Map smaller side as active
ISet Add/Remove methods return boolean indicating whether modifying the set succeeded (it can fail due to set's uniqueness constraint). To determine the proper return value, NHibernate needs to load the collection when modifying it. To ensure the best performance, we should think which side of the relationship is expected to have less values and then map this side as active set and the second one as Inverse bag. It's always good to load as few values as possible and there's no difference which side is Inverse from object-oriented perspective, as on object level both sides behaves identically.
Below is the correct mapping for our Users/Groups example. I've mapped user's groups collection as active set, as I expect one user to have only a few groups. I've mapped group's user list as bag and marked it as Inverse, as single group can possibly have thousands of members.
// in UserMap
HasManyToMany(x => x.Groups).AsSet();
// in GroupMap
HasManyToMany(x => x.Users).AsBag().Inverse();
And HBM version:
<!-- in User.hbm.xml -->
<set name="Groups" table="UsersInGroups">
<key column="UserId" />
<many-to-many column="GroupId" class="Group" />
</set>
<!-- in Group.hbm.xml -->
<bag name="Users" table="UsersInGroups" inverse="true">
<key column="GroupId" />
<many-to-many column="UserId" class="User" />
</bag>
Below is the metod to add the relationship. This is single method that touches both sides of the relationship at once, but only User side triggers the database call, as the Group side is read-only at database level. I've decided to put the method within Group class, as it fits there logically.
// in Group
public virtual void AddMember(User user)
{
this.Users.Add(user);
user.Groups.Add(this);
}
This post was a wonderful lifesaver! NHibernate kept removing all the items in my M-M Collection when I just added or removed a single entry. Rewriting the entire many to many collection is no fun. Switching my Fluent NHibernate HasManyToMany to .AsSet() and using System.Collections.Generic.ICollection solved it!
ReplyDeleteThanks!
Interesting behaviour!
ReplyDelete