diff --git a/src/NHibernate.Test/Async/Criteria/EntityJoinCriteriaTest.cs b/src/NHibernate.Test/Async/Criteria/EntityJoinCriteriaTest.cs new file mode 100644 index 00000000000..bce97afd549 --- /dev/null +++ b/src/NHibernate.Test/Async/Criteria/EntityJoinCriteriaTest.cs @@ -0,0 +1,546 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Criterion; +using NHibernate.Mapping.ByCode; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.Criteria +{ + using System.Threading.Tasks; + /// + /// Tests for explicit entity joins (not associated entities) + /// + [TestFixture] + public class EntityJoinCriteriaTestAsync : TestCaseMappingByCode + { + private const string customEntityName = "CustomEntityName"; + private EntityWithCompositeId _entityWithCompositeId; + private EntityWithNoAssociation _noAssociation; + private EntityCustomEntityName _entityWithCustomEntityName; + + //check JoinEntityAlias - JoinAlias analog for entity join + [Test] + public async Task CanJoinNotAssociatedEntityAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex entityComplex = null; + EntityWithNoAssociation root = null; + root = await (session.QueryOver(() => root) + .JoinEntityAlias(() => entityComplex, Restrictions.Where(() => root.Complex1Id == entityComplex.Id)).Take(1) + .SingleOrDefaultAsync()); + entityComplex = await (session.LoadAsync(root.Complex1Id)); + + Assert.That(NHibernateUtil.IsInitialized(entityComplex), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + //check JoinEntityAlias - JoinAlias analog for entity join + [Test] + public async Task CanJoinNotAssociatedEntity_ExpressionAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex entityComplex = null; + EntityWithNoAssociation root = null; + root = await (session.QueryOver(() => root) + .JoinEntityAlias(() => entityComplex, () => root.Complex1Id == entityComplex.Id).Take(1) + .SingleOrDefaultAsync()); + entityComplex = await (session.LoadAsync(root.Complex1Id)); + + Assert.That(NHibernateUtil.IsInitialized(entityComplex), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + //check JoinEntityQueryOver - JoinQueryOver analog for entity join + [Test] + public async Task CanJoinEntityQueryOverAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejQueryOver = null; + EntityWithNoAssociation root = null; + root = await (session.QueryOver(() => root) + .JoinEntityQueryOver(() => ejQueryOver, Restrictions.Where(() => root.Complex1Id == ejQueryOver.Id)) + .Take(1) + .SingleOrDefaultAsync()); + ejQueryOver = await (session.LoadAsync(root.Complex1Id)); + + Assert.That(NHibernateUtil.IsInitialized(ejQueryOver), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + //check JoinEntityQueryOver - JoinQueryOver analog for entity join + [Test] + public async Task CanJoinEntityQueryOver_ExpressionAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejQueryOver = null; + EntityWithNoAssociation root = null; + root = await (session.QueryOver(() => root) + .JoinEntityQueryOver(() => ejQueryOver, () => root.Complex1Id == ejQueryOver.Id) + .Take(1) + .SingleOrDefaultAsync()); + ejQueryOver = await (session.LoadAsync(root.Complex1Id)); + + Assert.That(NHibernateUtil.IsInitialized(ejQueryOver), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + //can join not associated entity and join associated entities for it via JoinQueryOver + [Test] + public async Task CanQueryOverForAssociationInNotAssociatedEntityAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejComplex = null; + EntityWithNoAssociation root = null; + root = await (session.QueryOver(() => root) + .JoinEntityQueryOver(() => ejComplex, () => root.Complex1Id == ejComplex.Id) + .JoinQueryOver(ej => ej.Child1) + .Take(1) + .SingleOrDefaultAsync()); + + ejComplex = await (session.LoadAsync(root.Complex1Id)); + + Assert.That(NHibernateUtil.IsInitialized(ejComplex), Is.True); + Assert.That(NHibernateUtil.IsInitialized(ejComplex.Child1), Is.True); + Assert.That(NHibernateUtil.IsInitialized(ejComplex.Child2), Is.False); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task SimpleProjectionForEntityJoinAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejComplex = null; + EntityWithNoAssociation root = null; + var name = await (session.QueryOver(() => root) + .JoinEntityQueryOver(() => ejComplex, () => root.Complex1Id == ejComplex.Id) + .Select((e) => ejComplex.Name) + .Take(1) + .SingleOrDefaultAsync()); + + Assert.That(name, Is.Not.Empty); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task EntityProjectionForEntityJoinAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntitySimpleChild ejChild1 = null; + + EntityComplex root = null; + EntityComplex st = null; + var r = await (session + .QueryOver(() => root) + .JoinAlias(c => c.SameTypeChild, () => st) + .JoinEntityAlias(() => ejChild1, () => ejChild1.Id == root.Child1.Id) + .Select( + Projections.RootEntity(), + Projections.Entity(() => st), + Projections.Entity(() => ejChild1) + ) + .SingleOrDefaultAsync()); + var rootObj = (EntityComplex) r[0]; + var mappedObj = (EntityComplex) r[1]; + var entityJoinObj = (EntitySimpleChild) r[2]; + + Assert.That(rootObj, Is.Not.Null); + Assert.That(mappedObj, Is.Not.Null); + Assert.That(entityJoinObj, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(rootObj), Is.True); + Assert.That(NHibernateUtil.IsInitialized(mappedObj), Is.True); + Assert.That(NHibernateUtil.IsInitialized(entityJoinObj), Is.True); + } + } + + //just check that it can be executed without error + [Test] + public async Task MixOfJoinsForAssociatedAndNotAssociatedEntitiesAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + + EntityComplex root = null; + EntityComplex ejLevel1 = null; + EntitySimpleChild customChildForEjLevel1 = null; + EntityComplex entityComplexForEjLevel1 = null; + EntitySimpleChild ejLevel2OnEntityComplexForEjLevel1 = null; + var obj = await (session + .QueryOver(() => root) + .JoinEntityAlias(() => ejLevel1, Restrictions.Where(() => ejLevel1.Id == root.SameTypeChild.Id && root.Id != null), JoinType.LeftOuterJoin) + .JoinAlias(() => ejLevel1.Child1, () => customChildForEjLevel1, JoinType.InnerJoin) + .JoinAlias(() => ejLevel1.SameTypeChild, () => entityComplexForEjLevel1, JoinType.LeftOuterJoin) + .JoinEntityAlias(() => ejLevel2OnEntityComplexForEjLevel1, () => entityComplexForEjLevel1.Id == ejLevel2OnEntityComplexForEjLevel1.Id) + .Where(() => customChildForEjLevel1.Id != null && ejLevel2OnEntityComplexForEjLevel1.Id != null) + .Take(1) + .SingleOrDefaultAsync()); + } + } + + [Test] + public async Task EntityJoinForCompositeKeyAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityWithCompositeId ejComposite = null; + EntityWithNoAssociation root = null; + root = await (session + .QueryOver(() => root) + .JoinEntityAlias(() => ejComposite, () => root.Composite1Key1 == ejComposite.Key.Id1 && root.Composite1Key2 == ejComposite.Key.Id2) + .Take(1).SingleOrDefaultAsync()); + var composite = await (session.LoadAsync(_entityWithCompositeId.Key)); + + 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 async Task NullLeftEntityJoinAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejLeftNull = null; + EntityWithNoAssociation root = null; + root = await (session.QueryOver(() => root) + //add some non existent join condition + .JoinEntityAlias(() => ejLeftNull, () => ejLeftNull.Id == null, JoinType.LeftOuterJoin) + .Take(1) + .SingleOrDefaultAsync()); + + Assert.That(root, Is.Not.Null, "root should not be null (looks like left join didn't work)"); + Assert.That(NHibernateUtil.IsInitialized(root), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task NullLeftEntityJoinWithEntityProjectionAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejLeftNull = null; + EntityWithNoAssociation root = null; + var objs = await (session.QueryOver(() => root) + //add some non existent join condition + .JoinEntityAlias(() => ejLeftNull, () => ejLeftNull.Id == null, JoinType.LeftOuterJoin) + .Select((e) => root.AsEntity(), e => ejLeftNull.AsEntity()) + .Take(1) + .SingleOrDefaultAsync()); + root = (EntityWithNoAssociation) objs[0]; + ejLeftNull = (EntityComplex) objs[1]; + + Assert.That(root, Is.Not.Null, "root should not be null (looks like left join didn't work)"); + Assert.That(NHibernateUtil.IsInitialized(root), Is.True); + Assert.That(ejLeftNull, Is.Null, "Entity join should be null"); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task EntityJoinForCustomEntityNameAsync() + { + + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityCustomEntityName ejCustomEntity= null; + EntityWithNoAssociation root = null; + root = await (session.QueryOver(() => root) + .JoinEntityAlias(() => ejCustomEntity, Restrictions.Where(() => ejCustomEntity.Id == root.CustomEntityNameId), JoinType.InnerJoin, customEntityName) + .Take(1) + .SingleOrDefaultAsync()); + + ejCustomEntity = (EntityCustomEntityName) await (session.LoadAsync(customEntityName, root.CustomEntityNameId)); + + Assert.That(NHibernateUtil.IsInitialized(ejCustomEntity), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task EntityJoinForCustomEntityName_ExpressionAsync() + { + + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityCustomEntityName ejCustomEntity= null; + EntityWithNoAssociation root = null; + root = await (session.QueryOver(() => root) + .JoinEntityAlias(() => ejCustomEntity, () => ejCustomEntity.Id == root.CustomEntityNameId, JoinType.InnerJoin, customEntityName) + .Take(1) + .SingleOrDefaultAsync()); + + ejCustomEntity = (EntityCustomEntityName) await (session.LoadAsync(customEntityName, root.CustomEntityNameId)); + + Assert.That(NHibernateUtil.IsInitialized(ejCustomEntity), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task EntityJoinFoSubquery_JoinEntityAliasAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ej = null; + EntityWithNoAssociation root = null; + + EntityComplex ejSub = null; + EntityWithNoAssociation rootSub = null; + + var subquery = QueryOver.Of(() => rootSub) + .JoinEntityAlias(() => ejSub, () => rootSub.Complex1Id == ejSub.Id) + .Where(r => ejSub.Name == ej.Name) + .Select(x => ejSub.Id); + + root = await (session.QueryOver(() => root) + .JoinEntityAlias(() => ej, () => root.Complex1Id == ej.Id) + .WithSubquery.WhereExists(subquery) + .SingleOrDefaultAsync()); + ej = await (session.LoadAsync(root.Complex1Id)); + + Assert.That(NHibernateUtil.IsInitialized(ej), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task EntityJoinFoSubquery_JoinQueryOverAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ej = null; + EntityWithNoAssociation root = null; + + EntityComplex ejSub = null; + EntityWithNoAssociation rootSub = null; + + var subquery = QueryOver.Of(() => rootSub) + .JoinEntityQueryOver(() => ejSub, () => rootSub.Complex1Id == ejSub.Id) + .Where(x => x.Name == ej.Name) + .Select(x => ejSub.Id); + + root = await (session.QueryOver(() => root) + .JoinEntityAlias(() => ej, () => root.Complex1Id == ej.Id) + .WithSubquery.WhereExists(subquery) + .SingleOrDefaultAsync()); + ej = await (session.LoadAsync(root.Complex1Id)); + + Assert.That(NHibernateUtil.IsInitialized(ej), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + #region Test Setup + + 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(Mapping.ByCode.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); + }); + + mapper.Class( + rc => + { + rc.ComponentAsId( + e => e.Key, + ekm => + { + ekm.Property(ek => ek.Id1); + ekm.Property(ek => ek.Id2); + }); + + rc.Property(e => e.Name); + }); + + mapper.Class( + rc => + { + rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + + rc.Property(e => e.Complex1Id); + rc.Property(e => e.Complex2Id); + rc.Property(e => e.Simple1Id); + rc.Property(e => e.Simple2Id); + rc.Property(e => e.Composite1Key1); + rc.Property(e => e.Composite1Key2); + rc.Property(e => e.CustomEntityNameId); + + }); + + mapper.Class( + rc => + { + rc.EntityName(customEntityName); + + rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + 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() + { + 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); + + _entityWithCustomEntityName = new EntityCustomEntityName() + { + Name = "EntityCustomEntityName" + }; + + session.Save(customEntityName, _entityWithCustomEntityName); + + _noAssociation = new EntityWithNoAssociation() + { + Complex1Id = parent.Id, + Complex2Id = parent.SameTypeChild.Id, + Composite1Key1 = _entityWithCompositeId.Key.Id1, + Composite1Key2 = _entityWithCompositeId.Key.Id2, + Simple1Id = child1.Id, + Simple2Id = child2.Id, + CustomEntityNameId = _entityWithCustomEntityName.Id + }; + + session.Save(_noAssociation); + + session.Flush(); + transaction.Commit(); + } + } + + #endregion Test Setup + } +} diff --git a/src/NHibernate.Test/Criteria/EntityJoinCriteriaTest.cs b/src/NHibernate.Test/Criteria/EntityJoinCriteriaTest.cs new file mode 100644 index 00000000000..aab9ea3fa94 --- /dev/null +++ b/src/NHibernate.Test/Criteria/EntityJoinCriteriaTest.cs @@ -0,0 +1,548 @@ +using System; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Criterion; +using NHibernate.Mapping.ByCode; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.Criteria +{ + /// + /// Tests for explicit entity joins (not associated entities) + /// + [TestFixture] + public class EntityJoinCriteriaTest : TestCaseMappingByCode + { + private const string customEntityName = "CustomEntityName"; + private EntityWithCompositeId _entityWithCompositeId; + private EntityWithNoAssociation _noAssociation; + private EntityCustomEntityName _entityWithCustomEntityName; + + //check JoinEntityAlias - JoinAlias analog for entity join + [Test] + public void CanJoinNotAssociatedEntity() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex entityComplex = null; + EntityWithNoAssociation root = null; + root = session.QueryOver(() => root) + .JoinEntityAlias(() => entityComplex, Restrictions.Where(() => root.Complex1Id == entityComplex.Id)).Take(1) + .SingleOrDefault(); + entityComplex = session.Load(root.Complex1Id); + + Assert.That(NHibernateUtil.IsInitialized(entityComplex), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + //check JoinEntityAlias - JoinAlias analog for entity join + [Test] + public void CanJoinNotAssociatedEntity_Expression() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex entityComplex = null; + EntityWithNoAssociation root = null; + root = session.QueryOver(() => root) + .JoinEntityAlias(() => entityComplex, () => root.Complex1Id == entityComplex.Id).Take(1) + .SingleOrDefault(); + entityComplex = session.Load(root.Complex1Id); + + Assert.That(NHibernateUtil.IsInitialized(entityComplex), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + //check JoinEntityQueryOver - JoinQueryOver analog for entity join + [Test] + public void CanJoinEntityQueryOver() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejQueryOver = null; + EntityWithNoAssociation root = null; + root = session.QueryOver(() => root) + .JoinEntityQueryOver(() => ejQueryOver, Restrictions.Where(() => root.Complex1Id == ejQueryOver.Id)) + .Take(1) + .SingleOrDefault(); + ejQueryOver = session.Load(root.Complex1Id); + + Assert.That(NHibernateUtil.IsInitialized(ejQueryOver), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + //check JoinEntityQueryOver - JoinQueryOver analog for entity join + [Test] + public void CanJoinEntityQueryOver_Expression() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejQueryOver = null; + EntityWithNoAssociation root = null; + root = session.QueryOver(() => root) + .JoinEntityQueryOver(() => ejQueryOver, () => root.Complex1Id == ejQueryOver.Id) + .Take(1) + .SingleOrDefault(); + ejQueryOver = session.Load(root.Complex1Id); + + Assert.That(NHibernateUtil.IsInitialized(ejQueryOver), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + //can join not associated entity and join associated entities for it via JoinQueryOver + [Test] + public void CanQueryOverForAssociationInNotAssociatedEntity() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejComplex = null; + EntityWithNoAssociation root = null; + root = session.QueryOver(() => root) + .JoinEntityQueryOver(() => ejComplex, () => root.Complex1Id == ejComplex.Id) + .JoinQueryOver(ej => ej.Child1) + .Take(1) + .SingleOrDefault(); + + ejComplex = session.Load(root.Complex1Id); + + Assert.That(NHibernateUtil.IsInitialized(ejComplex), Is.True); + Assert.That(NHibernateUtil.IsInitialized(ejComplex.Child1), Is.True); + Assert.That(NHibernateUtil.IsInitialized(ejComplex.Child2), Is.False); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void SimpleProjectionForEntityJoin() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejComplex = null; + EntityWithNoAssociation root = null; + var name = session.QueryOver(() => root) + .JoinEntityQueryOver(() => ejComplex, () => root.Complex1Id == ejComplex.Id) + .Select((e) => ejComplex.Name) + .Take(1) + .SingleOrDefault(); + + Assert.That(name, Is.Not.Empty); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void EntityProjectionForEntityJoin() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntitySimpleChild ejChild1 = null; + + EntityComplex root = null; + EntityComplex st = null; + var r = session + .QueryOver(() => root) + .JoinAlias(c => c.SameTypeChild, () => st) + .JoinEntityAlias(() => ejChild1, () => ejChild1.Id == root.Child1.Id) + .Select( + Projections.RootEntity(), + Projections.Entity(() => st), + Projections.Entity(() => ejChild1) + ) + .SingleOrDefault(); + var rootObj = (EntityComplex) r[0]; + var mappedObj = (EntityComplex) r[1]; + var entityJoinObj = (EntitySimpleChild) r[2]; + + Assert.That(rootObj, Is.Not.Null); + Assert.That(mappedObj, Is.Not.Null); + Assert.That(entityJoinObj, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(rootObj), Is.True); + Assert.That(NHibernateUtil.IsInitialized(mappedObj), Is.True); + Assert.That(NHibernateUtil.IsInitialized(entityJoinObj), Is.True); + } + } + + //just check that it can be executed without error + [Test] + public void MixOfJoinsForAssociatedAndNotAssociatedEntities() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + + EntityComplex root = null; + EntityComplex ejLevel1 = null; + EntitySimpleChild customChildForEjLevel1 = null; + EntityComplex entityComplexForEjLevel1 = null; + EntitySimpleChild ejLevel2OnEntityComplexForEjLevel1 = null; + var obj = session + .QueryOver(() => root) + .JoinEntityAlias(() => ejLevel1, Restrictions.Where(() => ejLevel1.Id == root.SameTypeChild.Id && root.Id != null), JoinType.LeftOuterJoin) + .JoinAlias(() => ejLevel1.Child1, () => customChildForEjLevel1, JoinType.InnerJoin) + .JoinAlias(() => ejLevel1.SameTypeChild, () => entityComplexForEjLevel1, JoinType.LeftOuterJoin) + .JoinEntityAlias(() => ejLevel2OnEntityComplexForEjLevel1, () => entityComplexForEjLevel1.Id == ejLevel2OnEntityComplexForEjLevel1.Id) + .Where(() => customChildForEjLevel1.Id != null && ejLevel2OnEntityComplexForEjLevel1.Id != null) + .Take(1) + .SingleOrDefault(); + } + } + + [Test] + public void EntityJoinForCompositeKey() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityWithCompositeId ejComposite = null; + EntityWithNoAssociation root = null; + root = session + .QueryOver(() => root) + .JoinEntityAlias(() => ejComposite, () => root.Composite1Key1 == ejComposite.Key.Id1 && root.Composite1Key2 == ejComposite.Key.Id2) + .Take(1).SingleOrDefault(); + var composite = session.Load(_entityWithCompositeId.Key); + + 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 NullLeftEntityJoin() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejLeftNull = null; + EntityWithNoAssociation root = null; + root = session.QueryOver(() => root) + //add some non existent join condition + .JoinEntityAlias(() => ejLeftNull, () => ejLeftNull.Id == null, JoinType.LeftOuterJoin) + .Take(1) + .SingleOrDefault(); + + Assert.That(root, Is.Not.Null, "root should not be null (looks like left join didn't work)"); + Assert.That(NHibernateUtil.IsInitialized(root), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void NullLeftEntityJoinWithEntityProjection() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ejLeftNull = null; + EntityWithNoAssociation root = null; + var objs = session.QueryOver(() => root) + //add some non existent join condition + .JoinEntityAlias(() => ejLeftNull, () => ejLeftNull.Id == null, JoinType.LeftOuterJoin) + .Select((e) => root.AsEntity(), e => ejLeftNull.AsEntity()) + .Take(1) + .SingleOrDefault(); + root = (EntityWithNoAssociation) objs[0]; + ejLeftNull = (EntityComplex) objs[1]; + + Assert.That(root, Is.Not.Null, "root should not be null (looks like left join didn't work)"); + Assert.That(NHibernateUtil.IsInitialized(root), Is.True); + Assert.That(ejLeftNull, Is.Null, "Entity join should be null"); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void EntityJoinForCustomEntityName() + { + + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityCustomEntityName ejCustomEntity= null; + EntityWithNoAssociation root = null; + root = session.QueryOver(() => root) + .JoinEntityAlias(() => ejCustomEntity, Restrictions.Where(() => ejCustomEntity.Id == root.CustomEntityNameId), JoinType.InnerJoin, customEntityName) + .Take(1) + .SingleOrDefault(); + + ejCustomEntity = (EntityCustomEntityName) session.Load(customEntityName, root.CustomEntityNameId); + + Assert.That(NHibernateUtil.IsInitialized(ejCustomEntity), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void EntityJoinForCustomEntityName_Expression() + { + + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityCustomEntityName ejCustomEntity= null; + EntityWithNoAssociation root = null; + root = session.QueryOver(() => root) + .JoinEntityAlias(() => ejCustomEntity, () => ejCustomEntity.Id == root.CustomEntityNameId, JoinType.InnerJoin, customEntityName) + .Take(1) + .SingleOrDefault(); + + ejCustomEntity = (EntityCustomEntityName) session.Load(customEntityName, root.CustomEntityNameId); + + Assert.That(NHibernateUtil.IsInitialized(ejCustomEntity), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void EntityJoinFoSubquery_JoinEntityAlias() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ej = null; + EntityWithNoAssociation root = null; + + EntityComplex ejSub = null; + EntityWithNoAssociation rootSub = null; + + var subquery = QueryOver.Of(() => rootSub) + .JoinEntityAlias(() => ejSub, () => rootSub.Complex1Id == ejSub.Id) + .Where(r => ejSub.Name == ej.Name) + .Select(x => ejSub.Id); + + root = session.QueryOver(() => root) + .JoinEntityAlias(() => ej, () => root.Complex1Id == ej.Id) + .WithSubquery.WhereExists(subquery) + .SingleOrDefault(); + ej = session.Load(root.Complex1Id); + + Assert.That(NHibernateUtil.IsInitialized(ej), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void EntityJoinFoSubquery_JoinQueryOver() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ej = null; + EntityWithNoAssociation root = null; + + EntityComplex ejSub = null; + EntityWithNoAssociation rootSub = null; + + var subquery = QueryOver.Of(() => rootSub) + .JoinEntityQueryOver(() => ejSub, () => rootSub.Complex1Id == ejSub.Id) + .Where(x => x.Name == ej.Name) + .Select(x => ejSub.Id); + + root = session.QueryOver(() => root) + .JoinEntityAlias(() => ej, () => root.Complex1Id == ej.Id) + .WithSubquery.WhereExists(subquery) + .SingleOrDefault(); + ej = session.Load(root.Complex1Id); + + Assert.That(NHibernateUtil.IsInitialized(ej), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + #region Test Setup + + 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(Mapping.ByCode.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); + }); + + mapper.Class( + rc => + { + rc.ComponentAsId( + e => e.Key, + ekm => + { + ekm.Property(ek => ek.Id1); + ekm.Property(ek => ek.Id2); + }); + + rc.Property(e => e.Name); + }); + + mapper.Class( + rc => + { + rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + + rc.Property(e => e.Complex1Id); + rc.Property(e => e.Complex2Id); + rc.Property(e => e.Simple1Id); + rc.Property(e => e.Simple2Id); + rc.Property(e => e.Composite1Key1); + rc.Property(e => e.Composite1Key2); + rc.Property(e => e.CustomEntityNameId); + + }); + + mapper.Class( + rc => + { + rc.EntityName(customEntityName); + + rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + 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() + { + 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); + + _entityWithCustomEntityName = new EntityCustomEntityName() + { + Name = "EntityCustomEntityName" + }; + + session.Save(customEntityName, _entityWithCustomEntityName); + + _noAssociation = new EntityWithNoAssociation() + { + Complex1Id = parent.Id, + Complex2Id = parent.SameTypeChild.Id, + Composite1Key1 = _entityWithCompositeId.Key.Id1, + Composite1Key2 = _entityWithCompositeId.Key.Id2, + Simple1Id = child1.Id, + Simple2Id = child2.Id, + CustomEntityNameId = _entityWithCustomEntityName.Id + }; + + session.Save(_noAssociation); + + session.Flush(); + transaction.Commit(); + } + } + + #endregion Test Setup + } + + public class EntityWithNoAssociation + { + public virtual Guid Id { get; set; } + public virtual Guid Complex1Id { get; set; } + public virtual Guid Complex2Id { get; set; } + public virtual Guid Simple1Id { get; set; } + public virtual Guid Simple2Id { get; set; } + public virtual int Composite1Key1 { get; set; } + public virtual int Composite1Key2 { get; set; } + public virtual Guid CustomEntityNameId { get; set; } + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate/Async/Impl/CriteriaImpl.cs b/src/NHibernate/Async/Impl/CriteriaImpl.cs index a40bc06bbd5..4bf96fbbebf 100644 --- a/src/NHibernate/Async/Impl/CriteriaImpl.cs +++ b/src/NHibernate/Async/Impl/CriteriaImpl.cs @@ -22,7 +22,7 @@ namespace NHibernate.Impl { using System.Threading.Tasks; - public partial class CriteriaImpl : ICriteria + public partial class CriteriaImpl : ICriteria, ISupportEntityJoinCriteria { public async Task ListAsync(CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/NHibernate/Criterion/ISupportEntityJoinQueryOver.cs b/src/NHibernate/Criterion/ISupportEntityJoinQueryOver.cs new file mode 100644 index 00000000000..11a91d8fa05 --- /dev/null +++ b/src/NHibernate/Criterion/ISupportEntityJoinQueryOver.cs @@ -0,0 +1,12 @@ +using System; +using System.Linq.Expressions; +using NHibernate.SqlCommand; + +namespace NHibernate.Criterion +{ + // 6.0 TODO: merge into IQueryOver. + public interface ISupportEntityJoinQueryOver + { + IQueryOver JoinEntityQueryOver(Expression> alias, ICriterion withClause, JoinType joinType, string entityName); + } +} diff --git a/src/NHibernate/Criterion/QueryOver.cs b/src/NHibernate/Criterion/QueryOver.cs index e84f406cc02..7e633541da4 100644 --- a/src/NHibernate/Criterion/QueryOver.cs +++ b/src/NHibernate/Criterion/QueryOver.cs @@ -65,6 +65,10 @@ internal static Exception GetDirectUsageException() [Serializable] public abstract partial class QueryOver : QueryOver, IQueryOver { + protected internal QueryOver Create(ICriteria criteria) + { + return new QueryOver(impl, criteria); + } private IList List() { @@ -280,7 +284,8 @@ IQueryOver IQueryOver.ReadOnly() /// Implementation of the interface /// [Serializable] - public class QueryOver : QueryOver, IQueryOver + public class QueryOver : QueryOver, IQueryOver, + ISupportEntityJoinQueryOver { protected internal QueryOver() @@ -658,6 +663,16 @@ public QueryOver JoinQueryOver(Expression>> path joinType)); } + public QueryOver JoinEntityQueryOver(Expression> alias, Expression> withClause, JoinType joinType = JoinType.InnerJoin, string entityName = null) + { + return JoinEntityQueryOver(alias, Restrictions.Where(withClause), joinType, entityName); + } + + public QueryOver JoinEntityQueryOver(Expression> alias, ICriterion withClause, JoinType joinType = JoinType.InnerJoin, string entityName = null) + { + return Create(criteria.CreateEntityCriteria(alias, withClause, joinType, entityName)); + } + public QueryOver JoinAlias(Expression> path, Expression> alias) { return AddAlias( @@ -974,6 +989,11 @@ IQueryOver IQueryOver.JoinAlias(Expression IQueryOver.JoinAlias(Expression>> path, Expression> alias, JoinType joinType, ICriterion withClause) { return JoinAlias(path, alias, joinType, withClause); } + IQueryOver ISupportEntityJoinQueryOver.JoinEntityQueryOver(Expression> alias, ICriterion withClause, JoinType joinType, string entityName) + { + return JoinEntityQueryOver(alias, withClause, joinType, entityName); + } + IQueryOverJoinBuilder IQueryOver.Inner { get { return new IQueryOverJoinBuilder(this, JoinType.InnerJoin); } } diff --git a/src/NHibernate/EntityJoinExtensions.cs b/src/NHibernate/EntityJoinExtensions.cs new file mode 100644 index 00000000000..ff88664b36d --- /dev/null +++ b/src/NHibernate/EntityJoinExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq.Expressions; +using NHibernate.Criterion; +using NHibernate.Impl; +using NHibernate.SqlCommand; + +namespace NHibernate +{ + public static class EntityJoinExtensions + { + #region QueryOver extensions + + public static TThis JoinEntityAlias(this TThis queryOver, Expression> alias, ICriterion withClause, JoinType joinType = JoinType.InnerJoin, string entityName = null) where TThis : IQueryOver + { + CreateEntityCriteria(queryOver.UnderlyingCriteria, alias, withClause, joinType, entityName); + return queryOver; + } + + public static TThis JoinEntityAlias(this TThis queryOver, Expression> alias, Expression> withClause, JoinType joinType = JoinType.InnerJoin, string entityName = null) where TThis : IQueryOver + { + return JoinEntityAlias(queryOver, alias, Restrictions.Where(withClause), joinType, entityName); + } + + public static IQueryOver JoinEntityQueryOver(this IQueryOver queryOver, Expression> alias, Expression> withClause, JoinType joinType = JoinType.InnerJoin, string entityName = null) + { + return JoinEntityQueryOver(queryOver, alias, Restrictions.Where(withClause), joinType, entityName); + } + + public static IQueryOver JoinEntityQueryOver(this IQueryOver queryOver, Expression> alias, ICriterion withClause, JoinType joinType = JoinType.InnerJoin, string entityName = null) + { + var q = CastOrThrow>(queryOver); + return q.JoinEntityQueryOver(alias, withClause, joinType, entityName); + } + + #endregion QueryOver extensions + + #region Criteria extensions + + public static ICriteria CreateEntityAlias(this ICriteria criteria, string alias, ICriterion withClause, JoinType joinType, string entityName) + { + CreateEntityCriteria(criteria, alias, withClause, joinType, entityName); + return criteria; + } + + public static ICriteria CreateEntityAlias(this ICriteria criteria, Expression> alias, ICriterion withClause, JoinType joinType = JoinType.InnerJoin, string entityName = null) + { + CreateEntityCriteria(criteria, alias, withClause, joinType, entityName); + return criteria; + } + + public static ICriteria CreateEntityCriteria(this ICriteria criteria, string alias, ICriterion withClause, JoinType joinType, string entityName) + { + var c = CastOrThrow(criteria); + return c.CreateEntityCriteria(alias, withClause, joinType, entityName); + } + + public static ICriteria CreateEntityCriteria(this ICriteria criteria, Expression> alias, ICriterion withClause, JoinType joinType = JoinType.InnerJoin, string entityName = null) + { + return CreateEntityCriteria(criteria, ExpressionProcessor.FindMemberExpression(alias.Body), withClause, joinType, entityName ?? typeof(U).FullName); + } + + #endregion Criteria extensions + + private static T CastOrThrow(object obj) where T : class + { + return obj as T + ?? throw new ArgumentException($"{obj.GetType().FullName} requires to implement {typeof(T).FullName} interface to support explicit entity joins."); + } + } +} diff --git a/src/NHibernate/Impl/CriteriaImpl.cs b/src/NHibernate/Impl/CriteriaImpl.cs index 5e3bbb60d88..eaee6f5b18a 100644 --- a/src/NHibernate/Impl/CriteriaImpl.cs +++ b/src/NHibernate/Impl/CriteriaImpl.cs @@ -15,7 +15,7 @@ namespace NHibernate.Impl /// Implementation of the interface /// [Serializable] - public partial class CriteriaImpl : ICriteria + public partial class CriteriaImpl : ICriteria, ISupportEntityJoinCriteria { private readonly System.Type persistentClass; private readonly List criteria = new List(); @@ -371,6 +371,11 @@ public ICriteria CreateAlias(string associationPath, string alias, JoinType join return this; } + public ICriteria CreateEntityCriteria(string alias, ICriterion withClause, JoinType joinType, string entityName) + { + return new Subcriteria(this, this, alias, alias, joinType, withClause, entityName); + } + public ICriteria Add(ICriteria criteriaInst, ICriterion expression) { criteria.Add(new CriterionEntry(expression, criteriaInst)); @@ -647,7 +652,7 @@ public sealed partial class Subcriteria : ICriteria private readonly JoinType joinType; private ICriterion withClause; - internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string alias, JoinType joinType, ICriterion withClause) + internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string alias, JoinType joinType, ICriterion withClause, string joinEntityName = null) { this.root = root; this.parent = parent; @@ -655,6 +660,7 @@ internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string al this.path = path; this.joinType = joinType; this.withClause = withClause; + JoinEntityName = joinEntityName; root.subcriteriaList.Add(this); @@ -668,6 +674,16 @@ internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string al internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, JoinType joinType) : this(root, parent, path, null, joinType) { } + /// + /// Entity name for "Entity Join" - join for entity with not mapped association + /// + public string JoinEntityName { get; } + + /// + /// Is this an Entity join for not mapped association + /// + public bool IsEntityJoin => JoinEntityName != null; + public ICriterion WithClause { get { return withClause; } diff --git a/src/NHibernate/Impl/ISupportEntityJoinCriteria.cs b/src/NHibernate/Impl/ISupportEntityJoinCriteria.cs new file mode 100644 index 00000000000..d45aa76f7cf --- /dev/null +++ b/src/NHibernate/Impl/ISupportEntityJoinCriteria.cs @@ -0,0 +1,11 @@ +using NHibernate.Criterion; +using NHibernate.SqlCommand; + +namespace NHibernate.Impl +{ + // 6.0 TODO: merge into 'ICriteria'. + public interface ISupportEntityJoinCriteria + { + ICriteria CreateEntityCriteria(string alias, ICriterion withClause, JoinType joinType, string entityName); + } +} diff --git a/src/NHibernate/Loader/AbstractEntityJoinWalker.cs b/src/NHibernate/Loader/AbstractEntityJoinWalker.cs index 30fbeca0eae..a789cbaeb90 100644 --- a/src/NHibernate/Loader/AbstractEntityJoinWalker.cs +++ b/src/NHibernate/Loader/AbstractEntityJoinWalker.cs @@ -31,7 +31,7 @@ public AbstractEntityJoinWalker(string rootSqlAlias, IOuterJoinLoadable persiste protected virtual void InitAll(SqlString whereString, SqlString orderByString, LockMode lockMode) { - WalkEntityTree(persister, Alias); + AddAssociations(); IList allAssociations = new List(associations); allAssociations.Add(CreateAssociation(persister.EntityType, alias)); @@ -48,7 +48,7 @@ protected void InitProjection(SqlString projectionString, SqlString whereString, protected void InitProjection(SqlString projectionString, SqlString whereString, SqlString orderByString, SqlString groupByString, SqlString havingString, IDictionary enabledFilters, LockMode lockMode, IList entityProjections) { - WalkEntityTree(persister, Alias); + AddAssociations(); int countEntities = entityProjections.Count; if (countEntities > 0) @@ -81,6 +81,11 @@ protected void InitProjection(SqlString projectionString, SqlString whereString, InitStatementString(projectionString, whereString, orderByString, groupByString, havingString, lockMode); } + protected virtual void AddAssociations() + { + WalkEntityTree(persister, Alias); + } + private OuterJoinableAssociation CreateAssociation(EntityType entityType, string tableAlias) { return new OuterJoinableAssociation( diff --git a/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs b/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs index 2394fef4f19..0a2dd06d017 100644 --- a/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs +++ b/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs @@ -76,6 +76,21 @@ public CriteriaJoinWalker(IOuterJoinLoadable persister, CriteriaQueryTranslator } } + protected override void AddAssociations() + { + base.AddAssociations(); + foreach (var entityJoinInfo in translator.GetEntityJoins().Values) + { + var tableAlias = translator.GetSQLAlias(entityJoinInfo.Criteria); + var criteriaPath = entityJoinInfo.Criteria.Alias; //path for entity join is equal to alias + var persister = entityJoinInfo.Persister as IOuterJoinLoadable; + AddExplicitEntityJoinAssociation(persister, tableAlias, translator.GetJoinType(criteriaPath), GetWithClause(criteriaPath)); + IncludeInResultIfNeeded(persister, entityJoinInfo.Criteria, tableAlias); + //collect mapped associations for entity join + WalkEntityTree(persister, tableAlias, criteriaPath, 1); + } + } + protected override void WalkEntityTree(IOuterJoinLoadable persister, string alias, string path, int currentDepth) { // NH different behavior (NH-1476, NH-1760, NH-1785) @@ -199,19 +214,7 @@ protected override string GenerateTableAlias(int n, string path, IJoinable joina ICriteria subcriteria = translator.GetCriteria(path); sqlAlias = subcriteria == null ? null : translator.GetSQLAlias(subcriteria); - if (joinable.ConsumesEntityAlias() && !translator.HasProjection) - { - includeInResultRowList.Add(subcriteria != null && subcriteria.Alias != null); - - if (sqlAlias != null) - { - if (subcriteria.Alias != null) - { - userAliasList.Add(subcriteria.Alias); //alias may be null - resultTypeList.Add(translator.ResultType(subcriteria)); - } - } - } + IncludeInResultIfNeeded(joinable, subcriteria, sqlAlias); } if (sqlAlias == null) @@ -220,6 +223,22 @@ protected override string GenerateTableAlias(int n, string path, IJoinable joina return sqlAlias; } + private void IncludeInResultIfNeeded(IJoinable joinable, ICriteria subcriteria, string sqlAlias) + { + if (joinable.ConsumesEntityAlias() && !translator.HasProjection) + { + includeInResultRowList.Add(subcriteria != null && subcriteria.Alias != null); + + if (sqlAlias != null) + { + if (subcriteria.Alias != null) + { + userAliasList.Add(subcriteria.Alias); //alias may be null + resultTypeList.Add(translator.ResultType(subcriteria)); + } + } + } + } protected override string GenerateRootAlias(string tableName) { diff --git a/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs b/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs index d1e1b0da801..fae80646f39 100644 --- a/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs +++ b/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs @@ -7,15 +7,23 @@ using NHibernate.Impl; using NHibernate.Param; using NHibernate.Persister.Collection; +using NHibernate.Persister.Entity; using NHibernate_Persister_Entity = NHibernate.Persister.Entity; using NHibernate.SqlCommand; using NHibernate.Type; using NHibernate.Util; +using IQueryable = NHibernate.Persister.Entity.IQueryable; namespace NHibernate.Loader.Criteria { public class CriteriaQueryTranslator : ICriteriaQuery, ISupportEntityProjectionCriteriaQuery { + public class EntityJoinInfo + { + public ICriteria Criteria; + public IQueryable Persister; + } + public static readonly string RootSqlAlias = CriteriaSpecification.RootAlias + '_'; private static readonly INHibernateLogger logger = NHibernateLogger.For(typeof(CriteriaQueryTranslator)); @@ -23,7 +31,6 @@ public class CriteriaQueryTranslator : ICriteriaQuery, ISupportEntityProjectionC private readonly ICriteriaQuery outerQueryTranslator; private readonly CriteriaImpl rootCriteria; - private readonly string rootEntityName; private readonly string rootSQLAlias; private int indexForAlias = 0; private readonly List entityProjections = new List(); @@ -47,7 +54,8 @@ public class CriteriaQueryTranslator : ICriteriaQuery, ISupportEntityProjectionC private readonly ICollection namedParameters; private readonly ISet subQuerySpaces = new HashSet(); - + private Dictionary entityJoins = new Dictionary(); + private readonly IQueryable rootPersister; public CriteriaQueryTranslator(ISessionFactoryImplementor factory, CriteriaImpl criteria, string rootEntityName, string rootSQLAlias, ICriteriaQuery outerQuery) @@ -62,8 +70,9 @@ public CriteriaQueryTranslator(ISessionFactoryImplementor factory, CriteriaImpl string rootSQLAlias) { rootCriteria = criteria; - this.rootEntityName = rootEntityName; + sessionFactory = factory; + rootPersister = GetQueryablePersister(rootEntityName); this.rootSQLAlias = rootSQLAlias; helper = new SessionFactoryHelper(factory); @@ -72,6 +81,7 @@ public CriteriaQueryTranslator(ISessionFactoryImplementor factory, CriteriaImpl CreateAliasCriteriaMap(); CreateAssociationPathCriteriaMap(); + CreateEntityJoinMap(); CreateCriteriaEntityNameMap(); CreateCriteriaCollectionPersisters(); CreateCriteriaSQLAliasMap(); @@ -115,6 +125,11 @@ public CriteriaImpl RootCriteria ICriteria ISupportEntityProjectionCriteriaQuery.RootCriteria => rootCriteria; + internal IReadOnlyDictionary GetEntityJoins() + { + return entityJoins; + } + public QueryParameters GetQueryParameters() { RowSelection selection = new RowSelection(); @@ -380,19 +395,35 @@ private string GetWholeAssociationPath(CriteriaImpl.Subcriteria subcriteria) private void CreateCriteriaEntityNameMap() { // initialize the rootProvider first - ICriteriaInfoProvider rootProvider = new EntityCriteriaInfoProvider((NHibernate_Persister_Entity.IQueryable)sessionFactory.GetEntityPersister(rootEntityName)); + ICriteriaInfoProvider rootProvider = new EntityCriteriaInfoProvider(rootPersister); criteriaInfoMap.Add(rootCriteria, rootProvider); nameCriteriaInfoMap.Add(rootProvider.Name, rootProvider); foreach (KeyValuePair me in associationPathCriteriaMap) { - ICriteriaInfoProvider info = GetPathInfo(me.Key); + ICriteriaInfoProvider info = GetPathInfo(me.Key, rootProvider); criteriaInfoMap.Add(me.Value, info); nameCriteriaInfoMap[info.Name] = info; } } + //explicit joins with not associated entities + private void CreateEntityJoinMap() + { + foreach (var criteria in rootCriteria.IterateSubcriteria()) + { + if (criteria.IsEntityJoin) + { + var entityJoinPersister = GetQueryablePersister(criteria.JoinEntityName); + entityJoins[criteria.Alias] = new EntityJoinInfo + { + Persister = entityJoinPersister, + Criteria = criteria, + }; + } + } + } private void CreateCriteriaCollectionPersisters() { @@ -408,15 +439,26 @@ private void CreateCriteriaCollectionPersisters() private Persister.Entity.IJoinable GetPathJoinable(string path) { - NHibernate_Persister_Entity.IJoinable last = (NHibernate_Persister_Entity.IJoinable)Factory.GetEntityPersister(rootEntityName); - NHibernate_Persister_Entity.IPropertyMapping lastEntity = (NHibernate_Persister_Entity.IPropertyMapping)last; - - string componentPath = ""; + // start with the root + IJoinable last = rootPersister; + + var tokens = path.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0) + return last; + + IPropertyMapping lastEntity = rootPersister; + int i = 0; + if (entityJoins.TryGetValue(tokens[0], out var entityJoinInfo)) + { + last = entityJoinInfo.Persister; + lastEntity = (IPropertyMapping) last; + i++; + } - StringTokenizer tokens = new StringTokenizer(path, ".", false); - foreach (string token in tokens) + string componentPath = string.Empty; + for (; i < tokens.Length; i++) { - componentPath += token; + componentPath += tokens[i]; IType type = lastEntity.ToType(componentPath); if (type.IsAssociationType) { @@ -446,20 +488,25 @@ private Persister.Entity.IJoinable GetPathJoinable(string path) return last; } - private ICriteriaInfoProvider GetPathInfo(string path) + private ICriteriaInfoProvider GetPathInfo(string path, ICriteriaInfoProvider rootProvider) { - StringTokenizer tokens = new StringTokenizer(path, ".", false); - string componentPath = string.Empty; - - // start with the 'rootProvider' - ICriteriaInfoProvider provider; - if (nameCriteriaInfoMap.TryGetValue(rootEntityName, out provider) == false) - throw new ArgumentException("Could not find ICriteriaInfoProvider for: " + path); + var tokens = path.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries); + // start with the root + ICriteriaInfoProvider provider = rootProvider; + if (tokens.Length == 0) + return provider; + int i = 0; + if (entityJoins.TryGetValue(tokens[0], out var entityJoinInfo)) + { + provider = new EntityCriteriaInfoProvider(entityJoinInfo.Persister); + i++; + } - foreach (string token in tokens) + string componentPath = string.Empty; + for (; i < tokens.Length; i++) { - componentPath += token; + componentPath += tokens[i]; logger.Debug("searching for {0}", componentPath); IType type = provider.GetType(componentPath); if (type.IsAssociationType) @@ -480,10 +527,8 @@ private ICriteriaInfoProvider GetPathInfo(string path) } else { - provider = new EntityCriteriaInfoProvider((NHibernate_Persister_Entity.IQueryable)sessionFactory.GetEntityPersister( - atype.GetAssociatedEntityName( - sessionFactory) - )); + provider = new EntityCriteriaInfoProvider( + GetQueryablePersister(atype.GetAssociatedEntityName(sessionFactory))); } componentPath = string.Empty; @@ -876,5 +921,11 @@ private void CreateSubQuerySpaces() } } + + private IQueryable GetQueryablePersister(string entityName) + { + return (IQueryable) sessionFactory.GetEntityPersister(entityName); + } } } + diff --git a/src/NHibernate/Loader/JoinWalker.cs b/src/NHibernate/Loader/JoinWalker.cs index 5450e09c84e..4dcdd15f832 100644 --- a/src/NHibernate/Loader/JoinWalker.cs +++ b/src/NHibernate/Loader/JoinWalker.cs @@ -295,6 +295,25 @@ private void WalkCollectionTree(IQueryableCollection persister, string alias, st } } + internal void AddExplicitEntityJoinAssociation( + IOuterJoinLoadable persister, + string tableAlias, + JoinType joinType, + SqlString withClause) + { + OuterJoinableAssociation assoc = + new OuterJoinableAssociation( + persister.EntityType, + string.Empty, + Array.Empty(), + tableAlias, + joinType, + withClause, + Factory, + enabledFilters); + AddAssociation(tableAlias, assoc); + } + private void WalkEntityAssociationTree(IAssociationType associationType, IOuterJoinLoadable persister, int propertyNumber, string alias, string path, bool nullable, int currentDepth, ILhsAssociationTypeSqlInfo associationTypeSQLInfo) diff --git a/src/NHibernate/SqlCommand/ANSIJoinFragment.cs b/src/NHibernate/SqlCommand/ANSIJoinFragment.cs index 4c9b88348f7..aa9421fa08e 100644 --- a/src/NHibernate/SqlCommand/ANSIJoinFragment.cs +++ b/src/NHibernate/SqlCommand/ANSIJoinFragment.cs @@ -39,6 +39,12 @@ public override void AddJoin(string tableName, string alias, string[] fkColumns, _fromFragment.Add(joinString + tableName + ' ' + alias + " on "); + if (fkColumns.Length == 0) + { + AddBareCondition(_fromFragment, on); + return; + } + for (int j = 0; j < fkColumns.Length; j++) { _fromFragment.Add(fkColumns[j] + "=" + alias + StringHelper.Dot + pkColumns[j]); @@ -96,4 +102,4 @@ public override void AddFromFragmentString(SqlString fromFragmentString) _fromFragment.Add(fromFragmentString); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/SqlCommand/JoinFragment.cs b/src/NHibernate/SqlCommand/JoinFragment.cs index 4b7a47af020..c91e0a7de7a 100644 --- a/src/NHibernate/SqlCommand/JoinFragment.cs +++ b/src/NHibernate/SqlCommand/JoinFragment.cs @@ -58,6 +58,20 @@ protected bool AddCondition(SqlStringBuilder buffer, string on) } } + /// + /// Adds condition to buffer without adding " and " prefix. Existing " and" prefix is removed + /// + protected void AddBareCondition(SqlStringBuilder buffer, SqlString condition) + { + if (SqlStringHelper.IsEmpty(condition)) + return; + + buffer.Add( + condition.StartsWithCaseInsensitive(" and ") + ? condition.Substring(4) + : condition); + } + protected bool AddCondition(SqlStringBuilder buffer, SqlString on) { if (SqlStringHelper.IsNotEmpty(on)) @@ -79,4 +93,4 @@ protected bool AddCondition(SqlStringBuilder buffer, SqlString on) public bool HasThetaJoins { get; set; } } -} \ No newline at end of file +}