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