diff --git a/src/NHibernate.Test/Async/Criteria/ProjectionsTest.cs b/src/NHibernate.Test/Async/Criteria/ProjectionsTest.cs index baacbc9fbb1..3294d467fd5 100644 --- a/src/NHibernate.Test/Async/Criteria/ProjectionsTest.cs +++ b/src/NHibernate.Test/Async/Criteria/ProjectionsTest.cs @@ -14,6 +14,7 @@ using System.Text.RegularExpressions; using NHibernate.Criterion; using NHibernate.Dialect; +using NHibernate.SqlCommand; using NHibernate.SqlTypes; using NHibernate.Type; using NUnit.Framework; @@ -57,10 +58,49 @@ protected override void OnSetUp() } } + private void PrepareDataForEntityProjectionTests(out Student gavin, out string courseCode) + { + courseCode = "HIB"; + using (ISession session = Sfi.OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + Course course = new Course + { + CourseCode = "HIB", + Description = "Hibernate Training" + }; + session.Save(course); + + gavin = new Student + { + Name = "Gavin King", + StudentNumber = 667, + + }; + session.Save(gavin); + + Enrolment enrolment = new Enrolment + { + Course = course, + CourseCode = course.CourseCode, + Semester = 1, + Year = 1999, + Student = gavin, + StudentNumber = gavin.StudentNumber + }; + gavin.Enrolments.Add(enrolment); + session.Save(enrolment); + + session.Flush(); + tx.Commit(); + } + } + protected override void OnTearDown() { using (ISession session = Sfi.OpenSession()) { + session.Delete($"from {typeof(Enrolment).FullName}"); session.Delete("from System.Object"); session.Flush(); } @@ -443,5 +483,111 @@ public async Task UseSubquerySumWithNullResultWithProjectionAsync() Assert.AreEqual(0, sum); } } + + [Test] + public async Task UseRootProjection_EagerAsync() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string _); + + using (ISession session = Sfi.OpenSession()) + { + Student g = await (session.CreateCriteria(typeof(Student)) + .Add(Expression.IdEq(gavin.StudentNumber)) + .SetFetchMode("Enrolments", FetchMode.Join) + .SetProjection(Projections.RootEntity(lazy: false)) + .UniqueResultAsync()); + + Assert.That(NHibernateUtil.IsInitialized(g), Is.True, "object must be initialized"); + Assert.That(g, Is.EqualTo(gavin).Using((Student x, Student y) => x.StudentNumber == y.StudentNumber && x.Name == y.Name ? 0 : 1)); + } + } + + [Test] + public async Task UseRootProjection_LazyAsync() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string _); + + using (ISession session = Sfi.OpenSession()) + { + Student g = await (session.CreateCriteria(typeof(Student)) + .Add(Expression.IdEq(gavin.StudentNumber)) + .SetFetchMode("Enrolments", FetchMode.Join) + .SetProjection(Projections.RootEntity(lazy: true)) + .UniqueResultAsync()); + + Assert.That(NHibernateUtil.IsInitialized(g), Is.False, "object must be lazy and not initialized"); + Assert.That(g, Is.EqualTo(gavin).Using((Student x, Student y) => x.StudentNumber == y.StudentNumber && x.Name == y.Name ? 0 : 1)); + } + } + + [Test] + public async Task UseEntityProjection_EagerAsync() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string courseCode); + + using (ISession session = Sfi.OpenSession()) + { + Student g = await (session.CreateCriteria(typeof(Enrolment)) + .Add(Expression.And(Expression.Eq("StudentNumber", gavin.StudentNumber), Expression.Eq("CourseCode", courseCode))) + .CreateAlias("Student", "s", JoinType.InnerJoin) + .SetProjection(Projections.Entity("s", lazy:false)) + .UniqueResultAsync()); + + Assert.That(NHibernateUtil.IsInitialized(g), Is.True, "object must be initialized"); + Assert.That(g, Is.EqualTo(gavin).Using((Student x, Student y) => x.StudentNumber == y.StudentNumber && x.Name == y.Name ? 0 : 1)); + } + } + + [Test] + public async Task UseEntityProjection_LazyAsync() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string courseCode); + + using (ISession session = Sfi.OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + Student g = await (session.CreateCriteria(typeof(Enrolment)) + .Add(Expression.And(Expression.Eq("StudentNumber", gavin.StudentNumber), Expression.Eq("CourseCode", courseCode))) + .CreateAlias("Student", "s", JoinType.InnerJoin) + .SetProjection(Projections.Entity("s", lazy:true)) + .UniqueResultAsync()); + + Assert.That(NHibernateUtil.IsInitialized(g), Is.False, "object must be lazy and not initialized"); + Assert.That(g, Is.EqualTo(gavin).Using((Student x, Student y) => x.StudentNumber == y.StudentNumber && x.Name == y.Name ? 0 : 1)); + } + } + + [Test] + public async Task UseMultipleEntityProjectionsAsync() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string courseCode); + + using (ISession session = Sfi.OpenSession()) + { + Enrolment en = null; + Student s = null; + Course c = null; + + var result = await (session.QueryOver(() => en) + .Where(e => e.StudentNumber == gavin.StudentNumber && e.CourseCode == courseCode) + .JoinAlias(e => e.Student, () => s) + .JoinAlias(e => e.Course, () => c) + .Select(Projections.RootEntity(lazy: true), Projections.Entity(() => s, lazy:false), Projections.Entity(() => c, lazy:false)) + .SingleOrDefaultAsync()); + + en = (Enrolment) result[0]; + s = (Student) result[1]; + c = (Course) result[2]; + + Assert.That(NHibernateUtil.IsInitialized(en), Is.False, "Object must be lazy and not initialized"); + Assert.That(NHibernateUtil.IsInitialized(s), Is.True, "Object must be initialized"); + Assert.That(NHibernateUtil.IsInitialized(c), Is.True, "Object must be initialized"); + } + } } } diff --git a/src/NHibernate.Test/Criteria/ProjectionsTest.cs b/src/NHibernate.Test/Criteria/ProjectionsTest.cs index f4d928f6d9f..c3b4dc62aa8 100644 --- a/src/NHibernate.Test/Criteria/ProjectionsTest.cs +++ b/src/NHibernate.Test/Criteria/ProjectionsTest.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using NHibernate.Criterion; using NHibernate.Dialect; +using NHibernate.SqlCommand; using NHibernate.SqlTypes; using NHibernate.Type; using NUnit.Framework; @@ -46,10 +47,49 @@ protected override void OnSetUp() } } + private void PrepareDataForEntityProjectionTests(out Student gavin, out string courseCode) + { + courseCode = "HIB"; + using (ISession session = Sfi.OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + Course course = new Course + { + CourseCode = "HIB", + Description = "Hibernate Training" + }; + session.Save(course); + + gavin = new Student + { + Name = "Gavin King", + StudentNumber = 667, + + }; + session.Save(gavin); + + Enrolment enrolment = new Enrolment + { + Course = course, + CourseCode = course.CourseCode, + Semester = 1, + Year = 1999, + Student = gavin, + StudentNumber = gavin.StudentNumber + }; + gavin.Enrolments.Add(enrolment); + session.Save(enrolment); + + session.Flush(); + tx.Commit(); + } + } + protected override void OnTearDown() { using (ISession session = Sfi.OpenSession()) { + session.Delete($"from {typeof(Enrolment).FullName}"); session.Delete("from System.Object"); session.Flush(); } @@ -432,5 +472,111 @@ public void UseSubquerySumWithNullResultWithProjection() Assert.AreEqual(0, sum); } } + + [Test] + public void UseRootProjection_Eager() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string _); + + using (ISession session = Sfi.OpenSession()) + { + Student g = session.CreateCriteria(typeof(Student)) + .Add(Expression.IdEq(gavin.StudentNumber)) + .SetFetchMode("Enrolments", FetchMode.Join) + .SetProjection(Projections.RootEntity(lazy: false)) + .UniqueResult(); + + Assert.That(NHibernateUtil.IsInitialized(g), Is.True, "object must be initialized"); + Assert.That(g, Is.EqualTo(gavin).Using((Student x, Student y) => x.StudentNumber == y.StudentNumber && x.Name == y.Name ? 0 : 1)); + } + } + + [Test] + public void UseRootProjection_Lazy() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string _); + + using (ISession session = Sfi.OpenSession()) + { + Student g = session.CreateCriteria(typeof(Student)) + .Add(Expression.IdEq(gavin.StudentNumber)) + .SetFetchMode("Enrolments", FetchMode.Join) + .SetProjection(Projections.RootEntity(lazy: true)) + .UniqueResult(); + + Assert.That(NHibernateUtil.IsInitialized(g), Is.False, "object must be lazy and not initialized"); + Assert.That(g, Is.EqualTo(gavin).Using((Student x, Student y) => x.StudentNumber == y.StudentNumber && x.Name == y.Name ? 0 : 1)); + } + } + + [Test] + public void UseEntityProjection_Eager() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string courseCode); + + using (ISession session = Sfi.OpenSession()) + { + Student g = session.CreateCriteria(typeof(Enrolment)) + .Add(Expression.And(Expression.Eq("StudentNumber", gavin.StudentNumber), Expression.Eq("CourseCode", courseCode))) + .CreateAlias("Student", "s", JoinType.InnerJoin) + .SetProjection(Projections.Entity("s", lazy:false)) + .UniqueResult(); + + Assert.That(NHibernateUtil.IsInitialized(g), Is.True, "object must be initialized"); + Assert.That(g, Is.EqualTo(gavin).Using((Student x, Student y) => x.StudentNumber == y.StudentNumber && x.Name == y.Name ? 0 : 1)); + } + } + + [Test] + public void UseEntityProjection_Lazy() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string courseCode); + + using (ISession session = Sfi.OpenSession()) + using (ITransaction tx = session.BeginTransaction()) + { + Student g = session.CreateCriteria(typeof(Enrolment)) + .Add(Expression.And(Expression.Eq("StudentNumber", gavin.StudentNumber), Expression.Eq("CourseCode", courseCode))) + .CreateAlias("Student", "s", JoinType.InnerJoin) + .SetProjection(Projections.Entity("s", lazy:true)) + .UniqueResult(); + + Assert.That(NHibernateUtil.IsInitialized(g), Is.False, "object must be lazy and not initialized"); + Assert.That(g, Is.EqualTo(gavin).Using((Student x, Student y) => x.StudentNumber == y.StudentNumber && x.Name == y.Name ? 0 : 1)); + } + } + + [Test] + public void UseMultipleEntityProjections() + { + //NH-3435 + PrepareDataForEntityProjectionTests(out Student gavin, out string courseCode); + + using (ISession session = Sfi.OpenSession()) + { + Enrolment en = null; + Student s = null; + Course c = null; + + var result = session.QueryOver(() => en) + .Where(e => e.StudentNumber == gavin.StudentNumber && e.CourseCode == courseCode) + .JoinAlias(e => e.Student, () => s) + .JoinAlias(e => e.Course, () => c) + .Select(Projections.RootEntity(lazy: true), Projections.Entity(() => s, lazy:false), Projections.Entity(() => c, lazy:false)) + .SingleOrDefault(); + + en = (Enrolment) result[0]; + s = (Student) result[1]; + c = (Course) result[2]; + + Assert.That(NHibernateUtil.IsInitialized(en), Is.False, "Object must be lazy and not initialized"); + Assert.That(NHibernateUtil.IsInitialized(s), Is.True, "Object must be initialized"); + Assert.That(NHibernateUtil.IsInitialized(c), Is.True, "Object must be initialized"); + } + } } } diff --git a/src/NHibernate.Test/NHSpecificTest/GH948/Entities.cs b/src/NHibernate.Test/NHSpecificTest/GH948/Entities.cs new file mode 100644 index 00000000000..20505f6e55d --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH948/Entities.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.GH948 +{ + public class EntitySimpleChild + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + } + + public class EntityComplex + { + public virtual Guid Id { get; set; } + + public virtual int Version { get; set; } + + public virtual string Name { get; set; } + + public virtual string LazyProp { get; set; } + + public virtual EntitySimpleChild Child1 { get; set; } + public virtual EntitySimpleChild Child2 { get; set; } + public virtual EntityComplex SameTypeChild { get; set; } + + public virtual IList ChildrenList { get; set; } + } + + public class CompositeKey + { + public int Id1 { get; set; } + public int Id2 { get; set; } + + public override bool Equals(object obj) + { + var key = obj as CompositeKey; + return key != null + && Id1 == key.Id1 + && Id2 == key.Id2; + } + + public override int GetHashCode() + { + var hashCode = -1596524975; + hashCode = hashCode * -1521134295 + Id1.GetHashCode(); + hashCode = hashCode * -1521134295 + Id2.GetHashCode(); + return hashCode; + } + } + + public class EntityWithCompositeId + { + public virtual CompositeKey Key { get; set; } + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH948/EntityProjection.cs b/src/NHibernate.Test/NHSpecificTest/GH948/EntityProjection.cs new file mode 100644 index 00000000000..e73fcf879e2 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH948/EntityProjection.cs @@ -0,0 +1,170 @@ +using System; +using NHibernate.Criterion; +using NHibernate.Engine; +using NHibernate.SqlCommand; +using NHibernate.Type; +using System.Linq; +using IQueryable = NHibernate.Persister.Entity.IQueryable; + +namespace NHibernate.Test.NHSpecificTest.GH948 +{ + using TThis = EntityProjection; + + [Serializable] + public class EntityProjection : IProjection + { + private string _entityAlias; + private string _columnAliasSuffix; + public string _tableAlias; + private IType[] _types; + + /// + /// Root entity projection + /// + public EntityProjection():this(null, null) + {} + + public EntityProjection(System.Type rootEntity, string entityAlias) + { + RootEntity = rootEntity; + _entityAlias = entityAlias; + } + + public bool FetchLazyProperties { get; set; } + public bool IsReadOnly { get; set; } + public bool Lazy { get; set; } + + internal string[] IdentifierColumnAliases { get; private set; } + internal string[][] PropertyColumnAliases { get; private set; } + internal IQueryable Persister { get; private set; } + internal System.Type RootEntity { get; private set; } + + #region Configuration methods + + public TThis SetLazy(bool lazy = true) + { + Lazy = lazy; + return this; + } + + public TThis SetAllPropertyFetch(bool fetchLazyProperties = true) + { + FetchLazyProperties = fetchLazyProperties; + return this; + } + + public TThis SetReadonly(bool isReadOnly = true) + { + IsReadOnly = isReadOnly; + return this; + } + + #endregion Configuration methods + + #region IProjection implementation + + public string[] Aliases + { + get; + private set; + } + + bool IProjection.IsAggregate + { + get { return false; } + } + + bool IProjection.IsGrouped + { + get { return false; } + } + + IType[] IProjection.GetTypes(string alias, ICriteria criteria, ICriteriaQuery criteriaQuery) + { + return new[] { criteriaQuery.GetType(criteria, alias) }; + } + + IType[] IProjection.GetTypes(ICriteria criteria, ICriteriaQuery criteriaQuery) + { + SetFields(criteria, criteriaQuery); + return _types; + } + + public string[] GetColumnAliases(int position, ICriteria criteria, ICriteriaQuery criteriaQuery) + { + SetFields(criteria, criteriaQuery); + return Aliases; + } + + public string[] GetColumnAliases(string alias, int position, ICriteria criteria, ICriteriaQuery criteriaQuery) + { + SetFields(criteria, criteriaQuery); + return Aliases; + } + + SqlString IProjection.ToSqlString(ICriteria criteria, int position, ICriteriaQuery criteriaQuery) + { + SetFields(criteria, criteriaQuery); + + string identifierSelectFragment = Persister.IdentifierSelectFragment(_tableAlias, _columnAliasSuffix); + if (Lazy) + return new SqlString(identifierSelectFragment); + + return new SqlString( + string.Concat( + identifierSelectFragment, + Persister.PropertySelectFragment(_tableAlias, _columnAliasSuffix, FetchLazyProperties))); + } + + SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery) + { + throw new NotImplementedException(); + } + + TypedValue[] IProjection.GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery) + { + throw new NotImplementedException(); + } + + #endregion IProjection implementation + + private void SetFields(ICriteria criteria, ICriteriaQuery criteriaQuery) + { + //Persister is required, so let's use it as "initialized marker" + if (Persister != null) + return; + + if (RootEntity == null) + { + RootEntity = criteria.GetRootEntityTypeIfAvailable(); + } + + if (_entityAlias == null) + { + _entityAlias = criteria.Alias; + } + + Persister = (IQueryable) criteriaQuery.Factory.GetEntityPersister(RootEntity.FullName); + + ICriteria subcriteria = criteria.GetCriteriaByAlias(_entityAlias); + if (subcriteria == null) + throw new HibernateException($"Criteria\\QueryOver alias '{_entityAlias}' for entity projection is not found."); + + _tableAlias = criteriaQuery.GetSQLAlias( + subcriteria, + Persister.IdentifierPropertyName ?? string.Empty); + + _columnAliasSuffix = criteriaQuery.GetIndexForAlias().ToString(); + + _types = new IType[] {new EntityProjectionType(this)}; + + IdentifierColumnAliases = Persister.GetIdentifierAliases(_columnAliasSuffix); + + PropertyColumnAliases = Lazy + ? new string[][] { } + : Enumerable.Range(0, Persister.PropertyNames.Length).Select(i => Persister.GetPropertyAliases(_columnAliasSuffix, i)).ToArray(); + + Aliases = IdentifierColumnAliases.Concat(PropertyColumnAliases.SelectMany(alias => alias)).ToArray(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH948/EntityProjectionType.cs b/src/NHibernate.Test/NHSpecificTest/GH948/EntityProjectionType.cs new file mode 100644 index 00000000000..e77065c3d76 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH948/EntityProjectionType.cs @@ -0,0 +1,98 @@ +using System; +using System.Data.Common; +using NHibernate.Engine; +using NHibernate.Event; +using NHibernate.Persister.Entity; +using NHibernate.Type; + +namespace NHibernate.Test.NHSpecificTest.GH948 +{ + /// + /// Specialized type for retrieving entity from Criteria/QueryOver API projection. + /// Intended to be used only inside + /// + [Serializable] + internal partial class EntityProjectionType : ManyToOneType, IType + { + private readonly EntityProjection _projection; + + public EntityProjectionType(EntityProjection projection) : base(projection.RootEntity.FullName, projection.Lazy) + { + _projection = projection; + } + + object IType.NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner) + { + //names parameter is ignored (taken from projection) + return NullSafeGet(rs, string.Empty, session, owner); + } + + public override object NullSafeGet(DbDataReader rs, string name, ISessionImplementor session, object owner) + { + var identifier = _projection.Persister.IdentifierType.NullSafeGet(rs, _projection.IdentifierColumnAliases, session, null); + + if (identifier == null) + { + return null; + } + + return _projection.Lazy + ? ResolveIdentifier(identifier, session) + : GetInitializedEntityFromProjection(rs, session, identifier); + } + + private object GetInitializedEntityFromProjection(DbDataReader rs, ISessionImplementor session, object identifier) + { + var entity = CreateInitializedEntity( + rs, + session, + _projection.Persister, + identifier, + _projection.PropertyColumnAliases, + LockMode.None, + _projection.FetchLazyProperties, + _projection.IsReadOnly); + + return entity; + } + + private static object CreateInitializedEntity(DbDataReader rs, ISessionImplementor session, IQueryable persister, object identifier, string[][] propertyAliases, LockMode lockMode, bool fetchLazyProperties, bool readOnly) + { + var eventSource = session as IEventSource; + PostLoadEvent postLoadEvent = null; + PreLoadEvent preLoadEvent = null; + object entity; + if (eventSource != null) + { + preLoadEvent = new PreLoadEvent(eventSource); + postLoadEvent = new PostLoadEvent(eventSource); + entity = eventSource.Instantiate(persister, identifier); + } + else + { + entity = session.Instantiate(persister.EntityName, identifier); + } + + TwoPhaseLoad.AddUninitializedEntity( + session.GenerateEntityKey(identifier, persister), + entity, + persister, + lockMode, + !fetchLazyProperties, + session); + + var hydrated = persister.Hydrate( + rs, + identifier, + entity, + persister, + propertyAliases, + fetchLazyProperties, + session); + + TwoPhaseLoad.PostHydrate(persister, identifier, hydrated, null, entity, lockMode, !fetchLazyProperties, session); + TwoPhaseLoad.InitializeEntity(entity, readOnly, session, preLoadEvent, postLoadEvent); + return entity; + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH948/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/GH948/FixtureByCode.cs new file mode 100644 index 00000000000..49e35612018 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH948/FixtureByCode.cs @@ -0,0 +1,343 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH948 +{ + [TestFixture] + public class ByCodeFixture : TestCaseMappingByCode + { + private EntityWithCompositeId _entityWithCompositeId; + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + + rc.Version(ep => ep.Version, vm => { }); + + rc.Property(x => x.Name); + + rc.Property(ep => ep.LazyProp, m => m.Lazy(true)); + + rc.ManyToOne(ep => ep.Child1, m => m.Column("Child1Id")); + rc.ManyToOne(ep => ep.Child2, m => m.Column("Child2Id")); + rc.ManyToOne(ep => ep.SameTypeChild, m => m.Column("SameTypeChildId")); + + rc.Bag(ep => ep.ChildrenList, m => + { + m.Cascade(Cascade.All); + m.Inverse(true); + }, a => a.OneToMany()); + + }); + + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + + mapper.Class( + rc => + { + rc.ComponentAsId( + e => e.Key, + ekm => + { + ekm.Property(ek => ek.Id1); + ekm.Property(ek => ek.Id2); + }); + + rc.Property(e => e.Name); + }); + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnTearDown() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnSetUp() + { + CreateObjects(); + } + + [Test] + public void RootEntityProjectionFullyInitializedAndWithUnfetchedLazyPropertiesByDefault() + { + using(var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + Sfi.Statistics.Clear(); + EntityComplex entitypRoot = session + .QueryOver() + .Where(ec => ec.LazyProp != null) + .Select(Projections.RootEntity()) + .Take(1).SingleOrDefault(); + + Assert.That(NHibernateUtil.IsInitialized(entitypRoot),Is.True, "Object must be initialized by default"); + Assert.That(session.IsReadOnly(entitypRoot),Is.False, "Object must not be readonly by default"); + Assert.That(NHibernateUtil.IsPropertyInitialized(entitypRoot, nameof(entitypRoot.LazyProp)), Is.False, "Lazy properties should not be initialized by default."); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void RootEntityProjectionLazy() + { + using (var session = OpenSession()) + { + EntityComplex entitypRoot = session + .QueryOver() + .Select(Projections.RootEntity().SetLazy(true)) + .Take(1).SingleOrDefault(); + + + Assert.That(NHibernateUtil.IsInitialized(entitypRoot),Is.False, "Object must be lazy loaded."); + } + } + + [Test] + public void AliasedEntityProjection() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntitySimpleChild child1 = null; + child1 = session + .QueryOver() + .JoinAlias(ep => ep.Child1, () => child1) + .Select(Projections.Entity(() => child1)) + .Take(1).SingleOrDefault(); + + Assert.That(child1, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(child1), Is.True, "Object must be initialized"); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void MultipleLazyEntityProjections() + { + using (var session = OpenSession()) + { + EntitySimpleChild child1 = null; + EntityComplex root = null; + EntityComplex sameTypeChild = null; + EntitySimpleChild child2 = null; + + var result = session + .QueryOver(() => root) + .JoinAlias(ep => ep.SameTypeChild, () => sameTypeChild) + .JoinAlias(ep => ep.Child1, () => child1) + .JoinAlias(ep => ep.Child2, () => child2) + .Select( + Projections.RootEntity().SetLazy(true), + Projections.Entity(() => child1).SetLazy(true), + Projections.Entity(() => sameTypeChild).SetLazy(true), + Projections.Entity(() => child2).SetLazy(true) + ) + .Take(1).SingleOrDefault(); + + root = (EntityComplex) result[0]; + child1 = (EntitySimpleChild) result[1]; + sameTypeChild = (EntityComplex) result[2]; + child2 = (EntitySimpleChild) result[3]; + + Assert.That(NHibernateUtil.IsInitialized(root), Is.False, "Object must be lazy loaded."); + Assert.That(NHibernateUtil.IsInitialized(sameTypeChild), Is.False, "Object must be lazy loaded."); + Assert.That(NHibernateUtil.IsInitialized(child1), Is.False, "Object must be lazy loaded."); + Assert.That(NHibernateUtil.IsInitialized(child2), Is.False, "Object must be lazy loaded."); + + //make sure objects are populated from different aliases for the same types + Assert.That(root.Id , Is.Not.EqualTo(sameTypeChild.Id), "Different objects are expected."); + Assert.That(child1.Id , Is.Not.EqualTo(child2.Id), "Different objects are expected."); + + } + } + + [Test] + public void EntityProjectionWithLazyPropertiesFetched() + { + using (var session = OpenSession()) + { + EntityComplex entitypRoot = session + .QueryOver() + .Where(ec => ec.LazyProp != null) + .Select(Projections.RootEntity().SetAllPropertyFetch(true)) + .Take(1).SingleOrDefault(); + + Assert.That(entitypRoot, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(entitypRoot), Is.True, "Object must be initialized"); + Assert.That(NHibernateUtil.IsPropertyInitialized(entitypRoot, nameof(entitypRoot.LazyProp)), Is.True, "Lazy property must be initialized"); + } + } + + [Test] + public void NullEntityProjection() + { + using (var session = OpenSession()) + { + EntitySimpleChild child1 = null; + child1 = session + .QueryOver() + .JoinAlias(ep => ep.Child1, () => child1, JoinType.LeftOuterJoin) + .Where(() => child1.Id == null) + .Select(Projections.Entity(() => child1)) + .Take(1).SingleOrDefault(); + + Assert.That(child1, Is.Null); + } + } + + [Test] + public void MultipleEntitiesProjections() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex root = null; + EntitySimpleChild child1 = null; + EntitySimpleChild child2 = null; + EntityComplex sameAsRootChild = null; + EntitySimpleChild nullListElem = null; + var objects = session + .QueryOver() + .JoinAlias(ep => ep.Child1, () => child1) + .JoinAlias(ep => ep.Child2, () => child2) + .JoinAlias(ep => ep.SameTypeChild, () => sameAsRootChild) + .JoinAlias(ep => ep.ChildrenList, () => nullListElem, JoinType.LeftOuterJoin) + .Select( + Projections.RootEntity(), + Projections.Entity(() => child1), + Projections.Entity(() => child2), + Projections.Entity(() => sameAsRootChild), + Projections.Entity(() => nullListElem) + ) + .Take(1).SingleOrDefault(); + + root = (EntityComplex) objects[0]; + child1 = (EntitySimpleChild) objects[1]; + child2 = (EntitySimpleChild) objects[2]; + sameAsRootChild = (EntityComplex) objects[3]; + nullListElem = (EntitySimpleChild) objects[4]; + + Assert.That(NHibernateUtil.IsInitialized(root), Is.True, "Object must be initialized"); + Assert.That(NHibernateUtil.IsInitialized(child1), Is.True, "Object must be initialized"); + Assert.That(NHibernateUtil.IsInitialized(child2), Is.True, "Object must be initialized"); + Assert.That(NHibernateUtil.IsInitialized(sameAsRootChild), Is.True, "Object must be initialized"); + Assert.That(nullListElem, Is.Null, "Object must be initialized"); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void ReadOnlyProjection() + { + using (var session = OpenSession()) + { + EntityComplex entitypRoot = session + .QueryOver() + .Select(Projections.RootEntity().SetReadonly(true)) + .Take(1).SingleOrDefault(); + + Assert.That(session.IsReadOnly(entitypRoot), Is.True, "Object must be loaded readonly."); + } + } + + [Test] + public void EntityProjectionForCompositeKeyInitialized() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var composite = session + .QueryOver() + .Select(Projections.RootEntity()) + .Take(1).SingleOrDefault(); + + + Assert.That(composite, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(composite), Is.True, "Object must be initialized"); + Assert.That(composite, Is.EqualTo(_entityWithCompositeId).Using((EntityWithCompositeId x, EntityWithCompositeId y) => (Equals(x.Key, y.Key) && Equals(x.Name, y.Name)) ? 0 : 1)); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + + } + + [Test] + public void EntityProjectionForCompositeKeyLazy() + { + using (var session = OpenSession()) + { + var composite = session + .QueryOver() + .Select(Projections.RootEntity().SetLazy(true)) + .Take(1).SingleOrDefault(); + + + Assert.That(composite, Is.Not.Null); + Assert.That(composite, Is.EqualTo(_entityWithCompositeId).Using((EntityWithCompositeId x, EntityWithCompositeId y) => (Equals(x.Key, y.Key)) ? 0 : 1)); + Assert.That(NHibernateUtil.IsInitialized(composite), Is.False, "Object must be lazy loaded."); + } + } + + private void CreateObjects() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var child1 = new EntitySimpleChild + { + Name = "Child1" + }; + var child2 = new EntitySimpleChild + { + Name = "Child1" + }; + + + var parent = new EntityComplex + { + Name = "ComplexEnityParent", + Child1 = child1, + Child2 = child2, + LazyProp = "SomeBigValue", + SameTypeChild = new EntityComplex() { Name = "ComplexEntityChild" } + }; + + _entityWithCompositeId = new EntityWithCompositeId + { + Key = new CompositeKey + { + Id1 = 1, + Id2 = 2 + }, + Name = "Composite" + }; + + session.Save(child1); + session.Save(child2); + session.Save(parent.SameTypeChild); + session.Save(parent); + session.Save(_entityWithCompositeId); + + session.Flush(); + transaction.Commit(); + } + } + + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH948/Projections.cs b/src/NHibernate.Test/NHSpecificTest/GH948/Projections.cs new file mode 100644 index 00000000000..0a74aafd684 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH948/Projections.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq.Expressions; +using NHibernate.Criterion; +using NHibernate.Impl; + +namespace NHibernate.Test.NHSpecificTest.GH948 +{ + public static class Projections + { + /// + /// Returns the root entity. + /// + /// The root entity. + public static EntityProjection RootEntity() + { + return new EntityProjection(); + } + + /// + /// Returns an aliased entity. + /// + /// The type of the entity. + /// The alias of the entity. + /// A projection of the entity. + public static EntityProjection Entity(System.Type type, string alias) + { + return new EntityProjection(type, alias); + } + + /// + /// Returns an aliased entity. + /// + /// /// The type of the entity. + /// The alias of the entity. + /// A projection of the entity. + public static EntityProjection Entity(string alias) + { + return Entity(typeof(T), alias); + } + + /// + /// Returns an aliased entity. + /// + /// /// The type of the entity. + /// The alias of the entity. + /// A projection of the entity. + public static EntityProjection Entity(Expression> alias) + { + return Entity(typeof(T), ExpressionProcessor.FindMemberExpression(alias.Body)); + } + } +} diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index 6ad305596f3..f1323793552 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -6,6 +6,8 @@ net461 true $(NoWarn);3001;3002;3003;3005 + false + false True @@ -19,16 +21,43 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + Always diff --git a/src/NHibernate/Criterion/BaseEntityProjection.cs b/src/NHibernate/Criterion/BaseEntityProjection.cs new file mode 100644 index 00000000000..9e2c60e92c5 --- /dev/null +++ b/src/NHibernate/Criterion/BaseEntityProjection.cs @@ -0,0 +1,123 @@ +using System; +using System.Linq; +using NHibernate.Engine; +using NHibernate.Persister.Entity; +using NHibernate.SqlCommand; +using NHibernate.Type; + +namespace NHibernate.Criterion +{ + [Serializable] + public abstract class BaseEntityProjection : IProjection + { + private string alias = null; + private string[] columnAliases = null; + private bool lazy = true; + + protected BaseEntityProjection(System.Type rootEntity, string alias) + { + this.RootEntity = rootEntity; + this.alias = alias; + } + + protected System.Type RootEntity + { + get; + private set; + } + + public BaseEntityProjection SetLazy(bool lazy) + { + this.lazy = lazy; + + return this; + } + + public string[] Aliases + { + get + { + return this.columnAliases; + } + } + + TypedValue[] IProjection.GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery) + { + throw new NotImplementedException(); + } + + IType[] IProjection.GetTypes(string alias, ICriteria criteria, ICriteriaQuery criteriaQuery) + { + return new IType[] { criteriaQuery.GetType(criteria, alias) }; + } + + private void SetFields(ICriteria criteria) + { + if (this.RootEntity == null) + { + this.RootEntity = criteria.GetRootEntityTypeIfAvailable(); + } + + if (this.alias == null) + { + this.alias = criteria.Alias; + } + } + + IType[] IProjection.GetTypes(ICriteria criteria, ICriteriaQuery criteriaQuery) + { + this.SetFields(criteria); + + return new IType[] { new ManyToOneType(this.RootEntity.FullName, this.lazy) }; + } + + bool IProjection.IsAggregate + { + get { return false; } + } + + bool IProjection.IsGrouped + { + get { return false; } + } + + SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery) + { + throw new NotImplementedException(); + } + + SqlString IProjection.ToSqlString(ICriteria criteria, int position, ICriteriaQuery criteriaQuery) + { + this.SetFields(criteria); + + SqlStringBuilder builder = new SqlStringBuilder(); + var persister = (ILoadable)criteriaQuery.Factory.GetEntityPersister(this.RootEntity.FullName); + ICriteria subcriteria = criteria.GetCriteriaByAlias(this.alias); + + this.columnAliases = persister.GetIdentifierAliases(string.Empty); + var columnNamesWithAliases = persister.IdentifierColumnNames + .Select( + (x, i) => string.Concat( + criteriaQuery.GetSQLAlias( + subcriteria, persister.IdentifierPropertyName ?? string.Empty), + ".", + criteriaQuery.Factory.Dialect.QuoteForColumnName(x), + " as ", + columnAliases[i] + )); + + builder.Add(string.Join(", ", columnNamesWithAliases)); + return builder.ToSqlString(); + } + + public string[] GetColumnAliases(int position, ICriteria criteria, ICriteriaQuery criteriaQuery) + { + return Aliases; + } + + public string[] GetColumnAliases(string alias, int position, ICriteria criteria, ICriteriaQuery criteriaQuery) + { + return Aliases; + } + } +} diff --git a/src/NHibernate/Criterion/EntityProjection.cs b/src/NHibernate/Criterion/EntityProjection.cs new file mode 100644 index 00000000000..7cd133ddd2a --- /dev/null +++ b/src/NHibernate/Criterion/EntityProjection.cs @@ -0,0 +1,12 @@ +using System; + +namespace NHibernate.Criterion +{ + [Serializable] + public class EntityProjection : BaseEntityProjection + { + public EntityProjection(System.Type rootEntity, String alias) : base(rootEntity, alias) + { + } + } +} diff --git a/src/NHibernate/Criterion/Projections.cs b/src/NHibernate/Criterion/Projections.cs index 88f750de722..24cff1fd3b1 100644 --- a/src/NHibernate/Criterion/Projections.cs +++ b/src/NHibernate/Criterion/Projections.cs @@ -16,6 +16,51 @@ namespace NHibernate.Criterion /// public static class Projections { + /// + /// Return the root entity. + /// + /// + /// The root entity. + public static IProjection RootEntity(bool lazy = true) + { + return new RootEntityProjection().SetLazy(lazy); + } + + /// + /// Return an aliased entity. + /// + /// The type of the entity. + /// The alias of the entity. + /// + /// A projection of the entity. + public static IProjection Entity(System.Type type, string alias, bool lazy = true) + { + return new EntityProjection(type, alias).SetLazy(lazy); + } + + /// + /// Return an aliased entity. + /// + /// /// The type of the entity. + /// The alias of the entity. + /// + /// A projection of the entity. + public static IProjection Entity(string alias, bool lazy = true) + { + return Entity(typeof(T), alias, lazy); + } + /// + /// Return an aliased entity. + /// + /// /// The type of the entity. + /// The alias of the entity. + /// + /// A projection of the entity. + public static IProjection Entity(Expression> alias, bool lazy = true) + { + return Entity(typeof(T), ExpressionProcessor.FindMemberExpression(alias.Body), lazy); + } + /// /// Create a distinct projection from a projection /// diff --git a/src/NHibernate/Criterion/RootEntityProjection.cs b/src/NHibernate/Criterion/RootEntityProjection.cs new file mode 100644 index 00000000000..ff839cf63cf --- /dev/null +++ b/src/NHibernate/Criterion/RootEntityProjection.cs @@ -0,0 +1,12 @@ +using System; + +namespace NHibernate.Criterion +{ + [Serializable] + public class RootEntityProjection : BaseEntityProjection + { + public RootEntityProjection() : base(null, null) + { + } + } +} \ No newline at end of file