使用Cascade.All时出现TransientObjectException

时间:2013-12-10 18:58:58

标签: c# .net nhibernate orm nhibernate-mapping

在我的对象图中,PersonAddress具有多对多关系,并且连接表还有其他列。

班级结构

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; }
}

映射

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));   
    }
}

用法

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(); 
}

异常

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

我相信上面的代码不会有问题,因为我正在保存Person,它会级联到PersonAddress,然后级联到Address。但是,NHibernate告诉我要么自动保存它(使用级联?),要么自己保存它。

解决方法

session.Save(person);
session.Save(address);

transaction.Commit(); 

然而,这是非常有问题的,因为实际的生产代码比简短的例子复杂得多。在实际的生产代码中,我有一个Organization对象,其中包含一个Person列表(其中包含personaddresses和地址)。

有没有办法解决这个问题,而不必进行额外的Save调用,因为在尝试将我的应用程序逻辑与持久性逻辑分开时,很难以通用的方式编写它。

为什么解决方法不适合我的方案

// 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();
}

正如您所看到的,UnitOfWorkUnitOfWorkFactoryOrganizationRepository都是抽象,因此我无法在不泄露实施细节的情况下保存地址和人员,如果持久性按照我的预期进行级联,我认为我应该能够做到这一点。

我的问题是,如果没有明确告诉NHibernate这样做,我如何坚持Address

2 个答案:

答案 0 :(得分:1)

有一件事是Address不是PersonAddress的子代。 PersonAddress是Person和Address的子节点。你可以说因为ManyToOne。

我还会将关系的另一端从Address down映射到PersonAddress。您需要这样才能将关系标记为INVERSE,因为看起来您希望子PersonAddress处理关系的所有权。

这是一个应该保存所有内容的快速映射。

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);
    }
}

并通过单元测试

    [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();
            }
        }
    }

答案 1 :(得分:1)

除非PersonAddress的映射不代表composite-id,否则您的所有内容都会有效。

尽管如此,您可以在Cascade.All映射中使用CompositeId

ComposedId(map =>
{
    map.ManyToOne( x => x.Person, 
            m => { m.Cascade(Cascade.All); // Cascade here is not applied

这不会被应用。 <composite-id> (doc 5.1.5)子元素<key-many-to-one>不支持级联。

但是,如果PersonAddress有一些代理键,所有内容都会有效,并且 Person Adress 将与many-to-one

一起映射为标准cascade="all"

另请参阅此处的答案NHibernate - How to map composite-id with parent child reference ...以获取更多使用代理的理由,而不是复合身份