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

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(..))?

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.)

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.

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

Related

Fluent NHibernate - mapping one-to-many relationship to different class/entity hierarchy

I have standard one-to-many table setup, but I want to be able to map it to a class structure that looks like this:
Class 1 -> Mapped to Table 1 (The one- side of the one-to-many relationship)
|
--- Intermediary class that has additional methods on and has a List of Class 2
|
---- Class 2 -> Mapped to Table 2 (The -many side of the relationship)
Is this possible with NHibernate?
it is possible for example
class Class1Map : ClassMap<Class1>
{
public Class1Map()
{
Id(x => x.Id);
Component(c =>
{
c.Map(x => x.Foo);
c.HasMany(x => x.Classes2);
});
}
}

Optional many-to-one/References results in foreign key violations on insert

I currently have the following relationship: ProductUom -> ProductImage
They both have the same primary keys: PROD_ID and UOM_TYPE
I have them mapped like this:
public ProductUomMap()
{
Table("PROD_UOM");
CompositeId()
.KeyReference(x => x.Product, "PROD_ID")
.KeyProperty(x => x.UomType, "UOM_TYPE");
References(x => x.Image)
.Columns(new string[] { "PROD_ID", "UOM_TYPE" })
.Not.Update()
.Not.Insert()
.NotFound.Ignore()
.Cascade.All();
}
public ProductImageMap()
{
Table("PROD_UOM_IMAGE");
CompositeId()
.KeyReference(x => x.ProductUom, new string[] {"PROD_ID", "UOM_TYPE"});
Map(x => x.Image, "PROD_IMAGE").Length(2147483647);
}
Whenever I create a ProductUom object that has a ProductImage it tries to insert the ProductImage first which results in a foreign key violation. I swear this was working at one time with the mapping that I have but it doesn't now.
I need the ProductImage to be a Reference (many-to-one) because the relationship here is optional and I want to be able to lazy load product images. The inserts do work correctly if I use a HasOne (one-to-one) mapping but the I cannot lazy load when I do this and querying a ProductUom seems to cause issues.
Is there something that I'm missing here? How can this mapping be modified to get what I want?
can you use LazyLoaded Properties? Then you could use something like this
Join("PROD_UOM_IMAGE", join =>
{
join.KeyColumn("PROD_ID", "UOM_TYPE");
join.Optional();
join.Map(x => x.Image, "PROD_IMAGE").Length(2147483647).LazyLoad();
}
another option is:
Id().GeneratedBy.Foreign(x => x.ProductUom);
can't test it here though, i'm writing on Mobile

How to delete a referenced object using FluentNHibernate (ye olde "deleted object would be resaved by cascade")

The error I'm getting is common, but the scenario I haven't found any answers that speak to my scenario:
Entities:
School
Teacher
Student
Mappings:
School: mapping.HasMany(x => x.Students).Cascade.AllDeleteOrphan();
Student:
mapping.References(x => x.Teacher).Not.Nullable().Cascade.SaveUpdate();
mapping.References(x => x.School).Not.Nullable().Cascade.SaveUpdate();
Teacher:
mapping.References(x => x.School).Not.Nullable().Cascade.SaveUpdate();
mapping.HasMany(x => x.Students).Cascade.All().Inverse();
Scenario: Student is linked to a School that has no other Students or Teachers. If I want to link the student to a different school, I'd like to delete the orphaned school.
if (oldSchool.Students.Count == 1 && oldSchool.Teachers.Count == 0)
{
//delete it
//oldSchool.Students.Remove(student);
student.School = null;
_schoolRepository.Delete(oldSchool);
}
What happens here is that, when I go to save "student", I get the dreaded "deleted object would be resaved by cascade" error.
As always, any help greatly appreciated.
What's the rest of your mappings look like? What's the inverse setting between the School and the Student?
Try mapping.HasMany(x => x.Students).Cascade.AllDeleteOrphan().Inverse().

NHibernate using wrong table alias

I am trying to filter a collection based on a foreign key. I have two classes which are mapped with
public class GroupPriceOverrideMap:ClassMap<GroupPriceOverride>
{
public GroupPriceOverrideMap()
{
CompositeId()
.KeyReference(x => x.Service,"ServiceCode")
.KeyReference(x => x.CustomerAssetGroup, "GroupID");
Map(x => x.Price);
Table("accGroupPriceOverride");
}
}
public class CustomerAssetGroupMap:ClassMap<CustomerAssetGroup>
{
public CustomerAssetGroupMap()
{
Id(x => x.GroupID).Unique();
Map(x => x.Description);
References(x => x.Customer).Column("CustomerID");
HasMany<GroupPriceOverride>(x => x.PriceOverrides).KeyColumn("GroupID");
Table("accCustAssetGroup");
}
}
I query it using
_session.Linq<GroupPriceOverride>.Where(x => x.CustomerAssetGroup.GroupID == groupID)
However this is generating
SELECT this_.ServiceCode as ServiceC1_9_0_, this_.GroupID as GroupID9_0_, this_.Price as Price9_0_ FROM accGroupPriceOverride this_ WHERE customeras1_.GroupID = #p0
there where clause is referencing a table alias which doesn't exist(customeras1). This is probably an alias for crossing with customerassetgroup but there is no need to perform that cross. I'm sure that it is just something in my mapping with is wrong but I can't find it. I've tried various column renaming in case the presence of GroupID in both tables was causing problems but that didn't fix it. Any ideas?
Edit
I found that if I queried doing
_session.Linq<CustomerAssetGroup>().Where(x => x.GroupID == groupID).FirstOrDefault().PriceOverrides;
then I got the correct result. I also found that if I saved a GroupPriceOverride and then queried for it using HQL then it wouldn't be found but I could still find the entity by loading the parent and looking at its collection of overrides.
_session.CreateQuery("FROM GroupPriceOverride i").List().Count;//returns 0
_session.CreateQuery("FROM CustomerAssetGroupi").List().FirstOrDefault().PriceOverrides.Count;//returns 1
Looks like a bug in the old LINQ provider. Could you file a bug here:
https://nhibernate.jira.com/secure/Dashboard.jspa
You might be able to get around it via:
_session.Linq<GroupPriceOverride>.Where(x => x.CustomerAssetGroup == group)
and let NHibernate figure out the ID. If you don't have the group already, you could do this:
var group = _session.Load<CustomerAssetGroup>(groupID);
_session.Linq<GroupPriceOverride>.Where(x => x.CustomerAssetGroup == group)
The ISession.Load(id) will only generate a proxy, but won't actually hit the database until you access a property (which you wouldn't be since you're just using it to specify the ID).

HasMany relation inside a Join Mapping

So, I'm having a problem mapping in fluent nhibernate. I want to use a join mapping to flatten an intermediate table: Here's my structure:
[Vehicle]
VehicleId
...
[DTVehicleValueRange]
VehicleId
DTVehicleValueRangeId
AverageValue
...
[DTValueRange]
DTVehicleValueRangeId
RangeMin
RangeMax
RangeValue
Note that DTValueRange does not have a VehicleID. I want to flatten DTVehicleValueRange into my Vehicle class. Tgis works fine for AverageValue, since it's just a plain value, but I can't seem to get a ValueRange collection to map correctly.
public VehicleMap()
{
Id(x => x.Id, "VehicleId");
Join("DTVehicleValueRange", x =>
{
x.Optional();
x.KeyColumn("VehicleId");
x.Map(y => y.AverageValue).ReadOnly();
x.HasMany(y => y.ValueRanges).KeyColumn("DTVehicleValueRangeId"); // This Guy
});
}
The HasMany mapping doesn't seem to do anything if it's inside the Join. If it's outside the Join and I specify the table, it maps, but nhibernate tries to use the VehicleID, not the DTVehicleValueRangeId.
What am I doing wrong?
Can you explain the average value column in the DTVehicleValueRange table? Isn't this a calculated value (i.e. no need to persist it)?
It looks like you have a many-to-many relationship between Vehicle and DTValueRange, which of course would not be mapped with a join, rather with a HasManyToMany call.
Ran into a similar issue today using a Map to create a view. The SQL generated showed it trying to do the HasMany<> inside the join based on the Id of the ParentThing and not WorkThing (same problem you were having)
After much mapping of head-to-desk it turns out adding the propertyref onto the hasmany solved it.
public class ThingMap : ClassMap<WorkThingView> {
public ThingMap() {
ReadOnly();
Table("ParentThing");
Id(x => x.ParentThingId);
Map(x => x.ParentName);
Join("WorkThing", join => {
join.KeyColumn("ParentThingId");
join.Map(m => m.FooCode);
join.Map(m => m.BarCode);
join.Map(x => x.WorkThingId);
join.HasMany(x => x.WorkThingCodes)
.Table("WorkThingCode").KeyColumn("WorkThingId").PropertyRef("WorkThingId")
.Element("WorkThingCode");
});
}
}