Search code examples
c#nhibernatefluent-nhibernatenhibernate-mappingdiscriminator

How can I use Fluent NHibernate to discriminate on a column of a parent relationship


I have this entity relationship.

public abstract class UserGroup
{
    public virtual int UserGroupId { get; set; }
    public virtual int GroupType { get; set; }
    public virtual string GroupName { get; set; }
    public virtual IList<UserGroupMember> GroupMembers { get; set; }
}

public class UserGroupMember
{
   public virtual int UserGroupMemberId { get; set; }
   public virtual User User { get; set; }
   public virtual UserGroup UserGroup { get; set; }
}

Note that UserGroup is base class and that I have a few derived classes that are discriminated by UserGroup.GroupType. I am using the Single Table Per Class Hierarchy inheritance mapping strategy.

Fluent NHibernate mappings:

public class UserGroupMap : BaseClassMap<UserGroup>
{
    public UserGroupMap()
    {
        Table("UserGroup");
        Id(x => x.UserGroupId);

        DiscriminateSubClassesOnColumn("GroupType");

        Map(x => x.GroupName);

        HasMany(x => x.GroupMembers)
            .KeyColumn("UserGroupId")
            .Cascade.AllDeleteOrphan()
            .Inverse();
    }
}

public class UserGroupMemberMap: BaseClassMap<UserGroupMember>
{
    public UserGroupMemberMap()
    {
        Table("UserGroupMember");
        Id(x => x.UserGroupMemberId);

        References(x => x.User, "UserId")
            .Cascade.SaveUpdate();

        References(x => x.UserGroup, "UserGroupId")
            .Cascade.SaveUpdate();
    }
}

This is what I want to achieve:
I would like to make UserGroupMember a base class also and have it discriminate on the UserGroup.GroupType discriminator. Is this possible?

Another way of wording this question, it would be great if I could add the following line:

DiscriminateSubClassesOnColumn("UserGroup.GroupType");

Into the UserGroupMemberMap


Solution

  • I. We cannot use DiscriminateSubClassesOnColumn("UserGroup.GroupType");

    Among many other good reasons, discriminator is managed by NHiberante. It means, that such a value must be easily INSERTable during any child object creation. And won't work with external tables/references included...

    II. But in these scenarios, we can go another way. Let's oberve the documentation (in xml mapping, but fluent is just a wrapper on top of it)

    5.1.6. discriminator

    <discriminator
            column="discriminator_column"      (1)
            type="discriminator_type"          (2)
            force="true|false"                 (3)
            insert="true|false"                (4)
            formula="arbitrary SQL expression" (5)
    />
    

    (1) column (optional - defaults to class) the name of the discriminator column.
    (2) type (optional - defaults to String) a name that indicates the NHibernate type
    (3) force (optional - defaults to false) "force" NHibernate to specify allowed discriminator values even when retrieving all instances of the root class.
    (4) insert (optional - defaults to true) set this to false if your discriminator column is also part of a mapped composite identifier.
    (5) formula (optional) an arbitrary SQL expression that is executed when a type has to be evaluated. Allows content-based discrimination.

    What is really very interesting and helpful for us is (4) and (5). We can simply set discriminator to be readonly (no insert) and to use formula.

    <discriminator insert="false"
     formula="(SELECT ug.GroupType FROM UserGroup ug WHERE ug.UserGroupId = UserGroupId)"/>
    

    NOTE: The last UserGroupId will be by NHibernate treated as a column in current table. Which is essential and awesome...

    Good. Now our <discriminator> does return correct value. It also does not insert a value during Creation of a new instance. We only have to be sure, that there will be assigned proper UserGroup reference, with required Type...

    Fluent should be obvious, but to be sure:

    DiscriminateSubClassesOnColumn(null)
        .Formula("(SELECT...")
        .ReadOnly()
        ;