nhibernate不保存外键ID

我有一个简单的模型,我试图使用流畅的nhibernate持久化:

public class Person { public int Id { get; set; } public string Name { get; set; } public IList
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)两个对象都是持久的,但外键未保存在Address表中:

在此处输入图像描述

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

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

请注意 ,我打算在其他实体中使用List

,我不想添加Address.Person属性。

更新1

我已经通过用Address.PersonId替换Address.PersonId来“工作”,但我不希望Address具有Person属性,因为我不想要那个循环引用。 此外,当插入上面的对象查看日志时,nHibernate似乎1)插入Person 2)插入地址为NULL PersonId 3)更新地址与PersonId(当刷新时)真正的步骤2和3可以同时完成? 如果在Address.PersonId上不允许NULL,则会导致另一个问题

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

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

 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 { 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 { 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() }; var pm = new Person { Firstname = "Paul", Lastname = "McCartney", PhoneNumbers = new List() }; // Notice that we didn't indicate Parent key(eg 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的相同数据集,尽管子实体上有Inverse属性…

 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成为非默认行为方面的疏忽。 我们大多数人都非常谨慎地看待数据的一致性, 绝不会让外键可以为空。 因此,我们应该始终明确指出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 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() }; var pm = new Person { Firstname = "Paul", Lastname = "McCartney", PhoneNumbers = new List() }; 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

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

 public class Person { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual IList
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 { public void Override(AutoMapping mapping) { mapping.Id(x => x.Id); mapping.HasMany(x => x.Addresses).KeyColumn("PersonId").Cascade.All(); } }

这是更改的代码,

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

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

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

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

 public class QuestionMap : ClassMap { 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 { 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 Answers { get; set; } } public class Answer { public virtual Question Question { get; set; } public virtual int AnswerId { get; set; } public virtual string TheAnswer { get; set; } } 

请注意,我们没有使用public virtual int QuestionId { get; set; } public virtual int QuestionId { get; set; } 在Answer类上,我们应该使用public virtual Question Question { 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(primaryKeyValueHere), TheAnswer = "42" }; 

似乎是一个设计问题

如果目的是为了避免在地址的上下文中使用该地址的人员

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

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

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

参考: 基于联合财产的判别者

或者,如果允许/可能,则修改db结构

 Persons [PersonId, ...] Addresses [AddressId] AddressDetails [AddressDetailId, AddressId, ...] 

使用Mappings如下(不知道如何以流畅的方式完成,但可以通过xml完成​​)1。)Person + Addresses通过连接表2.)Address + AddressDetails via reference

我肯定更喜欢第一种选择

欢呼……