Question

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?

Was it helpful?

Solution

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.

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top