In my object graph, a Person
has a many-to-many relationship with Address
, and the join table has additional columns.
Class Structure
class Person
{
private IList<PersonAddress> _personAddresses = new List<PersonAddress>();
public virtual int Id { get; set; }
public virtual IList<PersonAddress> PersonAddresses
{
get { return _personAddresses; }
set { _personAddresses = value; }
}
}
class PersonAddress
{
public virtual Person Person { get; set; }
public virtual Address Address { get; set; }
public virtual string Description { get; set; }
public override bool Equals(...) {...}
public override int GetHashCode(...) {...}
}
class Address
{
public virtual int Id { get; set; }
}
Mapping
class PersonMapping : ClassMapping<Person>
{
public PersonMapping()
{
Id(x => x.ID, m => m.Generator(Generators.Identity));
Bag(
x => x.PersonAddresses,
m => {
m.Cascade(Cascade.All);
m.Access(Accessor.Field);
},
r => r.OneToMany()
);
}
}
public class PersonAddressMapping : ClassMapping<PersonAddress>
{
public PersonAddressMapping()
{
ComposedId(map =>
{
map.ManyToOne(
x => x.Person,
m => {
m.Cascade(Cascade.All);
}
);
map.ManyToOne(
x => x.Address,
m => {
m.Cascade(Cascade.All);
}
);
map.Property(x => x.Description);
});
}
}
public class AddressMapping : ClassMapping<Address>
{
public AddressMapping()
{
Id(x => x.ID, m => m.Generator(Generators.Identity));
}
}
Usage
using (var session = sessionFactory.OpenSession())
using (var transaction = session.BeginTransaction())
{
var person = new Person();
var address = new Address();
var personAddress = new PersonAddress
{
Address = address,
Person = person,
Description = "This is my home address"
};
person.PersonAddresses.Add(personAddress);
session.Save(person);
// exception of NHibernate.TransientObjectException
transaction.Commit();
}
Exception
object references an unsaved transient instance -
save the transient instance before flushing or set
cascade action for the property to something that
would make it autosave.
Type: MyApp.Models.Address, Entity: MyApp.Models.Address
I believe that my above code should not be problematic, as I'm saving a Person
, which cascades down to the PersonAddress
, which then cascades down to the Address
. However, NHibernate is telling me to either autosave it (with cascade?), or to save it myself.
Workaround
session.Save(person);
session.Save(address);
transaction.Commit();
However, this is very problematic as the actual production code is much more complex than the short example. In the actual production code, I have an Organization
object which contains a list of Person
(which then has personaddresses, and addresses).
Is there a way to solve this problem without having to hack in an additional Save
call, as it's difficult to write that in a generic way while try to separate my application logic from the persistence logic.
Why the workaround wont work for my scenario
// where unitOfWork is a wrapper for the session
using (var unitOfWork = unitOfWorkFactory.Create())
{
var organization = unitOfWork.OrganizationRepository.GetById(24151);
organization.AddPerson(new Person {
PersonAddress = new PersonAddress {
Address = new Address(),
Description = "Some description"
}
});
unitOfWork.Commit();
}
As you can see, the UnitOfWork
, UnitOfWorkFactory
, and OrganizationRepository
are all abstractions, and therefore would be impossible for me to save both address and person without leaking that implementation detail, which I think I should be able to do if the persistence cascaded as I expected.
My question is, how do I persist Address
without explicitly telling NHibernate to do so?
All your stuff would work ... unless the mapping of the Person
and Address
won't be representing the composite-id
.
Despite fo the fact, that you could use Cascade.All
inside of the CompositeId
mapping
ComposedId(map =>
{
map.ManyToOne( x => x.Person,
m => { m.Cascade(Cascade.All); // Cascade here is not applied
this won't be applied. The <composite-id>
(doc 5.1.5) sub-element <key-many-to-one>
does not support cascading.
BUT, all the stuff would work, if the PersonAddress
would have some surrogated key, and references to Person and Adress will be mapped as standard many-to-one
with cascade="all"
Also see answers here NHibernate - How to map composite-id with parent child reference ... to get more reasons to use surrogated, not composite id