From ea6918973bbbbbae28fe0a367e23feca30eae471 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Mon, 1 Apr 2019 13:45:07 +0300 Subject: [PATCH 1/4] Support fetching individual lazy properties for Criteria EntityProjection --- .../Async/Criteria/EntityProjectionsTest.cs | 32 ++++++++++++++++++- .../Criteria/EntityProjectionsEntities.cs | 1 + .../Criteria/EntityProjectionsTest.cs | 32 ++++++++++++++++++- src/NHibernate/Criterion/EntityProjection.cs | 31 ++++++++++++++++-- .../Loader/AbstractEntityJoinWalker.cs | 8 ++++- src/NHibernate/Persister/Entity/IQueryable.cs | 3 +- 6 files changed, 100 insertions(+), 7 deletions(-) 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..186040c091c 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,16 @@ public EntityProjection SetFetchLazyProperties(bool fetchLazyProperties = true) return this; } + /// + /// Fetch individual lazy properties or property groups + /// 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 +133,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/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); From 09cae4d71652414f320781ee37c1c081ec6be27b Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Sat, 18 Apr 2020 19:30:40 +0300 Subject: [PATCH 2/4] Small refactoring to simplify 6.0 migration and adjust comments --- src/NHibernate/Criterion/EntityProjection.cs | 1 + .../Loader/OuterJoinableAssociation.cs | 22 ++++++++++--------- src/NHibernate/SelectMode.cs | 5 +++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/NHibernate/Criterion/EntityProjection.cs b/src/NHibernate/Criterion/EntityProjection.cs index 186040c091c..37750e90597 100644 --- a/src/NHibernate/Criterion/EntityProjection.cs +++ b/src/NHibernate/Criterion/EntityProjection.cs @@ -81,6 +81,7 @@ public EntityProjection SetFetchLazyProperties(bool fetchLazyProperties = true) /// /// 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) diff --git a/src/NHibernate/Loader/OuterJoinableAssociation.cs b/src/NHibernate/Loader/OuterJoinableAssociation.cs index 2c064154844..fb59ab9ef67 100644 --- a/src/NHibernate/Loader/OuterJoinableAssociation.cs +++ b/src/NHibernate/Loader/OuterJoinableAssociation.cs @@ -203,6 +203,7 @@ internal string GetSelectFragment(string entitySuffix, string collectionSuffix, { case SelectMode.Undefined: case SelectMode.Fetch: + //TODO 6.0: Just drop obsolete parts making all 3 cases handled by the same code #pragma warning disable 618 return Joinable.SelectFragment( next?.Joinable, @@ -228,16 +229,17 @@ internal string GetSelectFragment(string entitySuffix, string collectionSuffix, case SelectMode.FetchLazyPropertyGroup: return ReflectHelper.CastOrThrow(Joinable, "fetch lazy property") - .SelectFragment( - next?.Joinable, - next?.RHSAlias, - RHSAlias, - collectionSuffix, - ShouldFetchCollectionPersister(), - new EntityLoadInfo(entitySuffix) - { - LazyProperties = EntityFetchLazyProperties - }); + .SelectFragment( + next?.Joinable, + next?.RHSAlias, + RHSAlias, + collectionSuffix, + ShouldFetchCollectionPersister(), + new EntityLoadInfo(entitySuffix) + { + LazyProperties = EntityFetchLazyProperties, + IncludeLazyProps = SelectMode == SelectMode.FetchLazyProperties, + }); case SelectMode.ChildFetch: return ReflectHelper.CastOrThrow(Joinable, "child fetch select mode") .IdentifierSelectFragment(RHSAlias, entitySuffix); 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, From 7133d70fb7a6978d412a510d250ae7cc25c194ec Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Mon, 4 May 2020 08:53:10 +0300 Subject: [PATCH 3/4] undo whitespace changes --- .../Loader/OuterJoinableAssociation.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/NHibernate/Loader/OuterJoinableAssociation.cs b/src/NHibernate/Loader/OuterJoinableAssociation.cs index fb59ab9ef67..512bf615d50 100644 --- a/src/NHibernate/Loader/OuterJoinableAssociation.cs +++ b/src/NHibernate/Loader/OuterJoinableAssociation.cs @@ -203,7 +203,6 @@ internal string GetSelectFragment(string entitySuffix, string collectionSuffix, { case SelectMode.Undefined: case SelectMode.Fetch: - //TODO 6.0: Just drop obsolete parts making all 3 cases handled by the same code #pragma warning disable 618 return Joinable.SelectFragment( next?.Joinable, @@ -229,15 +228,15 @@ internal string GetSelectFragment(string entitySuffix, string collectionSuffix, case SelectMode.FetchLazyPropertyGroup: return ReflectHelper.CastOrThrow(Joinable, "fetch lazy property") - .SelectFragment( - next?.Joinable, - next?.RHSAlias, - RHSAlias, - collectionSuffix, - ShouldFetchCollectionPersister(), - new EntityLoadInfo(entitySuffix) - { - LazyProperties = EntityFetchLazyProperties, + .SelectFragment( + next?.Joinable, + next?.RHSAlias, + RHSAlias, + collectionSuffix, + ShouldFetchCollectionPersister(), + new EntityLoadInfo(entitySuffix) + { + LazyProperties = EntityFetchLazyProperties, IncludeLazyProps = SelectMode == SelectMode.FetchLazyProperties, }); case SelectMode.ChildFetch: From 01dcf98d1f9320eaa357f54a123890dd92bdf601 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Mon, 4 May 2020 08:55:33 +0300 Subject: [PATCH 4/4] use same whitespaces --- src/NHibernate/Loader/OuterJoinableAssociation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NHibernate/Loader/OuterJoinableAssociation.cs b/src/NHibernate/Loader/OuterJoinableAssociation.cs index 512bf615d50..8e3d680240f 100644 --- a/src/NHibernate/Loader/OuterJoinableAssociation.cs +++ b/src/NHibernate/Loader/OuterJoinableAssociation.cs @@ -237,8 +237,8 @@ internal string GetSelectFragment(string entitySuffix, string collectionSuffix, new EntityLoadInfo(entitySuffix) { LazyProperties = EntityFetchLazyProperties, - IncludeLazyProps = SelectMode == SelectMode.FetchLazyProperties, - }); + IncludeLazyProps = SelectMode == SelectMode.FetchLazyProperties, + }); case SelectMode.ChildFetch: return ReflectHelper.CastOrThrow(Joinable, "child fetch select mode") .IdentifierSelectFragment(RHSAlias, entitySuffix);