Search code examples
c#.netnhibernateormnhibernate-mapping

TransientObjectException when using Cascade.All


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?


Solution

  • 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