5

我有一个简单的模型,我正在尝试使用 fluent-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.PersonIdAddress.Person但我不希望Address拥有 Person 属性,因为我不想要那个循环引用。此外,当插入上述对象查看日志时,nHibernate 显示为 1) 插入人员 2) 插入带有 NULL PersonId 的地址 3) 使用 PersonId 更新地址(刷新时),当真正可以同时完成第 2 步和第 3 步时?如果在 Address.PersonId 上不允许使用 NULL,这会导致另一个问题

更新 2 删除属性Address.PersonId会导致在PersonId数据库中填充。nHibernate 不喜欢我提供我自己的 PersonId,它显然在内部用于插入/检索记录。所以我真的想Address.PersonId用“嘿,这不是一个独立的字段,它是你要在赛道上使用的字段,请特别对待它”标记我。此外,如上所述,nHibernate 似乎将 NULL 插入 PersonId 列(当Saveing 时),然后在之后更新它(当Flushing 时)?

4

5 回答 5

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();                       
    }
}

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

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

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”表示 Paul McCartney 是其父级,John Lennon 稍后将拥有该 PhoneNumber,因为它包含在 John 的子实体中;这是不使用 Inverse 标记子实体的性质,即使子实体想要属于其他父实体,父实体也会积极拥有属于它的所有子实体。没有逆,孩子没有任何权利选择自己的父母:-)

看看上面的 SQL 日志,看看这个 Main 的输出


然后在子实体上指示 Inverse 时,表示选择自己的父实体是子实体的责任;母公司永远不会干涉。

因此,给定上述 Main 方法的相同数据集,尽管子实体具有 Inverse 属性...

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

...,约翰列侬不会有任何孩子,ThePhoneNumber "1" 选择它自己的父母(保罗麦卡特尼),即使电话号码在约翰列侬的孩子实体中,它仍然会以保罗麦卡特尼作为它的父母被持久化到数据库中. 其他没有选择父母的电话号码将保持无父母状态。使用 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

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

首先,我们可以说这是对 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<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

于 2012-04-29T17:04:58.273 回答
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");
        }
    }
于 2012-05-01T10:46:43.650 回答
0

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

  1. 尝试将您的 PersonId 属性更改为 Person 类型并将其命名为 Person
  2. 验证您的代码是否在事务内部执行(它会影响 nhibernate 与关联的工作方式)
  3. 将您生成的自动映射保存到 XML 文件中,并查看 nhiberante 实际如何与您的模型一起使用
于 2012-04-27T07:14:13.013 回答
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; }                
}

请注意,我们没有public virtual int QuestionId { get; set; }在 Answer 类上使用,我们应该使用public virtual Question Question { get; set; }。这种方式更加面向对象,基本上清楚您的域模型的外观,摆脱对象应该如何相互关联(不是通过 int,不是通过字符串等;而是通过对象引用)

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

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

   TheAnswer = "42"
};
于 2012-04-29T04:43:06.790 回答
-1

似乎是设计问题

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

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

IE

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

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

Ref :基于联合财产的鉴别器

如果允许/可能,或者修改数据库结构

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

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

我肯定更喜欢第一个选项

欢呼...

于 2012-05-05T11:26:57.483 回答