使用 Cascade.All 时出现 TransientObjectException

Posted

技术标签:

【中文标题】使用 Cascade.All 时出现 TransientObjectException【英文标题】:TransientObjectException when using Cascade.All 【发布时间】:2013-12-10 18:58:58 【问题描述】:

在我的对象图中,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 列表(然后有个人地址和地址)。

有没有办法解决这个问题而无需额外调用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

【问题讨论】:

【参考方案1】:

你的所有东西都可以工作......除非PersonAddress 的映射不代表composite-id

尽管事实上,您可以在 CompositeId 映射中使用 Cascade.All

ComposedId(map =>

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

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

但是,如果PersonAddress 有一些代理键,那么所有的东西都会起作用,并且对 PersonAdress 的引用将映射为标准 many-to-onecascade="all"

也可以在这里查看答案NHibernate - How to map composite-id with parent child reference ... 以获得更多使用代理而不是复合 id 的理由

【讨论】:

不使用代理键是否可以实现? 没有。我确定:(对不起。您仍然可以调用 session.Save(address) ... 然后确定。但是在您喜欢的场景中...不。级联不能用于复合主键插入。在这种情况下地址必须存在【参考方案2】:

有一点是,Address 不是 PersonAddress 的子代。 PersonAddress 是 Person 和 Address 的子代。您可以通过 ManyToOne 来判断。

我还会将关系的另一端从 Address 映射到 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();
            
        
    

【讨论】:

当我给 Address 一个对 PersonAddress 的引用并将反向标记为 true 时,我得到了同样的异常(并且当为 false 时)。 谢谢,感谢您的帮助,但是我无法使用此解决方案,因为我无法修改 PersonAddress 表,因此使用复合 id 的原因。 是的。然后查看我的映射并使用来自 Radim 的信息来获取composedId 的东西。变化不大。导入部分是 cascade.all 和 inverse 的位置。

以上是关于使用 Cascade.All 时出现 TransientObjectException的主要内容,如果未能解决你的问题,请参考以下文章

Hibernate - 拥有的实体实例不再引用具有 cascade=”all-delete-orphan” 的集合

hibernate中 cascade属性详解

hibernate里的inverse和cascade是什么区别

NHibernate 不会删除一对多关系中的孤儿

运行 adb 命令时出现问题 / 使用平板设备进行开发时出现问题

使用 FolderBrowserDialog 时出现异常