Iain Holder
Iain Holder

Reputation: 14262

NHibernate mappings when self-join relationships have additional properties

How do you map a class to other instances of the same class when that relationship has properties itself?

I have a class called Person which is mapped to a table Person

PersonID   PersonName    PersonAge 
----------------------------------
       1   Dave Dee             55
       2   Dozy                 52
       3   Beaky                45
       4   Mick                 55
       5   Tich                 58

I want a many-to-many relationship between Person and Person using a join table called PersonPerson:

 PersonPersonID  PersonID  RelatedPersonID RelationshipID 
 --------------------------------------------------------
              1         1                5              1
              2         3                4              2
              3         2                1              3

I want the following attributes in the PersonPerson table:

RelationshipID  RelationshipName
--------------------------------
             1  Colleague
             2  Manager
             3  Tutor

This question and the linked-to post by Billy McCafferty explains that the PersonPerson relationship has to be promoted from a normal JOIN to an entity in its own right because of the additional columns in the PersonPerson table. However it doesn't explain what to when it is a self-join. The difference being that if I ask for all the related people to Dave Dee (ID = 1), not only should I get Tich (ID = 5), but I should get also get Dozy (ID = 2) as well because Dave Dee is also in the RelatedPersonID column.

What my solution is so far, is to have two properties in my Person class.

public virtual IList<PersonPerson> PersonPersonForward {get;set;}
public virtual IList<PersonPerson> PersonPersonBack {get;set;}

private List<PersonPerson> personPersonAll;
public virtual List<PersonPerson> PersonPersonAll 
{
   get
   {
       personPersonAll = new List<PersonPerson>(PersonPersonForward);
       personPersonAll.AddRange(PersonPersonBack);
       return personPersonAll;
   }
}

And have the following in the hbm:

 <bag name="PersonPersonForward" table="PersonPerson" cascade="all">
      <key column="PersonID"/>
      <one-to-many class="PersonPerson" />
 </bag>

 <bag name="PersonPersonBack" table="PersonPerson" cascade="all">
      <key column="RelatedPersonID"/>
      <one-to-many class="PersonPerson" />
 </bag>

This seems a trifle clunky and inelegant. NHibernate usually has elegant solutions to most everyday problems. Is the above the sensible way of doing this or is there a better way?

Upvotes: 5

Views: 2036

Answers (2)

Frederik Gheysels
Frederik Gheysels

Reputation: 56954

I think I would do it like that as well, but, I think it is a bit 'clumsy' to model it like this. I mean: you have a collection of persons to which a certain person is related, but you also have a 'back-relation'.
Is this really necessary ? Isn't it an option to remove this back-collection and instead, specify a method on the PersonRepository which can give you all persons back that have some kind of relation with a given person ?

Hmm, this can maybe sound a bit obscure, so here 's some code (note that for the sake of brevity, I left out the 'virtual' modifiers etc... (I also prefer not to have those modifiers, so in 99% of the time, I specify 'lazy=false' at my class-mapping).

public class Person
{
    public int Id {get; set;}
    public string Name {get; set;}

    public IList<PersonPerson> _relatedPersons;

    public ReadOnlyCollection<PersonPerson> RelatedPersons
    {
        get
        {
           // The RelatedPersons property is mapped with NHibernate, but
           // using its backed field _relatedPersons (can be done using the 
           // access attrib in the HBM.
           // I prefer to expose the collection itself as a readonlycollection
           // to the client, so that RelatedPersons have to be added through
           // the AddRelatedPerson method (and removed via a RemoveRelatedPerson method).

           return new List<PersonPerson) (_relatedPersons).AsReadOnly();
        }
    }

    public void AddRelatedPerson( Person p, RelationType relatesAs )
    {
       ...
    }

}

As you can see, the Person class only has one collection left, that is a collection of PersonPerson objects that represents relations that this Person has. In order to get the Persons that have relations with a given Person, you could create a specific method on your PersonRepository that returns those Persons, instead of having them in a collection on the Person class. I think this will improve performance as well.

public class NHPersonRepository : IPersonRepository
{
    ...

    public IList<Person> FindPersonsThatHaveARelationShipWithPerson( Person p )
    {
        ICriteria crit = _session.CreateCriteria <Person>();

        crit.AddAlias ("RelatedPersons", "r");

        crit.Add (Expression.Eq ("r.RelatedWithPerson", p));

        return crit.List();

    }
}

The 'back-reference' is not a member of the Person class; it has to be accessed via the repository. This is also what Eric Evans says in his DDD - book: in some cases , it is better to have a specialized method on the repository that can give you access to related objects, instead of having them (= the related objects) to carry around with the object itself.

I didn't test the code, I just typed it in here, so I also didn't check for syntax error, etc... but I think it should clarify a bit on how I would see this.

Upvotes: 2

Jeffrey Hantin
Jeffrey Hantin

Reputation: 36524

It looks to me like you've essentially built a model of a directed graph, and the two mappings PersonPersonForward and PersonPersonBack represent outgoing and incoming edges respectively.

This directedness is reinforced by the semantics of your Relationship types: while is-a-Colleague-of is most likely a symmetric relation, is-a-Manager-of and is-a-Tutor-of are almost definitely asymmetric.

I think in this case the data model is trying to tell you that the two collections of links, while of compatible type, are not the same thing in context.

Upvotes: 2

Related Questions