문제

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

다른 팁

One thing is that Address is not a child of PersonAddress. PersonAddress is the child of both Person and Address. You can tell because of the ManyToOne.

I would also map the other side of the relationship from Address down to PersonAddress. You need to this so that you can mark the relationship INVERSE because it looks like you want the child PersonAddress to handle the ownership of the relationship.

Here's a quick mapping that should save everything.

public class Person
{
    public virtual Guid Id { get; protected set; }
    public virtual String Name { get; set; }
    public virtual ICollection<PersonAddress> PersonAddresses { get; protected set; }

    public Person()
    {
        PersonAddresses = new List<PersonAddress>();
    }

    public virtual void AddPersonAddress(PersonAddress personAddress)
    {
        if (PersonAddresses.Contains(personAddress))
            return;

        PersonAddresses.Add(personAddress);
        personAddress.Person = this;
    }
}

public class PersonMap : ClassMapping<Person>
{
    public PersonMap()
    {
        Id(x => x.Id, map =>
        {
            map.Column("Id");
            map.Generator(Generators.GuidComb);
        });

        Property(x => x.Name);

        Bag(x => x.PersonAddresses, map =>
        {
            map.Table("PersonAddress");
            map.Key(k =>
            {
                k.Column(col => col.Name("PersonId"));
            });
            map.Cascade(Cascade.All);
        },
        action => action.OneToMany());
    }
}

public class Address
{
    public virtual Guid Id { get; protected set; }
    public virtual String AddressLine1 { get; set; }
    public virtual ICollection<PersonAddress> PersonAddresses { get; protected set; }

    public Address()
    {
        PersonAddresses = new List<PersonAddress>();
    }
}

public class AddressMap : ClassMapping<Address>
{
    public AddressMap()
    {
        Id(x => x.Id, map =>
        {
            map.Column("Id");
            map.Generator(Generators.GuidComb);
        });

        Property(x => x.AddressLine1);

        Bag(x => x.PersonAddresses, map =>
        {
            map.Inverse(true);
            map.Table("PersonAddress");
            map.Key(k =>
            {
                k.Column(col => col.Name("AddressId"));
            });
            //map.Cascade(Cascade.All);
        },
        action => action.OneToMany());
    }
}

public class PersonAddress
{
    public virtual Guid Id { get; set; }
    public virtual Person Person { get; set; }
    public virtual Address Address { get; set; }

    public virtual String Description { get; set; }
}

public class PersonAddressMap : ClassMapping<PersonAddress>
{
    public PersonAddressMap()
    {
        Id(x => x.Id, map =>
        {
            map.Column("Id");
            map.Generator(Generators.GuidComb);
        });

        ManyToOne(x => x.Person, map =>
        {
            map.Column("PersonId");
            map.NotNullable(false);
        });

        ManyToOne(x => x.Address, map =>
        {
            map.Column("AddressId");
            map.NotNullable(false);
            map.Cascade(Cascade.All);
        });

        Property(x => x.Description);
    }
}

And passing unit test

    [Test]
    public void CascadeMapTest()
    {
        using (ISession session = SessionFactory.OpenSession())
        {
            using (ITransaction tx = session.BeginTransaction())
            {
                var person = new Person { Name = "Test" };
                person.AddPersonAddress(new PersonAddress { Address = new Address { AddressLine1 = "123 main street" }, Description = "WORK" });

                session.Save(person);

                tx.Commit();
            }
        }
    }
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top