NHibernate many-to-many assocations making both ends as a parent by using a relationship entity in the Domain Model
Asked Answered
I

3

1

Entities: Team <-> TeamEmployee <-> Employee

Requirements:

  • A Team and an Employee can exist without its counterpart.
  • In the Team-TeamEmployee relation the Team is responsible (parent) [using later a TeamRepository].
  • In the Employee-TeamEmployee relation the Employee is responsible (parent) [using later an EmployeeRepository].
  • Duplicates are not allowed.
  • Deleting a Team deletes all Employees in the Team, if the Employee is not in another Team.
  • Deleting an Employee deletes only a Team, if the Team does not contain no more Employees.

Mapping:

public class TeamMap : ClassMap<Team>
{
    public TeamMap()
    {
        // identity mapping
        Id(p => p.Id)
            .Column("TeamID")
            .GeneratedBy.Identity();

        // column mapping
        Map(p => p.Name);

        // associations
        HasMany(p => p.TeamEmployees)
            .KeyColumn("TeamID")
            .Inverse()
            .Cascade.SaveUpdate()
            .AsSet()
            .LazyLoad();
    }
}

public class EmployeeMap : ClassMap<Employee>
{
    public EmployeeMap()
    {
        // identifier mapping
        Id(p => p.Id)
            .Column("EmployeeID")
            .GeneratedBy.Identity();

        // column mapping
        Map(p => p.EMail);
        Map(p => p.LastName);
        Map(p => p.FirstName);

        // associations
        HasMany(p => p.TeamEmployees)
            .Inverse()
            .Cascade.SaveUpdate()
            .KeyColumn("EmployeeID")
            .AsSet()
            .LazyLoad();

        HasMany(p => p.LoanedItems)
            .Cascade.SaveUpdate()
            .LazyLoad()
            .KeyColumn("EmployeeID");
    }
}

public class TeamEmployeeMap : ClassMap<TeamEmployee>
{
    public TeamEmployeeMap()
    {
        Id(p => p.Id);

        References(p => p.Employee)
            .Column("EmployeeID")
            .LazyLoad();

        References(p => p.Team)
            .Column("TeamID")
            .LazyLoad();
    }
}

Creating Employees and Teams:

    var employee1 = new Employee { EMail = "Mail", FirstName = "Firstname", LastName = "Lastname" };
    var team1 = new Team { Name = "Team1" };
    var team2 = new Team { Name = "Team2" };

    employee1.AddTeam(team1);
    employee1.AddTeam(team2);


    var employee2 = new Employee { EMail = "Mail2", FirstName = "Firstname2", LastName = "Lastname2" };
    var team3 = new Team { Name = "Team3" };

    employee2.AddTeam(team3);
    employee2.AddTeam(team1);

    team1.AddEmployee(employee1);
    team1.AddEmployee(employee2);
    team2.AddEmployee(employee1);
    team3.AddEmployee(employee2);

    session.SaveOrUpdate(team1);
    session.SaveOrUpdate(team2);
    session.SaveOrUpdate(team3);

    session.SaveOrUpdate(employee1);
    session.SaveOrUpdate(employee2);

After this I commit the changes by using transaction.Commit(). The first strange thing is that I have to save Teams and Employees instead only one of them (why?!). If I only save all teams or (Xor) all employees then I get a TransientObjectException:

"object references an unsaved transient instance - save the transient instance before flushing. Type: Core.Domain.Model.Employee, Entity: Core.Domain.Model.Employee"

When I save all created Teams and Employees everything saves fine, BUT the relation table TeamEmployee has duplicate assoications.

ID EID TID
1  1   1
2  2   1
3  1   2
4  2   3
5  1   1
6  1   2
7  2   3
8  2   1

So instead of 4 relations there are 8 relations. 4 relations for the left side and 4 relations for the right side. :[

What do I wrong?

Further questions: When I delete a Team or an Employee, do I have to remove the team or the Employee from the TeamEmployee list in the object model or does NHibernate make the job for me (using session.delete(..))?

Isabellisabella answered 18/12, 2009 at 14:40 Comment(0)
A
2

You are talking about business logic. It's not the purpose of NHibernate to implement the business logic.

What your code is doing:

You mapped two different collections of TeamEmployees, one in Team, one in Employee. In your code, you add items to both collections, creating new instances of TeamEmployee each time. So why do you expect that NHibernate should not store all these distinct instances?

What you could do to fix it:

You made TeamEmployee an entity (in contrast to a value type). To create an instance only once, you would have to instantiate it only once in memory and reuse it in both collections. Only do this when you really need this class in your domain model. (eg. because it contains additional information about the relations and is actually an entity of its own.)

If you don't need the class, it is much easier to map it as a many-to-many relation (as already proposed by Chris Conway). Because there are two collections in memory which are expected to contain the same data, you tell NHibernate to ignore one of them when storing, using Inverse.

The parent on both ends problem

There is no parent on both ends. I think it's clear that neither the Team nor the Employee is a parent of the other, they are independent. You probably mean that they are both parents of the intermediate TeamEmployee. They can't be parent (and therefore owner) of the same instance. Either one of them is the parent, or it is another independent instance, which makes managing it much more complicated (this is how you implemented it now). If you map it as a many-to-many relation, it will be managed by NHibernate.

To be done by your business logic:

  • storing new Teams and new Employees
  • managing the relations and keeping them in sync
  • deleting Teams and Employees when they are not used anymore. (There is explicitly no persistent garbage collection implementation in NHibernate, for several reasons.)
Aurelea answered 1/1, 2010 at 23:26 Comment(3)
why does the the Team side has no Cascading option?Isabellisabella
... and how do I delete a Team with the setup from Chris Conway?Isabellisabella
I wouldn't cascade anything here, but it is a matter of taste. IMHO, when there are two independent entities referencing each other, there should be business logic to create them and no cascading. Even if you have to care about it in code, it's clearer. --- When you delete a team, you need to iterate through the employees and delete them all. When you delete a employee, you could use a query which returns all the referenced teams that do not have any other employees and delete them too. You could use HQL to directly delete entities without loading them.Aurelea
Z
1

Looks like you need a HasManyToMany instead of two HasMany maps. Also, there is no need for the TeamEmployeeMap unless you have some other property in that table that needs mapped. Another thing, only one side needs to have the Inverse() set and since you're adding teams to employees I think you need to make the TeamMap the inverse. Having the inverse on one side only will get rid of the duplicate entries in the database.

Maybe something like this:

public class TeamMap : ClassMap<Team>
{
    public TeamMap()
    {
        // identity mapping
        Id(p => p.Id)
           .Column("TeamID")
           .GeneratedBy.Identity();

        // column mapping
        Map(p => p.Name);

        // associations
        HasManyToMany(x => x.TeamEmployees)
            .Table("TeamEmployees")
            .ParentKeyColumn("TeamID")
            .ChildKeyColumn("EmployeeID")
            .LazyLoad()
            .Inverse()
            .AsSet();
    }
}

public class EmployeeMap : ClassMap<Employee>
{
    public EmployeeMap()
    {
        // identifier mapping
        Id(p => p.Id)
            .Column("EmployeeID")
            .GeneratedBy.Identity();

        // column mapping
        Map(p => p.EMail);
        Map(p => p.LastName);
        Map(p => p.FirstName);

        // associations
        HasManyToMany(x => x.TeamEmployees)
            .Table("TeamEmployees")
            .ParentKeyColumn("EmployeeID")
            .ChildKeyColumn("TeamID")
            .Cascade.SaveUpdate()
            .LazyLoad()
            .AsSet();

        HasMany(p => p.LoanedItems)
            .Cascade.SaveUpdate()
            .LazyLoad()
            .KeyColumn("EmployeeID");
    }
}

Using this, the delete will delete the TeamEmployee from the database for you.

Zipper answered 18/12, 2009 at 14:58 Comment(1)
A many-to-many assocation mapping with NH does not allow both ends of the assocation to be a parent, that is why I use the 3rd relation object.Isabellisabella
D
0

NHibernate does not allow many-to-many association with parents at both ends.

Dozy answered 30/12, 2009 at 9:52 Comment(1)
Are you sure that NH generally does not allow many-to-many assocations wit parents at both ends? I THINK it does not work with the NH many-to-many keyword, but with one-to-many assocations, doesn't it?Isabellisabella

© 2022 - 2024 — McMap. All rights reserved.