(see bottom for updates)
So, I have two objects (we'll call them ClassA and ClassB) that exhibit a many-to-many relationship (ClassA can have multiple ClassB objects and vice versa).
Rather than a traditional many-to-many, though, the actual "relationship" is, itself, an object, with different information about the link between the two other classes (relationship start date, relationship name, etc.)
So, rather than having this defined as a many-to-many between ClassA and ClassB, I have two one-to-many relationships defined (one on ClassA and on ClassB) pointed at a third, intermediary class (we'll call it "Relationship"). Structurally, I expect the tables to look like this:
ClassA Relationship
---------------- ----------------
Id (PK) Id (PK)
Name Name
Description StartDate
ClassA_Id (FK)
ClassB ClassB_Id (FK)
----------------
Id (PK)
Name
Description
To achieve this, I have set up the mapping like so (after a ton of trial and error):
public ClassAMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasMany(x => x.Relationship)
.KeyColumn("ClassA_Id")
.ForeignKeyConstraintName("FK_Relationship_ClassA")
.LazyLoad()
.Cascade.All()
.Inverse();
}
public ClassBMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasMany(x => x.Relationship)
.KeyColumn("ClassB_Id")
.ForeignKeyConstraintName("FK_Relationship_ClassB")
.LazyLoad()
.Cascade.All()
.Inverse();
}
public RelationshipMap()
{
Id(x => x.Id);
Map(x => x.CreatedDate);
References(x => x.ClassAObject)
.Column("ClassA_Id")
.Cascade.SaveUpdate();
References(x => x.ClassBObject)
.Column("ClassB_Id")
.Cascade.SaveUpdate();
}
Now, for adding/deleting parent objects, his works exactly as expected. For example, if I create a new ClassA and ClassB, link them trough a Relationship and then Save ClassA, then all of the associated records are added to the three tables. Similarly, if I remove that same ClassA, then the ClassA record and the Relationship records are removed, but the ClassB record remains (expected behavior).
If, however, I just try to remove the Relationship from one of the parent objects (e.g. classA.Relationships.Remove(relationshipObject); classARepository.Update(classA);
), then I get errors similar to the following:
NHibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [ClassB]
NHibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations) [Relationship#9]
Additionally, if I completely reload the ClassA object from the database and remove a membership as described above, then it executes without error, but my Relationship is not removed from the database.
How can I alter the mapping to achieve the behavior that I an looking for?
For reference, here's an outline of the scenarios that must pass:
- Add new ClassA with Relationship
- Records for ClassA/B/Relationship added or updated
- Remove existing ClassA
- Records for ClassA and Relationships deleted
- ClassB records remain in table
- Remove Relationship from ClassA.Relationships list; update ClassA
- Remove Relationship from Relationships table
- ClassA and ClassB records remain
UPDATE: Based on @JamieIde's answer, I came up with the following adjustments:
- Null out the references to ClassA and ClassB on the Relationship:
// NOTE, this is an example snippet to show the steps that need to be taken
using(var session = SomeSessionFactory().OpenSession())
{
using (var transaction = session.BeginTransaction())
{
// preferably place these four lines in a public method on one of the objects
classAObject.Relationships.Remove(relationship);
classBObject.Relationships.Remove(relationship);
relationship.ClassA = null;
relationship.ClassB = null;
transaction.Commit();
}
}
- Change
.Cascade.All()
on the ClassA/ClassB toCascade.AllDeleteOrphan()
. This prevents NHibernate from inserting blank records into the ClassA and ClassB tables after nulling out the references on the Relationship object
public ClassAMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasMany(x => x.Relationship)
.KeyColumn("ClassA_Id")
.ForeignKeyConstraintName("FK_Relationship_ClassA")
.LazyLoad()
.Cascade.AllDeleteOrphan() // here's the key line
.Inverse();
}