nhibernate not saving foreign key Id

前端 未结 5 780
没有蜡笔的小新
没有蜡笔的小新 2021-02-04 19:26

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         


        
相关标签:
5条回答
  • 2021-02-04 19:58

    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 ...

    0 讨论(0)
  • 2021-02-04 20:10

    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");
            }
        }
    
    0 讨论(0)
  • 2021-02-04 20:14

    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
    0 讨论(0)
  • 2021-02-04 20:21

    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"
    };
    
    0 讨论(0)
  • 2021-02-04 20:22

    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

    0 讨论(0)
提交回复
热议问题