【问题标题】:TransientObjectException when using Cascade.All使用 Cascade.All 时出现 TransientObjectException
【发布时间】: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

【问题讨论】:

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


    【解决方案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 的位置。
    猜你喜欢
    • 2020-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-15
    • 1970-01-01
    • 2016-06-01
    相关资源
    最近更新 更多