Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,53 @@ public async Task SelectModeFetchLazyPropertiesAsync()
Assert.That(NHibernateUtil.IsInitialized(root), Is.True);
Assert.That(root.LazyProp, Is.Not.Null);
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp)), Is.True, "Lazy property must be fetched.");
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp2)), Is.True, "Lazy property must be fetched.");

Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected");
}
}

[Test]
public async Task SelectModeFetchKeepLazyPropertiesUninitializedAsync()
{
using (var sqlLog = new SqlLogSpy())
using (var session = OpenSession())
{
var root = await (session.QueryOver<EntityComplex>()
.Fetch(SelectMode.Fetch, ec => ec)
.Where(ec => ec.LazyProp != null)
.Take(1)
.SingleOrDefaultAsync());

Assert.That(root, Is.Not.Null);
Assert.That(NHibernateUtil.IsInitialized(root), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp)), Is.False, "Property must be lazy.");
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp2)), Is.False, "Property must be lazy.");

Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected");
}
}

[Test]
public async Task SelectModeFetchLazyPropertiesFetchGroupAsync()
{
using (var sqlLog = new SqlLogSpy())
using (var session = OpenSession())
{
var root = await (session.QueryOver<EntityComplex>()
.Fetch(SelectMode.FetchLazyPropertyGroup, ec => ec.LazyProp, ec => ec.LazyProp2, ec => ec.SameTypeChild.LazyProp2)
.Where(ec => ec.Id == _parentEntityComplexId)
.SingleOrDefaultAsync());

Assert.That(root, Is.Not.Null);
Assert.That(NHibernateUtil.IsInitialized(root), Is.True);
Assert.That(root.LazyProp, Is.Not.Null);
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp)), Is.True, "Lazy property must be fetched.");
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp2)), Is.True, "Lazy property must be fetched.");
Assert.That(NHibernateUtil.IsInitialized(root.SameTypeChild), Is.True, "Object must be initialized.");
Assert.That(root.SameTypeChild, Is.Not.Null);
Assert.That(NHibernateUtil.IsPropertyInitialized(root.SameTypeChild, nameof(root.LazyProp2)), Is.True, "Lazy property must be fetched.");
Assert.That(NHibernateUtil.IsPropertyInitialized(root.SameTypeChild, nameof(root.LazyProp)), Is.False, "Property must be lazy.");

Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected");
}
Expand Down Expand Up @@ -579,7 +626,16 @@ protected override HbmMapping GetMappings()

rc.Property(x => x.Name);

rc.Property(ep => ep.LazyProp, m => m.Lazy(true));
rc.Property(ep => ep.LazyProp, m =>
{
m.Lazy(true);
m.FetchGroup("LazyGroup");
});
rc.Property(ep => ep.LazyProp2, m =>
{
m.Lazy(true);
m.FetchGroup("LazyGroup2");
});

rc.ManyToOne(
ep => ep.Child1,
Expand Down Expand Up @@ -751,9 +807,12 @@ protected override void OnSetUp()
Child1 = child1,
Child2 = child2,
LazyProp = "SomeBigValue",
LazyProp2 = "SomeBigValue2",
SameTypeChild = new EntityComplex()
{
Name = "ComplexEntityChild"
Name = "ComplexEntityChild",
LazyProp = "LazyProp1",
LazyProp2 = "LazyProp2",
},
ChildrenList = new List<EntitySimpleChild> {child3, child1, child4 },
ChildrenListEmpty = new List<EntityComplex> { },
Expand Down
1 change: 1 addition & 0 deletions src/NHibernate.Test/Criteria/SelectModeTest/Entities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class EntityComplex
public virtual string Name { get; set; }

public virtual string LazyProp { get; set; }
public virtual string LazyProp2 { get; set; }

public virtual EntitySimpleChild Child1 { get; set; }
public virtual EntitySimpleChild Child2 { get; set; }
Expand Down
63 changes: 61 additions & 2 deletions src/NHibernate.Test/Criteria/SelectModeTest/SelectModeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,53 @@ public void SelectModeFetchLazyProperties()
Assert.That(NHibernateUtil.IsInitialized(root), Is.True);
Assert.That(root.LazyProp, Is.Not.Null);
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp)), Is.True, "Lazy property must be fetched.");
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp2)), Is.True, "Lazy property must be fetched.");

Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected");
}
}

[Test]
public void SelectModeFetchKeepLazyPropertiesUninitialized()
{
using (var sqlLog = new SqlLogSpy())
using (var session = OpenSession())
{
var root = session.QueryOver<EntityComplex>()
.Fetch(SelectMode.Fetch, ec => ec)
.Where(ec => ec.LazyProp != null)
.Take(1)
.SingleOrDefault();

Assert.That(root, Is.Not.Null);
Assert.That(NHibernateUtil.IsInitialized(root), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp)), Is.False, "Property must be lazy.");
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp2)), Is.False, "Property must be lazy.");

Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected");
}
}

[Test]
public void SelectModeFetchLazyPropertiesFetchGroup()
{
using (var sqlLog = new SqlLogSpy())
using (var session = OpenSession())
{
var root = session.QueryOver<EntityComplex>()
.Fetch(SelectMode.FetchLazyPropertyGroup, ec => ec.LazyProp, ec => ec.LazyProp2, ec => ec.SameTypeChild.LazyProp2)
.Where(ec => ec.Id == _parentEntityComplexId)
.SingleOrDefault();

Assert.That(root, Is.Not.Null);
Assert.That(NHibernateUtil.IsInitialized(root), Is.True);
Assert.That(root.LazyProp, Is.Not.Null);
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp)), Is.True, "Lazy property must be fetched.");
Assert.That(NHibernateUtil.IsPropertyInitialized(root, nameof(root.LazyProp2)), Is.True, "Lazy property must be fetched.");
Assert.That(NHibernateUtil.IsInitialized(root.SameTypeChild), Is.True, "Object must be initialized.");
Assert.That(root.SameTypeChild, Is.Not.Null);
Assert.That(NHibernateUtil.IsPropertyInitialized(root.SameTypeChild, nameof(root.LazyProp2)), Is.True, "Lazy property must be fetched.");
Assert.That(NHibernateUtil.IsPropertyInitialized(root.SameTypeChild, nameof(root.LazyProp)), Is.False, "Property must be lazy.");

Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected");
}
Expand Down Expand Up @@ -610,7 +657,16 @@ protected override HbmMapping GetMappings()

rc.Property(x => x.Name);

rc.Property(ep => ep.LazyProp, m => m.Lazy(true));
rc.Property(ep => ep.LazyProp, m =>
{
m.Lazy(true);
m.FetchGroup("LazyGroup");
});
rc.Property(ep => ep.LazyProp2, m =>
{
m.Lazy(true);
m.FetchGroup("LazyGroup2");
});

rc.ManyToOne(
ep => ep.Child1,
Expand Down Expand Up @@ -782,9 +838,12 @@ protected override void OnSetUp()
Child1 = child1,
Child2 = child2,
LazyProp = "SomeBigValue",
LazyProp2 = "SomeBigValue2",
SameTypeChild = new EntityComplex()
{
Name = "ComplexEntityChild"
Name = "ComplexEntityChild",
LazyProp = "LazyProp1",
LazyProp2 = "LazyProp2",
},
ChildrenList = new List<EntitySimpleChild> {child3, child1, child4 },
ChildrenListEmpty = new List<EntityComplex> { },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ namespace NHibernate.Persister.Collection
{
using System.Threading.Tasks;
using System.Threading;

public abstract partial class AbstractCollectionPersister : ICollectionMetadata, ISqlLoadableCollection,
IPostInsertIdentityPersister, ISupportSelectModeJoinable, ICompositeKeyPostInsertIdentityPersister
IPostInsertIdentityPersister, ISupportSelectModeJoinable, ICompositeKeyPostInsertIdentityPersister, ISupportLazyPropsJoinable
{

public Task InitializeAsync(object key, ISessionImplementor session, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ namespace NHibernate.Persister.Entity
using System.Threading;
public abstract partial class AbstractEntityPersister : IOuterJoinLoadable, IQueryable, IClassMetadata,
IUniqueKeyLoadable, ISqlLoadable, ILazyPropertyInitializer, IPostInsertIdentityPersister, ILockable,
ISupportSelectModeJoinable, ICompositeKeyPostInsertIdentityPersister
ISupportSelectModeJoinable, ICompositeKeyPostInsertIdentityPersister, ISupportLazyPropsJoinable
{

private partial class GeneratedIdentifierBinder : IBinder
Expand Down
24 changes: 24 additions & 0 deletions src/NHibernate/Impl/CriteriaImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public partial class CriteriaImpl : ICriteria, ISupportEntityJoinCriteria, ISupp
private readonly List<OrderEntry> orderEntries = new List<OrderEntry>(10);
private readonly Dictionary<string, SelectMode> selectModes = new Dictionary<string, SelectMode>();
private readonly Dictionary<string, LockMode> lockModes = new Dictionary<string, LockMode>();
private readonly Dictionary<string, HashSet<string>> _entityFetchLazyProperties = new Dictionary<string, HashSet<string>>();

private int maxResults = RowSelection.NoValue;
private int firstResult;
private int timeout = RowSelection.NoValue;
Expand Down Expand Up @@ -167,6 +169,15 @@ public SelectMode GetSelectMode(string path)
return result;
}

public HashSet<string> GetEntityFetchLazyProperties(string path)
{
if (_entityFetchLazyProperties.TryGetValue(path, out var result))
{
return result;
}
return null;
}

public IResultTransformer ResultTransformer
{
get { return resultTransformer; }
Expand Down Expand Up @@ -366,6 +377,19 @@ public ICriteria Fetch(SelectMode selectMode, string associationPath, string ali
return this;
}

if (selectMode == SelectMode.FetchLazyPropertyGroup)
{
StringHelper.ParsePathAndPropertyName(associationPath, out associationPath, out var propertyName);
if (_entityFetchLazyProperties.TryGetValue(associationPath, out var propertyNames))
{
propertyNames.Add(propertyName);
}
else
{
_entityFetchLazyProperties[associationPath] = new HashSet<string> {propertyName};
}
}

selectModes[associationPath] = selectMode;
return this;
}
Expand Down
2 changes: 1 addition & 1 deletion src/NHibernate/Loader/AbstractEntityJoinWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private void InitStatementString(OuterJoinableAssociation rootAssociation, SqlSt

Suffixes = BasicLoader.GenerateSuffixes(joins + 1);
var suffix = Suffixes[joins];
selectClause = new SqlString(GetSelectFragment(rootAssociation, suffix, null) + SelectString(associations));
selectClause = new SqlString(rootAssociation.GetSelectFragment(suffix, null, null) + SelectString(associations));
}

JoinFragment ojf = MergeOuterJoins(associations);
Expand Down
31 changes: 20 additions & 11 deletions src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,29 +99,37 @@ protected override void WalkEntityTree(IOuterJoinLoadable persister, string alia

protected override OuterJoinableAssociation CreateRootAssociation()
{
var selectMode = GetSelectMode(string.Empty);
var path = string.Empty;
var selectMode = GetSelectMode(path);
if (selectMode == SelectMode.JoinOnly || selectMode == SelectMode.Skip)
{
throw new NotSupportedException($"SelectMode {selectMode} for root entity is not supported. Use {nameof(SelectMode)}.{nameof(SelectMode.ChildFetch)} instead.");
}

return new OuterJoinableAssociation(
Persister.EntityType,
null,
null,
Alias,
JoinType.LeftOuterJoin,
null,
Factory,
CollectionHelper.EmptyDictionary<string, IFilter>(),
selectMode);
return InitAssociation(
new OuterJoinableAssociation(
Persister.EntityType,
null,
null,
Alias,
JoinType.LeftOuterJoin,
null,
Factory,
CollectionHelper.EmptyDictionary<string, IFilter>(),
selectMode),
path);
}

protected override SelectMode GetSelectMode(string path)
{
return translator.RootCriteria.GetSelectMode(path);
}

protected override ISet<string> GetEntityFetchLazyProperties(string path)
{
return translator.RootCriteria.GetEntityFetchLazyProperties(path);
}

private void WalkCompositeComponentIdTree(IOuterJoinLoadable persister, string alias, string path)
{
IType type = persister.IdentifierType;
Expand Down Expand Up @@ -197,6 +205,7 @@ protected override JoinType GetJoinType(IAssociationType type, FetchMode config,

case SelectMode.Fetch:
case SelectMode.FetchLazyProperties:
case SelectMode.FetchLazyPropertyGroup:
case SelectMode.ChildFetch:
case SelectMode.JoinOnly:
IsDuplicateAssociation(lhsTable, lhsColumns, type); //deliberately ignore return value!
Expand Down
3 changes: 3 additions & 0 deletions src/NHibernate/Loader/Criteria/CriteriaLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public CriteriaLoader(IOuterJoinLoadable persister, ISessionFactoryImplementor f
includeInResultRow = walker.IncludeInResultRow;
resultRowLength = ArrayHelper.CountTrue(IncludeInResultRow);
childFetchEntities = walker.ChildFetchEntities;
EntityFetchLazyProperties = walker.EntityFetchLazyProperties;
// fill caching objects only if there is a projection
if (translator.HasProjection)
{
Expand Down Expand Up @@ -95,6 +96,8 @@ protected override bool IsChildFetchEntity(int i)
return childFetchEntities?[i] == true;
}

protected override ISet<string>[] EntityFetchLazyProperties { get; }

public IList List(ISessionImplementor session)
{
return List(session, translator.GetQueryParameters(), querySpaces);
Expand Down
2 changes: 1 addition & 1 deletion src/NHibernate/Loader/Hql/QueryLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ protected override bool[] EntityEagerPropertyFetches
get { return _entityEagerPropertyFetches; }
}

protected override HashSet<string>[] EntityFetchLazyProperties
protected override ISet<string>[] EntityFetchLazyProperties
{
get { return _entityFetchLazyProperties; }
}
Expand Down
Loading