Aaronontheweb

Hacking .NET and Startups

Fluent NHibernate: How to Create Bidirectional Many-to-Many Relationships with Additional Attributes on Relationship Table

December 24, 2010 10:50 by Aaronontheweb in NHibernate // Tags: , // Comments (1)

Suppose you have a relationship that looks like this in your database:

fluent-nhibernate-complex-many-to-many-mapping

This is a common use-case. In this instance, I wanted to be able to create a table which would log all of the times an individual user read a particular item from an RSS feed. I used an identity key here instead of a composite key so I could take advantage of NHibernate’s auto-magical SaveOrUpdate feature.

Normally with a ManyToMany relationship between two entities in Fluent NHibernate you would simply define a HasManyToMany attribute on either side of the entity, and .Inverse one of them such that only one entity is actually responsible for persisting changes to the database.

In this type of relationship, where your many-to-many relationship actually has additional attributes sitting in the middle of it, you have to define a third type of entity to sit in the middle, and this third entity becomes your relationship table. Here’s what this looks like in code:

The Entities

User – the entity responsible for saving; simplified in order to make it easier to read the code.

public class User
{
    virtual public int UserId { get; set; }
    virtual public string UserName { get; set; }
    virtual public string EmailAddress { get; set; }
    virtual public string Password { get; set; }
    virtual public DateTime DateJoined { get; set; }
	
    virtual public IList<FeedRead> Reads { get; set; }

    public User()
    {
        Reads = new List<FeedRead>();
    }
}

FeedItem – the entity on the other side of the relationship; also simplified.

public class FeedItem
{
    public virtual int FeedItemId { get; set; }
    public virtual Feed Feed { get; set; }
    public virtual string Title { get; set; }
    public virtual DateTime DatePublished { get; set; }
    public virtual Uri OriginalUri { get; set; }

    public virtual IList<FeedRead> Reads { get; set; }

    public FeedItem()
    {
        Reads = new List<FeedRead>();
    }
}

FeedRead – the relationship entity which contains the bidirectional relationships.

public class FeedRead
{
    public virtual int ReadId { get; set; }
    public virtual DateTime TimeRead { get; set; }
    public virtual FeedItem ItemRead { get; set; }
    public virtual User Reader { get; set; }
}

The entities themselves are pretty simple – it’s the mappings that are more interesting.

The Mappings

UserMap – the mapping for our User entity class; the key item here is the HasMany relationship and its attributes. The .Fetch and .Join properties are optional (just a way of telling the NHibernate engine how I want to perform queries against the table on the other side of this relationship,) but everything else is key.

public class UserMap : ClassMap<User>
    {
        public UserMap()
        {
            Table("EasyFeedUser");
            Id(x => x.UserId).GeneratedBy.Identity();
            Map(x => x.UserName)
				.Length(DataConstants.UserNameLength)
				.Unique().Not.Nullable();
            Map(x => x.EmailAddress)
				.Length(DataConstants.EmailAddressLength)
				.Unique().Not.Nullable();
            Map(x => x.DateJoined)
				.Not.Nullable();
            Map(x => x.Password)
				.Length(DataConstants.PasswordHashLength)
				.Not.Nullable();
            HasMany(x => x.Reads)
				.Cascade.AllDeleteOrphan()
				.Fetch.Join()
				.Inverse().KeyColumn("UserId");
        }
    }

FeedItemMap – the mapping for the FeedItem entity class.

public class FeedItemMap : ClassMap<FeedItem>
{
    public FeedItemMap()
    {
        Id(x => x.FeedItemId).GeneratedBy.Identity();
        Map(x => x.OriginalUri)
			.Not.Nullable()
			.Unique()
			.Length(DataConstants.FeedUriLength);
        Map(x => x.DatePublished)
			.Not.Nullable();
        Map(x => x.Title)
			.Not.Nullable()
			.Length(DataConstants.FeedItemTitleLength);
        HasMany(x => x.Reads)
			.Cascade.AllDeleteOrphan()
			.Inverse()
			.Fetch.Join().KeyColumn("FeedItemId");
    }
}

FeedReadMap – the mapping for the FeedRead entity.

public class FeedReadMap : ClassMap<FeedRead>
    {
        public FeedReadMap()
        {
            Table("Reads");
            Id(x => x.ReadId).GeneratedBy.Identity();
            Map(x => x.TimeRead).Not.Nullable();
            References(x => x.Reader)
				.Not.Nullable()
				.Cascade.SaveUpdate()
				.Column("UserId");
            References(x => x.ItemRead)
				.Not.Nullable()
				.Cascade.SaveUpdate()
				.Column("FeedItemId");
        }
    }

So now that I’ve shown you all of the code, what does this mean?

In order to have a bidirectional many-to-many relationship with additional attributes, you need to do the following things:

  1. Create a third entity type which contains all of the columns for the relationship table;
  2. Define the relationships between the two primary entities and the relationship entity (FeedRead) as HasMany.Inverse().Cascade.AllDeleteOrphan and name an explicit foreign key.
  3. Map the relationships of the relationship entity (FeedRead) to the primary entities as References.Cascade.SaveUpdate() and name an explicit column name, which is the same name as the foreign key you defined on the other mappings.

And that’s it – now you can automatically add new FeedRead entities by calling Session.SaveOrUpdate(user) or Session.SaveOrUpdate(feeditem).

If you enjoyed this post, make sure you subscribe to my RSS feed!



How to Make it Easy for New Developers to Adopt Your Open Source Project

James Gregory is one of my heroes in the .NET community – he’s the creator of Fluent NHibernate, my favorite new ORM (Object-Relational Mapper) for my ASP.NET MVC projects. James expressed some dismay earlier today when a newbie Fluent NHibernate developer posted a $100+ bounty for a few basic usage examples; having just recently taught myself the very things the developer was asking for, I can empathize.

NHibernate (Fluent NHibernate runs on top of it) is a bear of an open source project for new developers – it’s a mature project that solves a problem with a massive surface area and the NHibernate community is comprised of experienced developers who’ve been using it for years. It can be difficult for new developers to get involved, as the learning curve is steep even for just adopting the project, let alone contributing to it.

So how can we make it easier for new developers to adopt advanced, mature projects like NHibernate?

Here are some ideas:

  1. Offer new developers a low-friction adoption path – in many of the projects I’ve tried to adopt over the years, I’ve had a difficult time figuring out what the best way to start using some of them itself. The thing that is going to kill adoption of your project is friction – if a new developer encounters resistance just trying to build a basic “Hello World” equivalent on top of your project, they’re going to lose interest and try to find something else. Gently guide your developers down on low-friction path to adoption.

  2. Describing the functionality of features isn’t enough; explain their intent – NHibernate has a ton of content for in its Wiki for new developers, but few of them explain the intent behind NHibernate’s powerful features – what new developers really need to understand is why and when they should use certain facets of your project’s functionality, not so much how that functionality works.

  3. Use real-world examples in the documentation – one issue that frustrates me when I’m trying to learn a new technology is when all of the 101-level examples are canned. I don’t want a Fibonacci sequence example or anything else that I would never use in the real-world. Show me the hard stuff; show me how to build a human user-interaction around your project’s technology; show me how to use it in a middle use-case; and show me how to integrate your technology into projects that resemble production scenarios.

  4. Include reference materials – .NET developers get to use object explorer as an option of last resort for reference materials, which is a plus. However, the goal of open source project leads should be to make it easy for new adopters to find the right piece of functionality to suit their needs, and a reference guide is often the best way to do it.

  5. StackOverFlow first – I was trying to troubleshoot a small Fluent NHibernate issue I ran into earlier today (will blog about it soon), and I saw a number of questions identical to my own on StackOverFlow – all of them unanswered. The first place your users are going to turn when they run into trouble is Bing and Google, and StackOverFlow will dominate the results; answering someone’s question on StackOverFlow doesn’t solve a problem for just the person who asked it – it answers the question for many other people who may run into the same issues down the road.

Open source is fun, and it's fun for new developers too. Onboarding new developers to your project isn't as sexy as writing a patch or coming out with a new version, but it's ultimately better for the longevity of the community.

If there's anything else I should add to this list, please add them in the comments!

If you enjoyed this post, make sure you subscribe to my RSS feed!



Search

About

My name is Aaron, I'm an entrepreneur and a .NET developer who develops web, cloud, and mobile applications.

I left Microsoft recently to start my own company, MarkedUp - we provide analytics for desktop developers, focusing initially on Windows 8 developers.

You can find me on Twitter or on Github!

Recent Comments

Comment RSS

Sign in