Reputation: 14262
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
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
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