Skip to content

Add support for fetching an individual lazy property with Criteria #2097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 20, 2020
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