我有一个简单的模型,我试图使用流利的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)
时,两个对象都被持久化但外键未保存在地址表中:
我做错了什么?我的映射覆盖如下:
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
更新2
删除属性Address.PersonId
会导致PersonId
填充到数据库中。 nHibernate不喜欢我提供我自己的PersonId,它在内部清楚地用于插入/检索记录。所以我真的想要标记我的Address.PersonId
'嘿,这不是一个独立的领域,你要在轨道上使用的领域请特别对待它'旗帜。另外,如上所述,nHibernate似乎在PersonId列中插入NULL(当Save
时),然后THEN更新它(当Flush
时)?
答案 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属性似乎是映射和常规属性,而不是对其他对象的引用。所以我建议你尝试两件事:
答案 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.)地址+地址详细信息通过参考
我肯定更喜欢第一个选项
欢呼......