diff --git a/src/NHibernate.Test/Async/Criteria/EntityProjectionsTest.cs b/src/NHibernate.Test/Async/Criteria/EntityProjectionsTest.cs index ad38f539dfa..9429c309985 100644 --- a/src/NHibernate.Test/Async/Criteria/EntityProjectionsTest.cs +++ b/src/NHibernate.Test/Async/Criteria/EntityProjectionsTest.cs @@ -47,7 +47,17 @@ 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("LazyProp1"); + }); + + rc.Property(ep => ep.LazyProp2, m => + { + m.Lazy(true); + m.FetchGroup("LazyProp2"); + }); rc.ManyToOne(ep => ep.Child1, m => m.Column("Child1Id")); rc.ManyToOne(ep => ep.Child2, m => m.Column("Child2Id")); @@ -340,6 +350,26 @@ public async Task EntityProjectionWithLazyPropertiesFetchedAsync() Assert.That(entityRoot, Is.Not.Null); Assert.That(NHibernateUtil.IsInitialized(entityRoot), Is.True, "Object must be initialized"); Assert.That(NHibernateUtil.IsPropertyInitialized(entityRoot, nameof(entityRoot.LazyProp)), Is.True, "Lazy property must be initialized"); + Assert.That(NHibernateUtil.IsPropertyInitialized(entityRoot, nameof(entityRoot.LazyProp2)), Is.True, "Lazy property must be initialized"); + } + } + + [Test] + public async Task EntityProjectionWithLazyPropertiesSinglePropertyFetchAsync() + { + using (var session = OpenSession()) + { + EntityComplex entityRoot; + entityRoot = await (session + .QueryOver() + .Where(ec => ec.LazyProp != null) + .Select(Projections.RootEntity().SetFetchLazyPropertyGroups(nameof(entityRoot.LazyProp))) + .Take(1).SingleOrDefaultAsync()); + + Assert.That(entityRoot, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(entityRoot), Is.True, "Object must be initialized"); + Assert.That(NHibernateUtil.IsPropertyInitialized(entityRoot, nameof(entityRoot.LazyProp)), Is.True, "Lazy property must be initialized"); + Assert.That(NHibernateUtil.IsPropertyInitialized(entityRoot, nameof(entityRoot.LazyProp2)), Is.False, "Property must be lazy"); } } diff --git a/src/NHibernate.Test/Criteria/EntityProjectionsEntities.cs b/src/NHibernate.Test/Criteria/EntityProjectionsEntities.cs index 9cbd8d892ee..9aa6d890243 100644 --- a/src/NHibernate.Test/Criteria/EntityProjectionsEntities.cs +++ b/src/NHibernate.Test/Criteria/EntityProjectionsEntities.cs @@ -24,6 +24,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; } diff --git a/src/NHibernate.Test/Criteria/EntityProjectionsTest.cs b/src/NHibernate.Test/Criteria/EntityProjectionsTest.cs index e377106dcd2..25220eb157a 100644 --- a/src/NHibernate.Test/Criteria/EntityProjectionsTest.cs +++ b/src/NHibernate.Test/Criteria/EntityProjectionsTest.cs @@ -36,7 +36,17 @@ 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("LazyProp1"); + }); + + rc.Property(ep => ep.LazyProp2, m => + { + m.Lazy(true); + m.FetchGroup("LazyProp2"); + }); rc.ManyToOne(ep => ep.Child1, m => m.Column("Child1Id")); rc.ManyToOne(ep => ep.Child2, m => m.Column("Child2Id")); @@ -329,6 +339,26 @@ public void EntityProjectionWithLazyPropertiesFetched() Assert.That(entityRoot, Is.Not.Null); Assert.That(NHibernateUtil.IsInitialized(entityRoot), Is.True, "Object must be initialized"); Assert.That(NHibernateUtil.IsPropertyInitialized(entityRoot, nameof(entityRoot.LazyProp)), Is.True, "Lazy property must be initialized"); + Assert.That(NHibernateUtil.IsPropertyInitialized(entityRoot, nameof(entityRoot.LazyProp2)), Is.True, "Lazy property must be initialized"); + } + } + + [Test] + public void EntityProjectionWithLazyPropertiesSinglePropertyFetch() + { + using (var session = OpenSession()) + { + EntityComplex entityRoot; + entityRoot = session + .QueryOver() + .Where(ec => ec.LazyProp != null) + .Select(Projections.RootEntity().SetFetchLazyPropertyGroups(nameof(entityRoot.LazyProp))) + .Take(1).SingleOrDefault(); + + Assert.That(entityRoot, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(entityRoot), Is.True, "Object must be initialized"); + Assert.That(NHibernateUtil.IsPropertyInitialized(entityRoot, nameof(entityRoot.LazyProp)), Is.True, "Lazy property must be initialized"); + Assert.That(NHibernateUtil.IsPropertyInitialized(entityRoot, nameof(entityRoot.LazyProp2)), Is.False, "Property must be lazy"); } } diff --git a/src/NHibernate/Criterion/EntityProjection.cs b/src/NHibernate/Criterion/EntityProjection.cs index 814e1a0decd..37750e90597 100644 --- a/src/NHibernate/Criterion/EntityProjection.cs +++ b/src/NHibernate/Criterion/EntityProjection.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using NHibernate.Engine; using NHibernate.Loader; using NHibernate.Loader.Criteria; +using NHibernate.Persister.Entity; using NHibernate.SqlCommand; using NHibernate.Type; using IQueryable = NHibernate.Persister.Entity.IQueryable; @@ -38,10 +40,16 @@ public EntityProjection(System.Type entityType, string entityAlias) } /// - /// Fetch lazy properties + /// Fetch all lazy properties /// public bool FetchLazyProperties { get; set; } + /// + /// Fetch individual lazy properties or property groups + /// Note: To fetch single property it must be mapped with unique fetch group (lazy-group) + /// + public ICollection FetchLazyPropertyGroups { get; set; } + /// /// Lazy load entity /// @@ -63,7 +71,7 @@ public EntityProjection SetLazy(bool lazy = true) } /// - /// Fetch lazy properties + /// Fetch all lazy properties /// public EntityProjection SetFetchLazyProperties(bool fetchLazyProperties = true) { @@ -71,6 +79,17 @@ public EntityProjection SetFetchLazyProperties(bool fetchLazyProperties = true) return this; } + /// + /// Fetch individual lazy properties or property groups + /// Provide lazy property name and it will be fetched along with properties that belong to the same fetch group (lazy-group) + /// Note: To fetch single property it must be mapped with unique fetch group (lazy-group) + /// + public EntityProjection SetFetchLazyPropertyGroups(params string[] lazyPropertyGroups) + { + FetchLazyPropertyGroups = lazyPropertyGroups; + return this; + } + #endregion Configuration methods #region IProjection implementation @@ -115,7 +134,14 @@ SqlString IProjection.ToSqlString(ICriteria criteria, int position, ICriteriaQue ? identifierSelectFragment : string.Concat( identifierSelectFragment, - Persister.PropertySelectFragment(TableAlias, ColumnAliasSuffix, FetchLazyProperties))); + GetPropertySelectFragment())); + } + + private string GetPropertySelectFragment() + { + return FetchLazyProperties + ? Persister.PropertySelectFragment(TableAlias, ColumnAliasSuffix, FetchLazyProperties) + : Persister.PropertySelectFragment(TableAlias, ColumnAliasSuffix, FetchLazyPropertyGroups); } SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery) diff --git a/src/NHibernate/Loader/AbstractEntityJoinWalker.cs b/src/NHibernate/Loader/AbstractEntityJoinWalker.cs index 1c2924f32a7..5fa438cc85a 100644 --- a/src/NHibernate/Loader/AbstractEntityJoinWalker.cs +++ b/src/NHibernate/Loader/AbstractEntityJoinWalker.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NHibernate.Criterion; using NHibernate.Engine; -using NHibernate.Loader.Criteria; using NHibernate.Persister.Entity; using NHibernate.SqlCommand; using NHibernate.Type; @@ -62,6 +61,7 @@ protected void InitProjection(SqlString projectionString, SqlString whereString, var associations = new OuterJoinableAssociation[countEntities]; var eagerProps = new bool[countEntities]; var suffixes = new string[countEntities]; + var fetchLazyProperties = new ISet[countEntities]; for (var i = 0; i < countEntities; i++) { var e = entityProjections[i]; @@ -70,6 +70,11 @@ protected void InitProjection(SqlString projectionString, SqlString whereString, { eagerProps[i] = true; } + + if (e.FetchLazyPropertyGroups?.Count > 0) + { + fetchLazyProperties[i] = new HashSet(e.FetchLazyPropertyGroups); + } suffixes[i] = e.ColumnAliasSuffix; } @@ -77,6 +82,7 @@ protected void InitProjection(SqlString projectionString, SqlString whereString, Suffixes = suffixes; EagerPropertyFetches = eagerProps; + EntityFetchLazyProperties = fetchLazyProperties; } else { diff --git a/src/NHibernate/Loader/OuterJoinableAssociation.cs b/src/NHibernate/Loader/OuterJoinableAssociation.cs index 2c064154844..8e3d680240f 100644 --- a/src/NHibernate/Loader/OuterJoinableAssociation.cs +++ b/src/NHibernate/Loader/OuterJoinableAssociation.cs @@ -236,7 +236,8 @@ internal string GetSelectFragment(string entitySuffix, string collectionSuffix, ShouldFetchCollectionPersister(), new EntityLoadInfo(entitySuffix) { - LazyProperties = EntityFetchLazyProperties + LazyProperties = EntityFetchLazyProperties, + IncludeLazyProps = SelectMode == SelectMode.FetchLazyProperties, }); case SelectMode.ChildFetch: return ReflectHelper.CastOrThrow(Joinable, "child fetch select mode") diff --git a/src/NHibernate/Persister/Entity/IQueryable.cs b/src/NHibernate/Persister/Entity/IQueryable.cs index 2178b43a024..f2de9dd6e52 100644 --- a/src/NHibernate/Persister/Entity/IQueryable.cs +++ b/src/NHibernate/Persister/Entity/IQueryable.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NHibernate.Util; namespace NHibernate.Persister.Entity @@ -16,7 +17,7 @@ internal static class AbstractEntityPersisterExtensions /// Given a query alias and an identifying suffix, render the property select fragment. /// //6.0 TODO: Merge into IQueryable - public static string PropertySelectFragment(this IQueryable query, string alias, string suffix, string[] fetchProperties) + public static string PropertySelectFragment(this IQueryable query, string alias, string suffix, ICollection fetchProperties) { return ReflectHelper.CastOrThrow(query, "individual lazy property fetches") .PropertySelectFragment(alias, suffix, fetchProperties); diff --git a/src/NHibernate/SelectMode.cs b/src/NHibernate/SelectMode.cs index ecefdd0f74a..b023e758829 100644 --- a/src/NHibernate/SelectMode.cs +++ b/src/NHibernate/SelectMode.cs @@ -33,12 +33,13 @@ public enum SelectMode JoinOnly, /// - /// Skips fetching for eagerly mapped association (no-op for lazy association). + /// Skips fetching for fetch="join" association (no-op for lazy association). /// Skip, /// - /// Fetch lazy property group + /// Fetch lazy property group. + /// Provide path to lazy property and it will be fetched along with properties that belong to the same fetch group (lazy-group) /// Note: To fetch single property it must be mapped with unique fetch group (lazy-group) /// FetchLazyPropertyGroup,