Search code examples
asp.net-corenhibernatenhibernate-mappingasp.net-core-2.2

Nhibernate collections .Child entities not saved


Using

.NET Core SDK (reflecting any global.json): Version: 2.2.300 Commit: 73efd5bd87

and

Nhibernate 5.2.5

Have the following entities

public class Customer : Entity {
    public Customer() {
        Initialize();
    }
    public virtual string Name { get; set; }
    public virtual string LegalName { get; set; }
    public virtual string VATCode { get; set; }
    public virtual ICollection<Site> Sites { get; set; }
    public virtual DateTime Created { get; set; } = DateTime.UtcNow;

    private void Initialize() {
        Sites = new List<Site>();
    }
}

public class Site : Entity {
    public virtual string Address { get; set; }
    public virtual string City { get; set; }
    public virtual string Country { get; set; }
    public virtual Customer Customer { get; set; }
}

with the following mappers

internal class CustomerConfiguration : ClassMapping<Customer> {
    public CustomerConfiguration() {
        Table( TableNames.CustomersTable );

        Id( x => x.EntityID, im => {
            im.Column( "CustomerID" );
            im.Generator( Generators.Identity );
        } );
        [... code omitted for brevity ...]
        Set<Site>( property => property.Sites,
            collection => {
                collection.Fetch( CollectionFetchMode.Join );
                collection.Lazy( CollectionLazy.Lazy );
                collection.Cascade( Cascade.Persist.Include( Cascade.DeleteOrphans ) );
                collection.Inverse( true );
                collection.Key( keyMapping => {
                    keyMapping.Column( "CustomerID" );
                } );
            },
            mapping => {
                mapping.OneToMany( relationalMapping => {
                    relationalMapping.Class( typeof( Site ) );
                } );
            } );
    }
}

internal class SiteConfiguration : ClassMapping<Site> {
    public SiteConfiguration() {
        Table( TableNames.SitesTable );

        Id( x => x.EntityID, im => {
            im.Column( "SiteID" );
            im.Generator( Generators.Identity );
        } );
        [... code omitted for brevity ...]
        ManyToOne( x => x.Customer, mm => {
            mm.Column( "CustomerID" );
            mm.NotNullable( true );
        } );
    }
}

I guess this mapping is not correct because if I do something like

using ( var session = sessionFactory.OpenSession() ) {
    var customer = new Customer() {
        Name = $"Customer 1",
        LegalName = $"Customer 1 LLC",
        VATCode = "xxxxxxxxx",
        Created = DateTime.UtcNow
    };
    customer.Sites.Add( new Site() {
        Address = $"Address Customer 1",
        City = $"City Customer 1",
        Country = $"Country Customer 1",
        Customer = customer
    } );
    session.Save( customer );
}

I get the following exception

Unhandled Exception: NHibernate.TransientObjectException: 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: Nhibernate.ConsoleApp.Entities.Site, Entity: [Site 0]

Any suggestion?

EDIT

Actually the problem was on a different area. it was because of the presence of two registered Listeners (IDeleteEventListener and IUpdateEventListener). Whenever I add those Update and delete does not work. However I was able to discover the problem thanks to Roman Artiukhin comment


Solution

  • Exception is telling that the root entity Customer is holding the reference to other (Site) entity which is transient entity. That is why, root entity cannot be persisted.

    Scenario 1: No cascade options were configured.
    In this case the child or referenced objects must be saved first.

    using ( var session = sessionFactory.OpenSession() ) {
        var customer = new Customer() {
            Name = $"Customer 1",
            LegalName = $"Customer 1 LLC",
            VATCode = "xxxxxxxxx",
            Created = DateTime.UtcNow
        };
        Site site = new Site() {
            Address = $"Address Customer 1",
            City = $"City Customer 1",
            Country = $"Country Customer 1",
            Customer = customer
        }
        customer.Sites.Add(site);
        session.Save( customer );//<--Save the master
        session.Save( site );//<--Save the site
        ...
        ...
        ...
        session.Flush();//<--You are calling this somewhere in your code
    }
    

    This should work.

    Scenario 2: Cascade options were not configured for all INPUT; UPDATE or DELETE operations.
    In this case the configuration must be changed or child or referenced objects must be saved first.

    You have already:

    so this is not a problem.

    Scenario3: Related transient objects where instantiated and associated to the persitent object but no save operation is to be performed on those objects.
    In this case the trainsient objects must be detached from the current session through the command: ISession.Evict(obj)

    Instead of Evict, you can call the Save to attach those objects explicitly as mentioned above.

    For more insight, have a look at documentation which explains various strategies.