Wednesday, 29 July 2009

NHibernate N:N relationships

NHibernate provides the facility to map out many-to-many relationships without the need for the intersection table to be represented in the object model. Take for example, the following schema of Users, Roles and the intersection table that links users to many roles and roles to many users;

image

We then have the following object model to represent this;

image

or, in code if you prefer;

public class User
{
    // Fields
    public virtual Guid Id { get; set; }
    public virtual string UserName { get; set; }
    public virtual string Email { get; set; }

    // Storage for the roles that this user belongs to
    private readonly IList<Role> _roles = new List<Role>();

    // Accessor to list of roles - note we don't return list
    // that we can add/remove from - we use methods to do that
    // so we can implement any business logic.
    public virtual ReadOnlyCollection<Role> Roles
    {
        get 
        { 
            return new ReadOnlyCollection<Role>(_roles);
        }
    }

    // Method to associate a role with this user
    public virtual void AddRole( Role role )
    {
        _roles.Add( role );
    }

    // Method to remove a role from this user
    public virtual void RemoveRole( Role role )
    {
        _roles.Remove(role);
    }
}
public class Role
{
    // Fields
    public virtual Guid Id { get; set; }
    public virtual string Name { get; set; }

    // Storage for the users that have this role
    private readonly IList<User> _users = new List<User>();

    // Returns list of users belonging to this role
    // Notice this can't be added or removed from.
    // Nor is there any logic to add users into roles
    // as the link is maintained from the User object.
    public virtual ReadOnlyCollection<User> Users
    {
        get
        {
            return new ReadOnlyCollection<User>(_users);
        }
    }
}

Notice that the relationship between users and roles is maintained and owned in the user object, not role. The role object just gives us information about which users belong to a role, but we can’t add or remove users into it without loading the user and accessing it’s Add/Remove Role methods. We can map this out as follows (notice, I’m using Fluent mappings for NHibernate here);

public class UserMap : ClassMap<User>
{
    public UserMap()
    {
        base.Id(x => x.Id).GeneratedBy.GuidComb();
        base.Map(x => x.UserName);
        base.Map(x => x.Email);

        HasManyToMany<Role>(Reveal.Property<User>("_roles"))
            .LazyLoad()
            .AsBag()
            .WithTableName("UserRoles")
            .WithParentKeyColumn("UserId")
            .WithChildKeyColumn("RoleId")
            .Access.AsReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore)
            .Cascade.All();
    }
}

public class RoleMap : ClassMap<Role>
{
    public RoleMap()
    {
        base.Id(x => x.Id).GeneratedBy.GuidComb();
        base.Map(x => x.Name);

        HasManyToMany<User>(Reveal.Property<Role>("_users"))
            .LazyLoad()
            .AsBag().Inverse()
            .WithTableName("UserRoles")
            .WithParentKeyColumn("RoleId")
            .WithChildKeyColumn("UserId")
            .Cascade.None();
    }
}

Notice that the role map has .Inverse() set, which indicates that it is not the owner of the relationship.

That is pretty much all there is to it, NHibernate will take care of maintaining the relationship table for you.

2 comments:

  1. Great post! I was struggling with this.

    ReplyDelete
  2. Hi Tony,

    I've used this mapping in a slightly different context and it gave me an UnknownPropertyException - maybe something to do with UserMap's 'HasManyToMany' mapping using both Reveal.Property and AsReadOnlyPropertyThroughCamelCaseField for the _roles collection? (i.e. expecting it to be a field AND and property)

    Nice blog though and the cascade settings helped me out :-)

    Cheers

    ReplyDelete