Given the following scenario, I want map the type hierarchy to the database schema using Fluent NHibernate.
I am using NHibernate 2.0
Type Hierarchy
public abstract class Item
{
public virtual int ItemId { get; set; }
public virtual string ItemType { get; set; }
public virtual string FieldA { get; set; }
}
public abstract class SubItem : Item
{
public virtual string FieldB { get; set; }
}
public class ConcreteItemX : SubItem
{
public virtual string FieldC { get; set; }
}
public class ConcreteItemY : Item
{
public virtual string FieldD { get; set; }
}
See image
The Item and SubItem classes are abstract.
Database Schema
+----------+ +---------------+ +---------------+
| Item | | ConcreteItemX | | ConcreteItemY |
+==========+ +===============+ +===============+
| ItemId | | ItemId | | ItemId |
| ItemType | | FieldC | | FieldD |
| FieldA | +---------------+ +---------------+
| FieldB |
+----------+
See image
The ItemType field determines the concrete type.
Each record in the ConcreteItemX table has a single corresponding record in the Item table; likewise for the ConcreteItemY table.
FieldB is always null if the item type is ConcreteItemY.
The Mapping (so far)
public class ItemMap : ClassMap<Item>
{
public ItemMap()
{
WithTable("Item");
Id(x => x.ItemId, "ItemId");
Map(x => x.FieldA, "FieldA");
JoinedSubClass<ConcreteItemX>("ItemId", MapConcreteItemX);
JoinedSubClass<ConcreteItemY>("ItemId", MapConcreteItemY);
}
private static void MapConcreteItemX(JoinedSubClassPart<ConcreteItemX> part)
{
part.WithTableName("ConcreteItemX");
part.Map(x => x.FieldC, "FieldC");
}
private static void MapConcreteItemY(JoinedSubClassPart<ConcreteItemY> part)
{
part.WithTableName("ConcreteItemX");
part.Map(x => x.FieldD, "FieldD");
}
}
FieldB is not mapped.
The Question
How do I map the FieldB property of the SubItem class using Fluent NHibernate?
Is there any way I can leverage DiscriminateSubClassesOnColumn using the ItemType field?
Addendum
I am able to achieve the desired result using an hbm.xml file:
<class name="Item" table="Item">
<id name="ItemId" type="Int32" column="ItemId">
<generator class="native"/>
</id>
<discriminator column="ItemType" type="string"/>
<property name="FieldA" column="FieldA"/>
<subclass name="ConcreteItemX" discriminator-value="ConcreteItemX">
<!-- Note the FieldB mapping here -->
<property name="FieldB" column="FieldB"/>
<join table="ConcreteItemX">
<key column="ItemId"/>
<property name="FieldC" column="FieldC"/>
</join>
</subclass>
<subclass name="ConcreteItemY" discriminator-value="ConcreteItemY">
<join table="ConcreteItemY">
<key column="ItemId"/>
<property name="FieldD" column="FieldD"/>
</join>
</subclass>
</class>
How do I accomplish the above mapping using Fluent NHibernate?
Is it possible to mix table-per-class-hierarchy with table-per-subclass using Fluent NHibernate?
I know this is really old, but it is now pretty simple to set up fluent to generate the exact mapping you initially desired. Since I came across this post when searching for the answer, I thought I'd post it.
You just create your ClassMap for the base class without any reference to your subclasses:
public class ItemMap : ClassMap<Item>
{
public ItemMap()
{
this.Table("Item");
this.DiscriminateSubClassesOnColumn("ItemType");
this.Id(x => x.ItemId, "ItemId");
this.Map(x => x.FieldA, "FieldA");
}
}
Then map your abstract subclass like this:
public class SubItemMap: SubclassMap<SubItemMap>
{
public SubItemMap()
{
this.Map(x => x.FieldB);
}
}
Then map your concrete subclasses like so:
public class ConcreteItemXMap : SubclassMap<ConcreteItemX>
{
public ConcretItemXMap()
{
this.Join("ConcreteItemX", x =>
{
x.KeyColumn("ItemID");
x.Map("FieldC")
});
}
}
Hopefully this helps somebody else looking for this type of mapping with fluent.
Well, I'm not sure that it's quite right, but it might work... If anyone can do this more cleanly, I'd love to see it (seriously, I would; this is an interesting problem).
Using the exact class definitions you gave, here are the mappings:
public class ItemMap : ClassMap<Item>
{
public ItemMap()
{
Id(x => x.ItemId);
Map(x => x.ItemType);
Map(x => x.FieldA);
AddPart(new ConcreteItemYMap());
}
}
public class SubItemMap : ClassMap<SubItem>
{
public SubItemMap()
{
WithTable("Item");
// Get the base map and "inherit" the mapping parts
ItemMap baseMap = new ItemMap();
foreach (IMappingPart part in baseMap.Parts)
{
// Skip any sub class parts... yes this is ugly
// Side note to anyone reading this that might know:
// Can you use GetType().IsSubClassOf($GenericClass$)
// without actually specifying the generic argument such
// that it will return true for all subclasses, regardless
// of the generic type?
if (part.GetType().BaseType.Name == "JoinedSubClassPart`1")
continue;
AddPart(part);
}
Map(x => x.FieldB);
AddPart(new ConcreteItemXMap());
}
}
public class ConcreteItemXMap : JoinedSubClassPart<ConcreteItemX>
{
public ConcreteItemXMap()
: base("ItemId")
{
WithTableName("ConcreteItemX");
Map(x => x.FieldC);
}
}
public class ConcreteItemYMap : JoinedSubClassPart<ConcreteItemY>
{
public ConcreteItemYMap()
: base("ItemId")
{
WithTableName("ConcreteItemY");
Map(x => x.FieldD);
}
}
Those mappings produce two hbm.xml files like so (some extraneous data removed for clarity):
<class name="Item" table="`Item`">
<id name="ItemId" column="ItemId" type="Int32">
<generator class="identity" />
</id>
<property name="FieldA" type="String">
<column name="FieldA" />
</property>
<property name="ItemType" type="String">
<column name="ItemType" />
</property>
<joined-subclass name="ConcreteItemY" table="ConcreteItemY">
<key column="ItemId" />
<property name="FieldD">
<column name="FieldD" />
</property>
</joined-subclass>
</class>
<class name="SubItem" table="Item">
<id name="ItemId" column="ItemId" type="Int32">
<generator class="identity" />
</id>
<property name="FieldB" type="String">
<column name="FieldB" />
</property>
<property name="ItemType" type="String">
<column name="ItemType" />
</property>
<property name="FieldA" type="String">
<column name="FieldA" />
</property>
<joined-subclass name="ConcreteItemX" table="ConcreteItemX">
<key column="ItemId" />
<property name="FieldC">
<column name="FieldC" />
</property>
</joined-subclass>
</class>
It's ugly, but it looks like it might generate a usable mapping file and it's Fluent! :/
You might be able to tweak the idea some more to get exactly what you want.
This is how I resolved my inheritance problem:
public static class DataObjectBaseExtension
{
public static void DefaultMap<T>(this ClassMap<T> DDL) where T : IUserAuditable
{
DDL.Map(p => p.AddedUser).Column("AddedUser");
DDL.Map(p => p.UpdatedUser).Column("UpdatedUser");
}
}
You can then add this to your superclass map constructor:
internal class PatientMap : ClassMap<Patient>
{
public PatientMap()
{
Id(p => p.GUID).Column("GUID");
Map(p => p.LocalIdentifier).Not.Nullable();
Map(p => p.DateOfBirth).Not.Nullable();
References(p => p.Sex).Column("RVSexGUID");
References(p => p.Ethnicity).Column("RVEthnicityGUID");
this.DefaultMap();
}
}
The line of code: if (part.GetType().BaseType.Name == "JoinedSubClassPart1")
can be rewritten as follows:
part.GetType().BaseType.IsGenericType && part.GetType().BaseType.GetGenericTypeDefinition() == typeof(JoinedSubClassPart<>)
Related
I have used NHibernate for quite some time now but still struggle to do some "simple" stuff. I am trying to work with a many-to-many relationship between my entity ServiceProvider and Features.
Basically every SERVICE_PROVIDERS can have different features which must be present in my table FEATURES.
These are my mapping files:
ServiceProviders.hbm.xml,
<class name="App.Domain.ServiceProvider, App.Domain" table="ServiceProviders">
<id name="Code" type="System.Guid" unsaved-value="00000000-0000-0000-0000-000000000000">
<column name="ServiceProviderCode" />
<generator class="guid.comb" />
</id>
<property name="Description" type="AnsiString">
<column name="Description" length="150" not-null="true" />
</property>
<set name="Features" table="ServiceProvidersFeatures" access="field.pascalcase-underscore" cascade="save-update" optimistic-lock="false">
<key column="ServiceProviderCode"></key>
<many-to-many class="App.Domain.Feature" column="FeatureCode" not-found="exception" />
</set>
</class>
Features,
<class name="App.Domain.Feature, App.Domain" table="Features">
<id name="Code" type="System.Guid" unsaved-value="00000000-0000-0000-0000-000000000000">
<column name="FeatureCode" />
<generator class="guid.comb" />
</id>
<property name="Description" type="AnsiString">
<column name="Description" length="150" not-null="true" />
</property>
<set name="ServiceProviders" table="ServiceProvidersFeatures" cascade="none" inverse="true" lazy="true" access="field.pascalcase-underscore" optimistic-lock="false" mutable="false">
<key column="FeatureCode"></key>
<many-to-many class="App.Domain.ServiceProvider" column="ServiceProviderCode" not-found="ignore" />
</set>
</class>
And these are the 2 main classes:
ServiceProvider.cs
public class ServiceProvider
{
public ServiceProvider()
{
this._Features = new HashSet<Feature>();
}
public virtual Guid Code { get; protected set; }
public virtual string Description { get; set; }
private ICollection<Feature> _Features = null;
public virtual ReadOnlyCollection<Feature> Features
{
get { return (new List<Feature>(_Features).AsReadOnly()); }
}
}
Feature.cs
public class Feature
{
public Feature()
{
this._ServiceProviders = new HashSet<ServiceProvider>();
}
public virtual Guid Code { get; protected set; }
public virtual string Description { get; set; }
private ICollection<ServiceProvider> _ServiceProviders = null;
public virtual ReadOnlyCollection<ServiceProvider> ServiceProviders
{
get { return (new List<ServiceProvider>(_ServiceProviders).AsReadOnly()); }
}
}
What I am trying to do is fetch all the services-providers (with all the features) where the description starts with a certain string, and they have at least one feature specified (param).
I reckon I need a subquery but I don't know how to build the QueryOver. It should be something like this:
var serviceProviders =
session.QueryOver<App.Domain.ServiceProvider>()
.Inner.JoinAlias(x => x.Features, () => features)
.WhereRestrictionOn(f => f.Description).IsLike("%" + "test" + "%")
.AndRestrictionOn(() => features.Code).IsIn(<SubQuery>)
.List();
I've come up with an extension method with allows me to build expressions:
public static class ServiceProviderSearch
{
public static IQueryOver<App.Domain.ServiceProvider, App.Domain.ServiceProvider> AttachWhereForSearchText(this IQueryOver<App.Domain.ServiceProvider, App.Domain.ServiceProvider> mainQuery, string searchText)
{
if (!string.IsNullOrEmpty(searchText))
{
ICriterion filterSearchText = Expression.Disjunction()
.Add(Restrictions.On<App.Domain.ServiceProvider>(f => f.Description).IsLike(searchText, MatchMode.Anywhere))
.Add(Restrictions.On<App.Domain.ServiceProvider>(f => f.ExtendedDescription).IsLike(searchText, MatchMode.Anywhere));
mainQuery.Where(filterSearchText);
}
return (mainQuery);
}
public static IQueryOver<App.Domain.ServiceProvider, App.Domain.ServiceProvider> AttachWhereForFeatures(this IQueryOver<App.Domain.ServiceProvider, App.Domain.ServiceProvider> mainQuery, App.Domain.ServiceProvider serviceProvider, App.Domain.Feature features, Guid[] listOfFeatures)
{
if ((listOfFeatures != null) && (listOfFeatures.Count() > 0))
{
mainQuery
.Inner.JoinAlias(() => serviceProvider.Features, () => features);
mainQuery.WithSubquery.WhereProperty(() => features.Code)
.In(
QueryOver.Of<App.Domain.Feature>()
.WhereRestrictionOn(f => f.Code)
.IsIn(listOfFeatures)
.Select(f => f.Code)
);
}
return (mainQuery);
}
}
So now I can write something like this:
App.Domain.ServiceProvider serviceProvider = null;
App.Domain.Feature features = null;
App.Domain.Language languages = null;
App.Domain.Service services = null;
Guid[] selectedFeatures = {};
var serviceProviders = Session.QueryOver(() => serviceProvider);
serviceProviders
.AttachWhereForSearchText(<searchText>)
.AttachWhereForFeatures(serviceProvider, features, selectedFeatures);
Results = serviceProviders
.TransformUsing(Transformers.DistinctRootEntity)
.Take(<pageSize>)
.Skip((<page> - 1) * <pageSize>)
.ToList<App.Domain.ServiceProvider>();
Inspiration from this answer.
There are couple of things I would change in your snippet. First, you don't need to specify the % in your IsLike method: NHibernate does that automatically.
Second, you can construct the subquery in the following manner:
var subquery =
session.QueryOver<App.Domain.Feature>()
.WhereRestrictionOn(f => f.Description).IsLike("test", MatchMode.Anywhere)
.Select(f => f.Code);
You can plug this in your main query:
var serviceProviders =
session.QueryOver<App.Domain.ServiceProvider>()
.WithSubquery.WhereProperty(s => s.Code).In(subquery)
.List();
Or you can even try:
var serviceProviders =
session.QueryOver<App.Domain.ServiceProvider>()
.JoinQueryOver<App.Domain.Feature>(s => s.Features)
.WhereRestrictionOn(f => f.Description).IsLike("test", MatchMode.Anywhere)
.List<App.Domain.ServiceProvider>();
Depending on your lazy settings, you can then initialize the Features collection with,
serviceProviders.ToList()
.ForEach(service => NHibernateUtil.Initialize(service.Features));
I have following FNH mapping:
public class ItemMap : ClassMap<Item>
{
public ItemMap ()
{
this.HasManyToMany(a => a.ChildItems).ChildWhere("IsDeleted = 0").AsSet();
}
}
Result hbm file is:
<hibernate-mapping>
<class name="Item" table="Item">
<set name="ChildItems" table="ItemsToChildItems">
...
<many-to-many class="ChildItem" where="IsDeleted = 0">
<column name="ChildItemId" />
</many-to-many>
</set>
</class>
</hibernate-mapping>
I want to implement the same mapping using "sexy mapping by code feature :-) / conformist approach" of NHibernate 3.2
Note:
Following approach doesn't work:
public class ItemMap : ClassMapping<Item>
{
public ItemMap()
{
this.Set(x => x.ChildItems
, map =>
{
map.Where("IsDeleted = 0");
}
, action => action.ManyToMany());
}
}
Because:
It is following FNH mapping:
public class ItemMap : ClassMap<Item>
{
public ItemMap ()
{
this.HasManyToMany(a => a.ChildItems).Where("IsDeleted = 0").AsSet();
}
}
.Where("IsDeleted = 0") and .ChildWhere("IsDeleted = 0") not the same.
HBM differences:
Result hbm file using .ChildWhere("IsDeleted = 0") is:
<hibernate-mapping>
<class name="Item" table="Item">
<set name="ChildItems" table="ItemsToChildItems">
...
<many-to-many class="ChildItem" where="IsDeleted = 0">
<column name="ChildItemId" />
</many-to-many>
</set>
</class>
</hibernate-mapping>
Result hbm file using .Where("IsDeleted = 0") is:
<hibernate-mapping>
<class name="Item" table="Item">
<set name="ChildItems" table="ItemsToChildItems" where="IsDeleted = 0">
...
<many-to-many class="ChildItem">
<column name="ChildItemId" />
</many-to-many>
</set>
</class>
</hibernate-mapping>
Who have a similar problem or can offer a solution? Need help.
i had the same problem and I found a solution:
https://nhibernate.jira.com/browse/NH-2997
Solution in 3 steps:
1) Add new interface method to IManyToManyMapper interface:
public interface IManyToManyMapper : IColumnsMapper
{
void Where(string sqlWhereClause);
}
2) Implement new method in ManyToManyCustomizer class:
public void Where(string sqlWhereClause)
{
customizersHolder.AddCustomizer(propertyPath, (IManyToManyMapper x) => x.Where(sqlWhereClause));
}
3) Implement new method in ManyToManyMapper class:
public void Where(string sqlWhereClause)
{
manyToMany.where = sqlWhereClause;
}
I am looking at moving to Fluent NHibernate - the only issue I have encounter so far is that you can not specify a foreign key name on a joined sub class mapping.
Has anyone got a solution for this, or a workaround?
I found this post but the suggestion clearly was not added to the code.
I would like to avoid customising the code myself if possible.
Any help would be great...
Example:
public class Product
{
public string Name { get; set; }
}
public class Hammer : Product
{
public string Description { get; set; }
}
public class ProductMap : ClassMap<Product, long>
{
public ProductMap()
{
Polymorphism.Implicit();
Map(x => x.Name);
}
}
public class HammerMap : SubclassMap<Hammer>
{
public HammerMap()
{
Extends<Product>();
}
}
This generates something like:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="field.camelcase-underscore" auto-import="false" default-cascade="none" default-lazy="true">
<class xmlns="urn:nhibernate-mapping-2.2" dynamic-insert="true" dynamic-update="true" mutable="true" polymorphism="implicit" optimistic-lock="version" name="Domain.Product, Domain" table="Product">
<id name="Id" type="System.Int64">
<column name="Id" />
<generator class="native">
<param name="sequence">ProductId</param>
</generator>
</id>
<property name="Name" type="System.String">
<column name="Name" />
</property>
<joined-subclass name="Domain.Hammer, Domain" table="Hammer">
<key>
<column name="Product_Id" />
</key>
<property name="Description" type="System.String">
<column name="Description" />
</property>
</joined-subclass>
</class>
</hibernate-mapping>
Note that there is no foreign key name specified in the mapping hbm file - as in:
<joined-subclass name="Domain.Hammer, Domain" table="Hammer">
<key column="Product_Id" foreign-key="FK_Hammer_Product"/>
</joined-subclass>
Try something like this
public class JoinedSubclassForeignKeyConvention : IJoinedSubclassConvention
{
public void Apply(IJoinedSubclassInstance instance)
{
instance.Key.ForeignKey(string.Format("FK_{0}_{1}",
instance.EntityType.Name, instance.Type.Name));
}
}
We need to map simple class using NHibernate:
public class CatalogItem
{
private IList<CatalogItem> children = new List<CatalogItem>();
public Guid Id { get; set; }
public string Name { get; set; }
public CatalogItem Parent { get; set; }
public IList<CatalogItem> Children
{
get { return children; }
}
public bool IsRoot { get { return Parent == null; } }
public bool IsLeaf { get { return Children.Count == 0; } }
}
There are a batch of tutorials in the internet on this subject, but none of them cover little nasty detail: we need order to be preserved in Children collection. We've tried following mapping, but it led to strange exeptions thrown by NHibernate ("Non-static method requires a target.").
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="Domain.Model" assembly="Domain">
<class name="CatalogItem" lazy="false">
<id name="Id" type="guid">
<generator class="guid" />
</id>
<property name="Name" />
<many-to-one name="Parent" class="CatalogItem" lazy="false" />
<list name="Children" cascade="all">
<key property-ref="Parent"/>
<index column="weight" type="Int32" />
<one-to-many not-found="exception" class="CatalogItem"/>
</list>
</class>
</hibernate-mapping>
Does anyone have any thoughts?
I'm no expert, but <key property-ref=...> looks strange to me in this usage. You should be able to do <key column="ParentID"/>, and NHibernate will automatically use the primary key of the associated class -- itself, in this case.
You may also need to set the list to inverse="true", since the relationship is bidirectional. [See section 6.8 in the docs.]
One-to-one relations within nhibernate can be lazyloaded either "false" or "proxy". I was wondering if anyone knows a way to do a lazy one-to-one mapping.
I worked out a hack to achieve the same result by using a lazy set mapped to a private field, and having the public property return the first result of that set. It works, but isn't the cleanest code...
Thanks in advance!
Lazy loading of one-to-one isn't supported unless the association is mandatory. See here for the reasoning.
It boils down to the fact that in order to decide if the other side of the relationship exists (N)Hibernate has to go to the database. Since you've already taken the database hit, you might as well load the full object.
While there are cases where hitting the DB just to see if the related object exists without actually loading the object makes sense (if the related object is very "heavy"), it isn't currently supported in NHibernate.
As far as I know, there isn't a non-hacky way to lazy load a one-to-one. I hope I'm wrong, but last time I checked it was the case.
There is way thought. It's described here in details :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernateTest" namespace="NHibernateTest">
<class name="Person" >
<id name="PersonID" type="Int32">
<generator class="identity" />
</id>
<property name="LastName" type="String" length="50" />
<property name="FirstName" type="String" length="50" />
<many-to-one name="Photo" class="PersonPhoto" />
</class>
<class name="PersonPhoto">
<id name="PersonID" type="Int32">
<generator class="foreign">
<param name="property">Owner</param>
</generator>
</id>
<property name="Photo" type="BinaryBlob" />
<one-to-one name="Owner" class="Person" constrained="true" />
</class>
</hibernate-mapping>
I tried the example used by Artem Tikhomirov above. I kept getting an error that the Photo column does not exist. After looking at this, I figured out that the mapping was off a little. When I changed the many-to-one mapping to specify the column name like this:
many-to-one name="Photo" column="PersonID" class="PersonPhoto" unique="true"
I got it to work. I hope this helps someone :o)
After reading the answers here, I´ve manage to get it to work.
I´m just going to add this example because I´m using a One to One relation with Constrained= False and because it´s a Mapping by Code Example
Two Classes:
public class Pedido:BaseModel
{
public virtual BuscaBase Busca { get; set; }
}
public class BuscaBase : BaseModel
{
public virtual Pedido Pedido { get; set; }
}
Mappings:
public class PedidoMap : ClassMapping<Pedido>
{
public PedidoMap()
{
Id(x => x.Id, x => x.Generator(Generators.Identity));
ManyToOne(x => x.Busca, m =>
{
m.Cascade(Cascade.DeleteOrphans);
m.NotNullable(true); m.Unique(true);
m.Class(typeof(BuscaBase));
});
}
}
public class BuscaBaseMap : ClassMapping<BuscaBase>
{
public BuscaBaseMap()
{
Id(x => x.Id, x => x.Generator(Generators.Sequence, g => g.Params(new { sequence = "buscaefetuada_id_seq" })));
OneToOne(x => x.Pedido, m =>
{
m.Lazy(LazyRelation.NoProxy);
m.Constrained(false);
m.Cascade(Cascade.None);
m.Class(typeof(Pedido));
});
}
}
Note: I was Using for the one-to-one mapping m.PropertyReference(typeof(Pedido).GetProperty("Busca")); but this does't work for the lazy loading. You have to specify the relation using the Class
A quick brief about the Constrained = False used in here, the "Pedido" object might not exist in "BuscaBase" object.
What worked for me is the following (very similar to #Daniel) but I found that it was necessary to specify the LazyRelation.NoProxy on both ends of the mapping.
public class Person
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual PersonDetails Details { get; set; }
public class PersonMap : ClassMapping<Person>
{
public PersonMap()
{
Id(x => x.Id, m =>
{
m.Generator(Generators.Native);
});
Property(x => x.Name);
OneToOne(x => x.Details, m =>
{
m.Lazy(LazyRelation.NoProxy);
m.Cascade(Cascade.Persist);
});
}
}
}
public class PersonDetails
{
public virtual int Id { get; set; }
public virtual string ExtraDetails { get; set; }
public virtual Person Person { get; set; }
public class PersonDetailsMap : ClassMapping<PersonDetails>
{
public PersonDetailsMap()
{
Id(x => x.Id, m =>
{
m.Generator(Generators.Native);
});
Property(x => x.ExtraDetails);
ManyToOne(x => x.Person, m =>
{
m.Lazy(LazyRelation.NoProxy);
m.Unique(true);
m.NotNullable(true);
});
}
}
}
using var session = NhHelper.OpenSession();
var person1 = new Person();
person1.Name = "A";
var person1Details = new PersonDetails();
person1Details.ExtraDetails = "A details";
person1.Details = person1Details;
person1Details.Person = person1;
session.Save(person1);
//because of PersonMapping's Cascade.Persist it is not necessary to manually save person1Details object.
using var session = NhHelper.OpenSession();
foreach(var person in session.Query<Person>()) {
Console.WriteLine(person.Name); //<-- does not load PersonDetails unless it's property is accessed
}
NHibernate 5.3.5
Npgsql 5.0.3 (Postgresql Db).