views:

968

answers:

2

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?

+2  A: 

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.

Frederik Gheysels
@Frederik Gheysels Great answer, I'll try that now. It seems an obvious solution now that you've said it!
IainMH
@Frederik - I like the idea of doing this in the repository, but it's still unclear to me how I would get back all the related instances and the relationship types as well.
IainMH
I would return the Person objects that have a relationship with the given person. Offcourse, those Person objects have their 'PersonPerson' collection that contains all the relationships that this Person has.
Frederik Gheysels
It depends a bit on what you're wanting to do, I think... Maybe it could be sufficient to just return a list of PersonPerson instances that represents relationships that other Persons have with the given Person.
Frederik Gheysels
@Frederik What would go in the getter for RelatedPersons? I would want to avoid having to call a repository directly from the POCO. Is there a way of calling it from the HBM?
IainMH
With relatedPersons, do you mean the 'back-reference' (the collection that is removed from my example) ?
Frederik Gheysels
I've updated my post a little bit.
Frederik Gheysels
@Frederik Sorry if this question is a little n00b, but how do you map the FindPersonsThatHaveARelationShipWithPerson method in the HBM?
IainMH
This is not mapped in the HBM; it is a member method of the 'PersonRepository' object (http://martinfowler.com/eaaCatalog/repository.html), and inside that method, you use HQL or the Criteria API to retrieve the persons that have a relationship with a given person.
Frederik Gheysels
Does that mean I would always have to call the method on the repository after I have already been to the repository to get the root object? My first solution has it as a property on the POCO which although looks clunky, is a nice separation.
IainMH
You call the method whenever you need those objects yes; will you need those back-relationships everytime you work with a Person object ?
Frederik Gheysels
Yes, I want them lazily loaded. I want to do display things like Person.RelatedPersons.Count on a strongly typed View<Person> page. I can do that with the way I have it now. It's just really ugly. I'd like to combine both our solutions into one somehow.
IainMH
Then, you'll have to inject the repository into your instance maybe ?Anyway, if you really want lazy loading (that is: retrieve the collection the first time it is needed, and 'cache' it afterwards), I think you might run into some problems:
Frederik Gheysels
You'll have to update the backreference of person X if you specify that Person Y has a relationship with person x.
Frederik Gheysels
Ok, I'll probably stick with what I have now. It's ugly, but it maintains the separation that I want. However you deserve the points for highlighting the alternative way of putting it in the repository plus the work you put in to answering it. Many thanks Frederik.
IainMH
+1  A: 

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.

Jeffrey Hantin
These are slightly contrived tables, but I take your point. I would like the symmetric relationships to be treated the same as the asymmetric ones. Wouldn't that just mean that the symmetric ones are the same each way?
IainMH
Yes, an undirected edge in an otherwise directed graph would be represented by a pair of directed edges, one pointing each way.
Jeffrey Hantin
If you're really after a hybrid, semi-directed graph, try separating the symmetric relationships into their own table with a check constraint (left-id <= right-id) for normalization, and perhaps a view with appropriate mutators defined to express the symmetry relationally.
Jeffrey Hantin
Then lump the two kinds of relationships together with polymorphism.
Jeffrey Hantin