nhibernate不保存外键ID

时间:2012-04-27 02:44:34

标签: c# nhibernate fluent-nhibernate

我有一个简单的模型,我试图使用流利的nhibernate持续:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IList<Address> Addresses { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public int PersonId { get; set; }
    public string Street { get; set; }        
}

一些示例数据:

var person = new Person() {Name = "Name1", Addresses = new[]
    {
        new Address { Street = "Street1"},
        new Address { Street = "Street2"}
    }};

当我调用session.SaveOrUpdate(person)时,两个对象都被持久化但外键未保存在地址表中:

enter image description here

我做错了什么?我的映射覆盖如下:

public class PersonOverrides : IAutoMappingOverride<Person>
{
    public void Override(AutoMapping<Person> mapping)
    {
        mapping.Id(x => x.Id);
        mapping.HasMany(x => x.Addresses).KeyColumn("PersonId").Cascade.All();            
    }
}

public class AddressOverrides : IAutoMappingOverride<Address>
{
    public void Override(AutoMapping<Address> mapping)
    {
        mapping.Id(x => x.Id);               
    }
}

请注意,我打算在其他实体中使用List<Address>,我不想添加Address.Person属性。

更新1

我通过将Address.PersonId替换为Address.Person来实现“工作”,但我不希望Address拥有Person属性,因为我不希望这样循环参考。此外,当插入上面的对象时,看到nHibernate出现的日志 1)插入人物 2)使用NULL PersonId插入地址 3)使用PersonId更新地址(刷新时) 当真正的步骤2&amp; 3可以同时完成吗?如果在Address.PersonId

上不允许NULL,则会导致另一个问题

更新2 删除属性Address.PersonId会导致PersonId填充到数据库中。 nHibernate不喜欢我提供我自己的PersonId,它在内部清楚地用于插入/检索记录。所以我真的想要标记我的Address.PersonId'嘿,这不是一个独立的领域,你要在轨道上使用的领域请特别对待它'旗帜。另外,如上所述,nHibernate似乎在PersonId列中插入NULL(当Save时),然后THEN更新它(当Flush时)?

5 个答案:

答案 0 :(得分:10)

我模拟了您的问题情况,插入时具有空父键的子项,然后使用右父键更新。

BEGIN; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
select nextval ('person_person_id_seq')
select nextval ('person_person_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('phone_number_phone_number_id_seq')
INSERT INTO person (lastname, firstname, person_id) VALUES (((E'McCartney')::text), ((E'Paul')::text), ((109)::int4))
INSERT INTO person (lastname, firstname, person_id) VALUES (((E'Lennon')::text), ((E'John')::text), ((110)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'9')::text), ((NULL)::int4), ((306)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'8')::text), ((NULL)::int4), ((307)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'6')::text), ((NULL)::int4), ((308)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'1')::text), ((109)::int4), ((309)::int4))
UPDATE phone_number SET person_id = ((110)::int4) WHERE phone_number_id = ((306)::int4)
UPDATE phone_number SET person_id = ((110)::int4) WHERE phone_number_id = ((307)::int4)
UPDATE phone_number SET person_id = ((110)::int4) WHERE phone_number_id = ((308)::int4)
UPDATE phone_number SET person_id = ((110)::int4) WHERE phone_number_id = ((309)::int4)
COMMIT

缺少反向......

public class PersonMap : ClassMap<Person>
{
    public PersonMap ()
    {           
        Id (x => x.PersonId).GeneratedBy.Sequence("person_person_id_seq");

        Map (x => x.Lastname).Not.Nullable();
        Map (x => x.Firstname).Not.Nullable();

        // No Inverse
        HasMany(x => x.PhoneNumbers).Cascade.All ();
    }
}

public class PhoneNumberMap : ClassMap<PhoneNumber>     
{
    public PhoneNumberMap ()
    {
        References(x => x.Person);          

        Id (x => x.PhoneNumberId).GeneratedBy.Sequence("phone_number_phone_number_id_seq");

        Map (x => x.ThePhoneNumber).Not.Nullable();                       
    }
}

......父母有责任拥有子实体。

这就是为什么即使你没有向孩子(集合)表示反向而且孩子没有任何预定义的父母,你的孩子似乎能够正确地坚持自己...

public static void Main (string[] args)
{
    var sess = Mapper.GetSessionFactory().OpenSession();

    var tx = sess.BeginTransaction();

    var jl = new Person { Firstname = "John", Lastname = "Lennon", PhoneNumbers = new List<PhoneNumber>() };
    var pm = new Person { Firstname = "Paul", Lastname = "McCartney", PhoneNumbers = new List<PhoneNumber>() };

    // Notice that we didn't indicate Parent key(e.g. Person = jl) for ThePhoneNumber 9.       
    // If we don't have Inverse, it's up to the parent entity to own the child entities
    jl.PhoneNumbers.Add(new PhoneNumber { ThePhoneNumber = "9" });
    jl.PhoneNumbers.Add(new PhoneNumber { ThePhoneNumber = "8" });
    jl.PhoneNumbers.Add(new PhoneNumber { ThePhoneNumber = "6" });

    jl.PhoneNumbers.Add(new PhoneNumber { Person = pm, ThePhoneNumber = "1" });


    sess.Save (pm);
    sess.Save (jl);                     


    tx.Commit();            
}

...因此我们可以说,由于缺少Inverse属性,我们的对象图的持久性只是一个侥幸;在具有良好设计的数据库上,最重要的是我们的数据是一致的,这是必须的,我们永远不应该在子项的外键上指明可为空,特别是如果该子项与父项紧密耦合。在上面的场景中,即使ThePhoneNumber“1”表示保罗·麦卡特尼为其父母,约翰·列侬稍后将拥有该电话号码,因为它包含在约翰的儿童实体中;这是不使用Inverse标记子实体的本质,即使孩子想要属于其他父级,父级也会主动拥有属于它的所有子实体。没有反向,孩子们没有权利选择自己的父母: - )

查看上面的SQL日志以查看此Main的输出


<强>反

然后,当指示对子实体的反向时,这意味着孩子有责任选择自己的父母;父实体永远不会干涉。

所以给定上面方法Main的相同数据集,尽管在子实体上有反向属性...

HasMany(x => x.PhoneNumbers).Inverse().Cascade.All ();

...,John Lennon将不会有任何孩子,ThePhoneNumber“1”选择自己的父母(Paul McCartney),即使那个电话号码在John Lennon的子女实体中,它仍将与Paul McCartney一起保存到数据库中作为其父母。未选择其父母的其他电话号码将保持无父母。使用Inverse,孩子可以自由选择自己的父母,没有积极的父母可以拥有任何人的孩子。

后端方面,这就是对象图的持久性:

BEGIN; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
select nextval ('person_person_id_seq')
select nextval ('person_person_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('phone_number_phone_number_id_seq')
INSERT INTO person (lastname, firstname, person_id) VALUES (((E'McCartney')::text), ((E'Paul')::text), ((111)::int4))
INSERT INTO person (lastname, firstname, person_id) VALUES (((E'Lennon')::text), ((E'John')::text), ((112)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'9')::text), ((NULL)::int4), ((310)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'8')::text), ((NULL)::int4), ((311)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'6')::text), ((NULL)::int4), ((312)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'1')::text), ((111)::int4), ((313)::int4))
COMMIT

那么持久化root对象及其子实体有什么好的做法?

首先,我们可以说这是对Hibernate / NHibernate团队在使Inverse成为非默认行为方面的疏忽。我们大多数人都非常谨慎地认为数据的一致性,永远不会使外键可以为空。因此,我们应该始终明确指出反向作为默认行为。

其次,每当我们将一个子实体添加到父级时,通过父级的辅助方法执行此操作。因此,即使我们忘记指示孩子的父母,帮助方法也可以明确地拥有该子实体。

public class Person
{
    public virtual int PersonId { get; set; }
    public virtual string Lastname { get; set; }
    public virtual string Firstname { get; set; }

    public virtual IList<PhoneNumber> PhoneNumbers { get; set; }


    public virtual void AddToPhoneNumbers(PhoneNumber pn)
    {
        pn.Person = this;
        PhoneNumbers.Add(pn);
    }
}

这是我们的对象持久化例程的样子:

public static void Main (string[] args)
{
    var sess = Mapper.GetSessionFactory().OpenSession();

    var tx = sess.BeginTransaction();

    var jl = new Person { Firstname = "John", Lastname = "Lennon", PhoneNumbers = new List<PhoneNumber>() };
    var pm = new Person { Firstname = "Paul", Lastname = "McCartney", PhoneNumbers = new List<PhoneNumber>() };

    jl.AddToPhoneNumbers(new PhoneNumber { ThePhoneNumber = "9" });
    jl.AddToPhoneNumbers(new PhoneNumber { ThePhoneNumber = "8" });
    jl.AddToPhoneNumbers(new PhoneNumber { ThePhoneNumber = "6" });

    pm.AddToPhoneNumbers(new PhoneNumber { ThePhoneNumber = "1" });


    sess.Save (pm);
    sess.Save (jl);                     


    tx.Commit();            
}

这是我们的对象持久化的方式:

BEGIN; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
select nextval ('person_person_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('person_person_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('phone_number_phone_number_id_seq')
select nextval ('phone_number_phone_number_id_seq')
INSERT INTO person (lastname, firstname, person_id) VALUES (((E'McCartney')::text), ((E'Paul')::text), ((113)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'1')::text), ((113)::int4), ((314)::int4))
INSERT INTO person (lastname, firstname, person_id) VALUES (((E'Lennon')::text), ((E'John')::text), ((114)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'9')::text), ((114)::int4), ((315)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'8')::text), ((114)::int4), ((316)::int4))
INSERT INTO phone_number (the_phone_number, person_id, phone_number_id) VALUES (((E'6')::text), ((114)::int4), ((317)::int4))
COMMIT

Inverse的另一个很好的类比:https://stackoverflow.com/a/1067854

答案 1 :(得分:1)

通过对原始代码进行一些调整,可以实现这一点,为了清楚起见,我将发布整个代码,

public class Person
    {
        public virtual int Id { get; set; }

        public virtual string Name { get; set; }

        public virtual IList<Address> Addresses { get; set; }

    }

public class Address
    {
        public virtual int Id { get; set; }

        public virtual int PersonId { get; set; }

        public virtual string Street { get; set; }  
    }

public class PersonOverrides : IAutoMappingOverride<Person>
    {
        public void Override(AutoMapping<Person> mapping)
        {
            mapping.Id(x => x.Id);
            mapping.HasMany(x => x.Addresses).KeyColumn("PersonId").Cascade.All();            
        }
    }

以下是更改后的代码

public class AddressOverrides : IAutoMappingOverride<Address>
    {
        public void Override(AutoMapping<Address> mapping)
        {
            mapping.Id(x => x.Id);
            mapping.Map(x => x.PersonId).Column("PersonId");
        }
    }

答案 2 :(得分:0)

您的PersonId属性似乎是映射和常规属性,而不是对其他对象的引用。所以我建议你尝试两件事:

  1. 尝试将PersonId属性更改为Person类型并将其命名为Person
  2. 验证您的代码是否在转换中执行(它可能会影响nhibernate如何与关联一起使用)
  3. 将生成的自动化保存到XML文件中,看看nhiberante如何与您的模型实际配合使用

答案 3 :(得分:0)

您需要使用参考。我不熟悉IAutoMappingOverride,但这是我在手动映射中的方法,请注意AnswerMap的问题参考:

public class QuestionMap : ClassMap<Question>
{
    public QuestionMap()
    {
        Id(x => x.QuestionId).GeneratedBy.Sequence("question_seq");

        Map(x => x.TheQuestion).Not.Nullable();

        HasMany(x => x.Answers).Inverse().Not.LazyLoad().Cascade.AllDeleteOrphan();
    }
}

public class AnswerMap : ClassMap<Answer>
{
    public AnswerMap()
    {
        References(x => x.Question);

        Id(x => x.AnswerId).GeneratedBy.Sequence("answer_seq");

        Map(x => x.TheAnswer).Not.Nullable();
    }
}

<小时/> 在这里建模:

public class Question
{
    public virtual int QuestionId { get; set; }

    public virtual string TheQuestion { get; set; }        

    public virtual IList<Answer> Answers { get; set; }
}

public class Answer
{
    public virtual Question Question { get; set; }

    public virtual int AnswerId { get; set; }

    public virtual string TheAnswer { get; set; }                
}

请注意,我们在Answer类上没有使用public virtual int QuestionId { get; set; },我们应该使用public virtual Question Question { get; set; }。这种方式更多是OOP,它基本上清楚你的领域模型是怎样的,不受对象如何相互关联的影响(不是通过int,不是通过字符串等;而是通过对象引用)

为了减轻您的后顾之忧,加载对象(通过session.Load)不会导致数据库往返。

var answer = new Answer {
   // session.Load does not make database request
   Question = session.Load<Question>(primaryKeyValueHere), 

   TheAnswer = "42"
};

答案 4 :(得分:0)

似乎是一个设计问题

如果目的是避免在地址的上下文使用的地址中引用地址内的人

然后我会介绍一个“鉴别器”

即。

Address {AddressId, ...}
PersonAddress : Address {Person, ...}, 
CustomerAddress : Address {Customer, ...}, 
VendorAddress : Address {Vendor, ...}

您可以通过公式推断鉴别器,而不是指定硬判别值

参考:Discriminator based on joined property

如果允许/可能

,或者修改db结构
Persons [PersonId, ...]
Addresses [AddressId]
AddressDetails [AddressDetailId, AddressId, ...]

使用如下映射(不知道如何以流畅的方式完成,但可以通过xml完成​​) 1.)Person + Addresses通过连接表 2.)地址+地址详细信息通过参考

我肯定更喜欢第一个选项

欢呼......