4
votes

I'm trying to map a many-to-many relationship between two entities, but I need to decorate that entity with a number of properties - see the diagram below:

Many-to-Many Relationship with additional attributes on relationship table

Reads is my relationship table in this case - I added an identity column on it in order to avoid using a composite key, but the valuable information here is really the UserId, the FeedItemId, and the TimeRead attribute. Here is how I have tried to map this relationship, based on similar examples I've seen on StackOverFlow:

User

public class UserMap : ClassMap<User>
{
    public UserMap()
    {
        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();
        HasManyToMany(x => x.UserRoles).Cascade.AllDeleteOrphan().AsBag().Table("UsersInRole");
        HasManyToMany(x => x.SubscribedFeeds).Cascade.DeleteOrphan().AsBag().Table("Subscriptions");
        HasManyToMany(x => x.OwnedFeeds).Cascade.All().AsBag().Table("FeedOwners");
        HasMany(x => x.Reads).Cascade.DeleteOrphan().Fetch.Join().Inverse().KeyColumn("UserId");
    }
}

FeedItem

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);
        References(x => x.Feed);
        HasManyToMany(x => x.Tags).Cascade.All().Table("PostTags");
        HasManyToMany(x => x.Categories).Cascade.All().Table("PostsInCategory");
        HasMany(x => x.Reads).Cascade.AllDeleteOrphan().Inverse().Fetch.Join().KeyColumn("FeedItemId");
    }
}

Reads:

public class FeedReadMap : ClassMap<FeedRead>
{
    public FeedReadMap()
    {
        Table("Reads");
        //CompositeId()
        //    .KeyProperty(x => x.TimeRead, "TimeRead")
        //    .KeyReference(x => x.ItemRead, "FeedItemId")
        //    .KeyReference(x => x.Reader, "UserId");
        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");
    }
}

This code doesn't raise errors as-is, but nothing is ever persisted to the Reads table when I try to do the following:

var read = new FeedRead {ItemRead = feed.Items[0], Reader = user, TimeRead = DateTime.Now};
        user.Reads.Add(read);
        feed.Items[0].Reads.Add(read);

        _repository.SaveUser(user);

This is probably because of the fact that both User and FeedItem have a .Inverse on the relationship mapping - I did it this way because it's what I saw in most other examples that tried to model this same relationship.

When I remove the .Inverse off of the User mapping, I get this error instead:

NHibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing.

My ultimate goal is to be able to do Session.SaveOrUpdate(user) and have it insert any new feed reads directly into the Reads table, but I haven't been able to find a way to do it yet. What am I doing wrong?

I've read through virtually every other question on StackOverFlow about this topic already and haven't found a clear answer to this question.

1

1 Answers

5
votes

It all depends on how you want to save new Reads.

If you want to save new Reads via User, then you need Cacascade.AllDeleteOrphan() for User. As it stands, it will not cascade the new reads as you only have DeleteOrphan.

public class UserMap : ClassMap<User>
{
    public UserMap()
    {
        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();
        HasManyToMany(x => x.UserRoles).Cascade.AllDeleteOrphan().AsBag().Table("UsersInRole");
        HasManyToMany(x => x.SubscribedFeeds).Cascade.DeleteOrphan().AsBag().Table("Subscriptions");
        HasManyToMany(x => x.OwnedFeeds).Cascade.All().AsBag().Table("FeedOwners");
        HasMany(x => x.Reads).Cascade.AllDeleteOrphan().Fetch.Join().Inverse().KeyColumn("UserId");
    }
}

Just a thought, I always like to minimize bi-directional relationships in an attempt to keep the domain simpler. Hence I would probably not have the collections on User or FeedItem if it could be avoided.