nhibernate not saving foreign key Id

2019-03-14 02:05发布

问题:

I have a simple model I'm attempting to persist using 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; }        
}

Some sample data:

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

When I call session.SaveOrUpdate(person) both objects are persisted but the foreign key is not saved in the Address table:

What am I doing wrong? My mappings overrides are as follows:

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

Please note, I plan on using List<Address> in other entities and I don't want to add an Address.Person property.

UPDATE 1

I've got this to 'work' by replacing Address.PersonId with Address.Person but I don't want the Address to have a Person property, as I don't want that circular reference. Also, when inserting the above object looking at the logs nHibernate appears to 1) insert Person 2) insert Address with NULL PersonId 3) update Address with PersonId (when flushing) when really steps 2 & 3 can be done at the same time? This causes another issue if NULL is disallowed on Address.PersonId

UPDATE 2 Removing the property Address.PersonId results in PersonId becoming populated in the database. nHibernate doesnt like me providing my own PersonId that its clearly using internally for inserting/retrieving records. So really I want to flag my Address.PersonId with a 'hey this isnt a standalone field its the field you're going to use down the track please done treat it specially' flag. Also, as above, nHibernate appears to insert NULL into the PersonId column (when Saveing) and THEN update it afterwards (when Flushing) ??

回答1:

I simulate your problem situation, the child with null parent key upon insert which then is updated later with the right parent key.

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

On absence of Inverse...

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

...it's the parent's responsibility to own the child entities.

That's why even you didn't indicate Inverse to the child (collection) and the child don't have any predefined parent, your child is seemingly able to persist itself properly...

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

...,hence we can say that with the absence of Inverse attribute, our object graph's persistability is just a fluke; on a database with a good design, it's paramount that our data is consistent, that is it's a must that we should never indicate nullable on child's foreign keys, especially if that child is tightly coupled to parent. And in the scenario above, even if ThePhoneNumber "1" indicates Paul McCartney as its parent, John Lennon will later own that PhoneNumber, as it is included in John's children entities; this is the nature of not tagging the children entities with Inverse, a parent is aggressive to own all children entities that belong to it, even if the child wanted to belong to other parent. With no Inverse, the children don't have any rights to choose their own parent :-)

Take a look at the SQL log above to see this Main's output


Inverse

Then when indicating Inverse on child entities, it mean it's the child's responsibility to choose its own parent; parent entity will never meddle.

So given the same set of data on the method Main above, albeit with Inverse attribute on child entities...

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

..., John Lennon won't have any children, ThePhoneNumber "1" which choose its own parent(Paul McCartney) even that phone number is in John Lennon's children entities, it will still be persisted to database with Paul McCartney as its parent. Other phone numbers which didn't choose their parent, will remain parentless. With Inverse, a child can freely choose its own parent, there's no aggressive parent that can own anyone's child.

Back-end-wise, this is how the objects graph is persisted:

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

So what's a good practice for persisting root object and its children entities?

First, we could say that it is an oversight on Hibernate/NHibernate team's part on making the Inverse a non-default behavior. Most of us who regarded data consistency with utmost care, would never make foreign keys nullable. So, we should always explicitly indicate the Inverse as the default behavior.

Second, whenever we add a child entity to a parent, do this via parent's helper method. So even we forgot to indicate the child's parent, the helper method can explicitly own that child entity.

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

This is how our object persistence routine shall look like:

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

This is how our objects are persisted:

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

Another good analogy for Inverse: https://stackoverflow.com/a/1067854



回答2:

With a little tweak to your original code this can be achieved, for clarity I will post the entire code,

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

Here is the changed code,

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


回答3:

Your PersonId property seems to be mapped and regular property, not as reference to other object. So I would suggest you to try two things:

  1. Try to change your PersonId property to be of type Person and name it Person
  2. Verify that your code is executed inside transation (it can affect how nhibernate works with associations)
  3. Save your generated automappings into XML files and see how nhiberante actually works with your model


回答4:

You need to use References. I'm not familiar with IAutoMappingOverride, but this is how I do it in manual mapping, notice the AnswerMap's References on Question:

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


Model here:

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

Note that we didn't use public virtual int QuestionId { get; set; } on Answer class, we should use public virtual Question Question { get; set; } instead. That way is more OOP, it's basically clear how your domain model looks like, free from the cruft of how objects should relate to each other(not by int, not by string, etc; but by object references)

To allay your worries, loading object (via session.Load) doesn't incur database roundtrip.

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

   TheAnswer = "42"
};


回答5:

Seems to be a design issue

If the intent is to avoid reference of person within address on a/c of contextual use of the address

Then i would introduce a "discriminator"

i.e.

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

You could infer the discriminator via formula as well rather specifying a hard discriminator value

Ref : Discriminator based on joined property

Alternatively modify the db structure if allowed / possible

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

With Mappings as follows (no idea how this would be done in fluent, but is possible via xml) 1.) Person + Addresses via a join table 2.) Address + AddressDetails via reference

I would definitely prefer the first option

cheers ...