diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH3472/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH3472/Fixture.cs new file mode 100644 index 00000000000..fb8d95d5d49 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH3472/Fixture.cs @@ -0,0 +1,152 @@ +//------------------------------------------------------------------------------ +// +// 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.Collections.Generic; +using NHibernate.Criterion; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH3472 +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : BugTestCase + { + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var c = new Cat + { + Age = 6, + Children = new HashSet + { + new Cat + { + Age = 4, + Children = new HashSet + { + new Cat { Color = "Ginger", Age = 1 }, + new Cat { Color = "Black", Age = 3 } + } + } + } + }; + s.Save(c); + t.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Delete("from Cat"); + t.Commit(); + } + } + + [Test] + public async Task CriteriaQueryWithMultipleJoinsToSameAssociationAsync() + { + using (var s = OpenSession()) + { + var list = + await (s + .CreateCriteria("cat") + .CreateAlias( + "cat.Children", + "gingerCat", + JoinType.LeftOuterJoin, + Restrictions.Eq("Color", "Ginger")) + .CreateAlias( + "cat.Children", + "blackCat", + JoinType.LeftOuterJoin, + Restrictions.Eq("Color", "Black")) + .SetProjection( + Projections.Alias(Projections.Property("gingerCat.Age"), "gingerCatAge"), + Projections.Alias(Projections.Property("blackCat.Age"), "blackCatAge") + ).AddOrder(new Order(Projections.Property("Age"), true)).ListAsync()); + Assert.That(list, Has.Count.EqualTo(4)); + Assert.That(list[0], Is.EqualTo(new object[] { null, null })); + Assert.That(list[1], Is.EqualTo(new object[] { null, null })); + Assert.That(list[2], Is.EqualTo(new object[] { 1, 3 })); + Assert.That(list[3], Is.EqualTo(new object[] { null, null })); + } + } + + [Test] + public async Task QueryWithFetchesAndAliasDoNotDuplicateJoinAsync() + { + using (var s = OpenSession()) + { + Cat parent = null; + using (var spy = new SqlLogSpy()) + { + var list = + await (s + .QueryOver() + .Fetch(SelectMode.Fetch, o => o.Parent) + .Fetch(SelectMode.Fetch, o => o.Parent.Parent) + .JoinAlias(o => o.Parent, () => parent) + .Where(x => parent.Age == 4) + .ListAsync()); + + // Two joins to Cat are expected: one for the immediate parent, and a second for the grand-parent. + // So checking if it does not contain three joins or more. (The regex uses "[\s\S]" instead of "." + // because the SQL is formatted by default and contains "\n" which are not matched by ".".) + Assert.That(spy.GetWholeLog(), Does.Not.Match(@"(?:\bjoin\s*Cat\b[\s\S]*){3,}").IgnoreCase); + Assert.That(list, Has.Count.EqualTo(2)); + Assert.That( + NHibernateUtil.IsInitialized(list[0].Parent), + Is.True, + "first cat parent initialization status"); + Assert.That( + NHibernateUtil.IsInitialized(list[1].Parent), + Is.True, + "second cat parent initialization status"); + Assert.That( + NHibernateUtil.IsInitialized(list[0].Parent.Parent), + Is.True, + "first cat parent parent initialization status"); + Assert.That( + NHibernateUtil.IsInitialized(list[1].Parent.Parent), + Is.True, + "second cat parent parent initialization status"); + } + } + } + + [Test, Explicit("Debatable use case")] + public async Task QueryWithFetchesAndMultipleJoinsToSameAssociationAsync() + { + using (var s = OpenSession()) + { + Cat ginger = null; + Cat black = null; + var list = + await (s + .QueryOver() + .Fetch(SelectMode.Fetch, o => o.Children) + .JoinAlias(o => o.Children, () => ginger) + .Where(x => ginger.Color == "Ginger") + .JoinAlias(o => o.Children, () => black) + .Where(x => black.Color == "Black") + .ListAsync()); + + Assert.That(list, Has.Count.EqualTo(1)); + Assert.That(list[0].Children, Has.Count.EqualTo(2)); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3472/Domain.cs b/src/NHibernate.Test/NHSpecificTest/NH3472/Domain.cs new file mode 100644 index 00000000000..14a655d43ae --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3472/Domain.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3472 +{ + public class Cat + { + public virtual int Id { get; set; } + + public virtual string Color { get; set; } + public virtual int Age { get; set; } + public virtual Cat Parent { get; set; } + + public virtual ISet Children { get; set; } = new HashSet(); + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3472/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3472/Fixture.cs new file mode 100644 index 00000000000..aee93d374b2 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3472/Fixture.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using NHibernate.Criterion; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH3472 +{ + [TestFixture] + public class Fixture : BugTestCase + { + protected override void OnSetUp() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var c = new Cat + { + Age = 6, + Children = new HashSet + { + new Cat + { + Age = 4, + Children = new HashSet + { + new Cat { Color = "Ginger", Age = 1 }, + new Cat { Color = "Black", Age = 3 } + } + } + } + }; + s.Save(c); + t.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Delete("from Cat"); + t.Commit(); + } + } + + [Test] + public void CriteriaQueryWithMultipleJoinsToSameAssociation() + { + using (var s = OpenSession()) + { + var list = + s + .CreateCriteria("cat") + .CreateAlias( + "cat.Children", + "gingerCat", + JoinType.LeftOuterJoin, + Restrictions.Eq("Color", "Ginger")) + .CreateAlias( + "cat.Children", + "blackCat", + JoinType.LeftOuterJoin, + Restrictions.Eq("Color", "Black")) + .SetProjection( + Projections.Alias(Projections.Property("gingerCat.Age"), "gingerCatAge"), + Projections.Alias(Projections.Property("blackCat.Age"), "blackCatAge") + ).AddOrder(new Order(Projections.Property("Age"), true)).List(); + Assert.That(list, Has.Count.EqualTo(4)); + Assert.That(list[0], Is.EqualTo(new object[] { null, null })); + Assert.That(list[1], Is.EqualTo(new object[] { null, null })); + Assert.That(list[2], Is.EqualTo(new object[] { 1, 3 })); + Assert.That(list[3], Is.EqualTo(new object[] { null, null })); + } + } + + [Test] + public void QueryWithFetchesAndAliasDoNotDuplicateJoin() + { + using (var s = OpenSession()) + { + Cat parent = null; + using (var spy = new SqlLogSpy()) + { + var list = + s + .QueryOver() + .Fetch(SelectMode.Fetch, o => o.Parent) + .Fetch(SelectMode.Fetch, o => o.Parent.Parent) + .JoinAlias(o => o.Parent, () => parent) + .Where(x => parent.Age == 4) + .List(); + + // Two joins to Cat are expected: one for the immediate parent, and a second for the grand-parent. + // So checking if it does not contain three joins or more. (The regex uses "[\s\S]" instead of "." + // because the SQL is formatted by default and contains "\n" which are not matched by ".".) + Assert.That(spy.GetWholeLog(), Does.Not.Match(@"(?:\bjoin\s*Cat\b[\s\S]*){3,}").IgnoreCase); + Assert.That(list, Has.Count.EqualTo(2)); + Assert.That( + NHibernateUtil.IsInitialized(list[0].Parent), + Is.True, + "first cat parent initialization status"); + Assert.That( + NHibernateUtil.IsInitialized(list[1].Parent), + Is.True, + "second cat parent initialization status"); + Assert.That( + NHibernateUtil.IsInitialized(list[0].Parent.Parent), + Is.True, + "first cat parent parent initialization status"); + Assert.That( + NHibernateUtil.IsInitialized(list[1].Parent.Parent), + Is.True, + "second cat parent parent initialization status"); + } + } + } + + [Test, Explicit("Debatable use case")] + public void QueryWithFetchesAndMultipleJoinsToSameAssociation() + { + using (var s = OpenSession()) + { + Cat ginger = null; + Cat black = null; + var list = + s + .QueryOver() + .Fetch(SelectMode.Fetch, o => o.Children) + .JoinAlias(o => o.Children, () => ginger) + .Where(x => ginger.Color == "Ginger") + .JoinAlias(o => o.Children, () => black) + .Where(x => black.Color == "Black") + .List(); + + Assert.That(list, Has.Count.EqualTo(1)); + Assert.That(list[0].Children, Has.Count.EqualTo(2)); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH3472/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3472/Mappings.hbm.xml new file mode 100644 index 00000000000..cd40636acdc --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3472/Mappings.hbm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs b/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs index 75ba986cc20..4ec1d841976 100644 --- a/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs +++ b/src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs @@ -81,8 +81,9 @@ protected override void AddAssociations() { var tableAlias = translator.GetSQLAlias(entityJoinInfo.Criteria); var criteriaPath = entityJoinInfo.Criteria.Alias; //path for entity join is equal to alias + var criteriaAlias = entityJoinInfo.Criteria.Alias; var persister = entityJoinInfo.Persister as IOuterJoinLoadable; - AddExplicitEntityJoinAssociation(persister, tableAlias, translator.GetJoinType(criteriaPath), criteriaPath); + AddExplicitEntityJoinAssociation(persister, tableAlias, translator.GetJoinType(criteriaPath, criteriaAlias), criteriaPath, criteriaAlias); IncludeInResultIfNeeded(persister, entityJoinInfo.Criteria, tableAlias, criteriaPath); //collect mapped associations for entity join WalkEntityTree(persister, tableAlias, criteriaPath, 1); @@ -166,13 +167,21 @@ public override string Comment get { return "criteria query"; } } - protected override JoinType GetJoinType(IAssociationType type, FetchMode config, string path, string lhsTable, - string[] lhsColumns, bool nullable, int currentDepth, - CascadeStyle cascadeStyle) + /// + protected override IReadOnlyCollection GetChildAliases(string parentSqlAlias, string childPath) { - if (translator.IsJoin(path)) + var alias = translator.GetChildAliases(parentSqlAlias, childPath); + if (alias.Count == 0) + return base.GetChildAliases(parentSqlAlias, childPath); + return alias; + } + + protected override JoinType GetJoinType(IAssociationType type, FetchMode config, string path, string pathAlias, + string lhsTable, string[] lhsColumns, bool nullable, int currentDepth, CascadeStyle cascadeStyle) + { + if (translator.IsJoin(path, pathAlias)) { - return translator.GetJoinType(path); + return translator.GetJoinType(path, pathAlias); } if (translator.HasProjection) @@ -184,7 +193,7 @@ protected override JoinType GetJoinType(IAssociationType type, FetchMode config, switch (selectMode) { case SelectMode.Undefined: - return base.GetJoinType(type, config, path, lhsTable, lhsColumns, nullable, currentDepth, cascadeStyle); + return base.GetJoinType(type, config, path, pathAlias, lhsTable, lhsColumns, nullable, currentDepth, cascadeStyle); case SelectMode.Fetch: case SelectMode.FetchLazyProperties: @@ -200,7 +209,7 @@ protected override JoinType GetJoinType(IAssociationType type, FetchMode config, } } - protected override string GenerateTableAlias(int n, string path, IJoinable joinable) + protected override string GenerateTableAlias(int n, string path, string pathAlias, IJoinable joinable) { // TODO: deal with side-effects (changes to includeInSelectList, userAliasList, resultTypeList)!!! @@ -225,14 +234,14 @@ protected override string GenerateTableAlias(int n, string path, IJoinable joina if (shouldCreateUserAlias) { - ICriteria subcriteria = translator.GetCriteria(path); + var subcriteria = translator.GetCriteria(path, pathAlias); sqlAlias = subcriteria == null ? null : translator.GetSQLAlias(subcriteria); IncludeInResultIfNeeded(joinable, subcriteria, sqlAlias, path); } if (sqlAlias == null) - sqlAlias = base.GenerateTableAlias(n + translator.SQLAliasCount, path, joinable); + sqlAlias = base.GenerateTableAlias(n + translator.SQLAliasCount, path, pathAlias, joinable); return sqlAlias; } @@ -261,9 +270,9 @@ protected override string GenerateRootAlias(string tableName) // NH: really not used (we are using a different ctor to support SubQueryCriteria) } - protected override SqlString GetWithClause(string path) + protected override SqlString GetWithClause(string path, string pathAlias) { - return translator.GetWithClause(path); + return translator.GetWithClause(path, pathAlias); } } } diff --git a/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs b/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs index 9a9acd430fd..05749b3fdce 100644 --- a/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs +++ b/src/NHibernate/Loader/Criteria/CriteriaQueryTranslator.cs @@ -44,10 +44,12 @@ public class EntityJoinInfo private readonly HashSet uncacheableCollectionPersisters = new HashSet(); private readonly ISet criteriaCollectionPersisters = new HashSet(); private readonly IDictionary criteriaSQLAliasMap = new Dictionary(); + private readonly Dictionary sqlAliasToCriteriaAliasMap = new Dictionary(); + private readonly Dictionary> associationAliasToChildrenAliasesMap = new Dictionary>(); private readonly IDictionary aliasCriteriaMap = new Dictionary(); - private readonly Dictionary associationPathCriteriaMap = new Dictionary(); - private readonly IDictionary associationPathJoinTypesMap = new LinkedHashMap(); - private readonly IDictionary withClauseMap = new Dictionary(); + private readonly Dictionary associationPathCriteriaMap = new Dictionary(); + private readonly Dictionary associationPathJoinTypesMap = new Dictionary(); + private readonly Dictionary withClauseMap = new Dictionary(); private readonly ISessionFactoryImplementor sessionFactory; private SessionFactoryHelper helper; @@ -58,6 +60,44 @@ public class EntityJoinInfo private Dictionary entityJoins = new Dictionary(); private readonly IQueryable rootPersister; + //Key for the dictionary sub-criteria + private class AliasKey : IEquatable + { + public AliasKey(string alias, string path) + { + Alias = alias; + Path = path; + } + + public string Alias { get; } + public string Path { get; } + + public bool Equals(AliasKey other) + { + return other != null && string.Equals(Alias, other.Alias) && string.Equals(Path, other.Path); + } + + public override int GetHashCode() + { + unchecked + { + return ((Alias != null ? Alias.GetHashCode() : 0) * 397) ^ (Path != null ? Path.GetHashCode() : 0); + } + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + return true; + return Equals(obj as AliasKey); + } + + public override string ToString() + { + return $"path: {Path}; alias: {Alias}"; + } + } + public CriteriaQueryTranslator(ISessionFactoryImplementor factory, CriteriaImpl criteria, string rootEntityName, string rootSQLAlias, ICriteriaQuery outerQuery) : this(factory, criteria, rootEntityName, rootSQLAlias) @@ -280,23 +320,76 @@ private ICriteria GetAliasedCriteria(string alias) return result; } + // Since v5.2 + [Obsolete("Use overload with a critAlias additional parameter", true)] public bool IsJoin(string path) { - return associationPathCriteriaMap.ContainsKey(path); + return associationPathCriteriaMap.Keys.Any(k => k.Path == path); + } + + public bool IsJoin(string path, string critAlias) + { + return associationPathCriteriaMap.ContainsKey(new AliasKey(critAlias, path)); } + /// + /// Returns the child criteria aliases for a parent SQL alias and a child path. + /// + public IReadOnlyCollection GetChildAliases(string parentSqlAlias, string childPath) + { + var alias = new List(); + + if (!sqlAliasToCriteriaAliasMap.TryGetValue(parentSqlAlias, out var parentAlias)) + parentAlias = rootCriteria.Alias; + + if (!associationAliasToChildrenAliasesMap.TryGetValue(parentAlias, out var children)) + return alias; + + foreach (var child in children) + { + if (associationPathJoinTypesMap.ContainsKey(new AliasKey(child, childPath))) + alias.Add(child); + } + + return alias; + } + + // Since v5.2 + [Obsolete("Use overload with a critAlias additional parameter", true)] public JoinType GetJoinType(string path) { - JoinType result; - if (associationPathJoinTypesMap.TryGetValue(path, out result)) + return + associationPathJoinTypesMap + .Where(kv => kv.Key.Path == path) + .Select(kv => (JoinType?) kv.Value) + .SingleOrDefault() ?? JoinType.InnerJoin; + } + + public JoinType GetJoinType(string path, string critAlias) + { + if (associationPathJoinTypesMap.TryGetValue(new AliasKey(critAlias, path), out var result)) return result; return JoinType.InnerJoin; } + // Since v5.2 + [Obsolete("Use overload with a critAlias additional parameter", true)] public ICriteria GetCriteria(string path) { - associationPathCriteriaMap.TryGetValue(path, out var result); - logger.Debug("getCriteria for path={0} crit={1}", path, result); + var result = + associationPathCriteriaMap + .Where(kv => kv.Key.Path == path) + .Select(kv => kv.Value) + .SingleOrDefault(); + logger.Debug("getCriteria for path {0}: crit={1}", path, result); + return result; + } + + public ICriteria GetCriteria(string path, string critAlias) + { + var key = new AliasKey(critAlias, path); + associationPathCriteriaMap.TryGetValue(key, out var result); + logger.Debug("getCriteria for {0}: crit={1}", key, result); return result; } @@ -322,42 +415,49 @@ private void CreateAliasCriteriaMap() private void CreateAssociationPathCriteriaMap() { - foreach (CriteriaImpl.Subcriteria crit in rootCriteria.IterateSubcriteria()) + foreach (var crit in rootCriteria.IterateSubcriteria()) { - string wholeAssociationPath = GetWholeAssociationPath(crit); + var wholeAssociationPath = GetWholeAssociationPath(crit, out var parentAlias); + if (parentAlias != null) + { + if (!associationAliasToChildrenAliasesMap.TryGetValue(parentAlias, out var children)) + { + children = new HashSet(); + associationAliasToChildrenAliasesMap.Add(parentAlias, children); + } + children.Add(crit.Alias); + } + var key = new AliasKey(crit.Alias, wholeAssociationPath); try { - associationPathCriteriaMap.Add(wholeAssociationPath, crit); + associationPathCriteriaMap.Add(key, crit); } catch (ArgumentException ae) { - throw new QueryException("duplicate association path: " + wholeAssociationPath, ae); + throw new QueryException("duplicate association path: " + key, ae); } try { - associationPathJoinTypesMap.Add(wholeAssociationPath, crit.JoinType); + associationPathJoinTypesMap.Add(key, crit.JoinType); } catch (ArgumentException ae) { - throw new QueryException("duplicate association path: " + wholeAssociationPath, ae); + throw new QueryException("duplicate association path: " + key, ae); } try { - if (crit.WithClause != null) - { - withClauseMap.Add(wholeAssociationPath, crit.WithClause); - } + withClauseMap.Add(key, crit.WithClause); } catch (ArgumentException ae) { - throw new QueryException("duplicate association path: " + wholeAssociationPath, ae); + throw new QueryException("duplicate association path: " + key, ae); } } } - private string GetWholeAssociationPath(CriteriaImpl.Subcriteria subcriteria) + private string GetWholeAssociationPath(CriteriaImpl.Subcriteria subcriteria, out string parentAlias) { string path = subcriteria.Path; @@ -385,6 +485,8 @@ private string GetWholeAssociationPath(CriteriaImpl.Subcriteria subcriteria) path = StringHelper.Unroot(path); } + parentAlias = parent.Alias; + if (parent.Equals(rootCriteria)) { // if its the root criteria, we are done @@ -393,7 +495,7 @@ private string GetWholeAssociationPath(CriteriaImpl.Subcriteria subcriteria) else { // otherwise, recurse - return GetWholeAssociationPath((CriteriaImpl.Subcriteria)parent) + '.' + path; + return GetWholeAssociationPath((CriteriaImpl.Subcriteria) parent, out _) + '.' + path; } } @@ -405,11 +507,11 @@ private void CreateCriteriaEntityNameMap() nameCriteriaInfoMap.Add(rootProvider.Name, rootProvider); - foreach (KeyValuePair me in associationPathCriteriaMap) + foreach (var me in associationPathCriteriaMap) { - ICriteriaInfoProvider info = GetPathInfo(me.Key, rootProvider); + var info = GetPathInfo(me.Key.Path, rootProvider); criteriaInfoMap.Add(me.Value, info); - nameCriteriaInfoMap[info.Name] = info; + nameCriteriaInfoMap[info.Name] = info; } } @@ -432,9 +534,9 @@ private void CreateEntityJoinMap() private void CreateCriteriaCollectionPersisters() { - foreach (KeyValuePair me in associationPathCriteriaMap) + foreach (var me in associationPathCriteriaMap) { - if (GetPathJoinable(me.Key) is ICollectionPersister collectionPersister) + if (GetPathJoinable(me.Key.Path) is ICollectionPersister collectionPersister) { criteriaCollectionPersisters.Add(collectionPersister); @@ -569,11 +671,14 @@ private void CreateCriteriaSQLAliasMap() { alias = me.Value.Name; // the entity name } - criteriaSQLAliasMap[crit] = StringHelper.GenerateAlias(alias, i++); - logger.Debug("put criteria={0} alias={1}", - crit, criteriaSQLAliasMap[crit]); + var sqlAlias = StringHelper.GenerateAlias(alias, i++); + criteriaSQLAliasMap[crit] = sqlAlias; + logger.Debug("put criteria={0} alias={1}", crit, sqlAlias); + if (!string.IsNullOrEmpty(crit.Alias)) + sqlAliasToCriteriaAliasMap[sqlAlias] = alias; } criteriaSQLAliasMap[rootCriteria] = rootSQLAlias; + sqlAliasToCriteriaAliasMap[rootSQLAlias] = rootCriteria.Alias; } public bool HasProjection @@ -768,14 +873,25 @@ public string GetPropertyName(string propertyName) return propertyName; } + // Since v5.2 + [Obsolete("Use overload with a critAlias additional parameter", true)] public SqlString GetWithClause(string path) { - ICriterion crit; - if (withClauseMap.TryGetValue(path, out crit)) - { - return crit == null ? null : crit.ToSqlString(GetCriteria(path), this); - } - return null; + var crit = + withClauseMap + .Where(kv => kv.Key.Path == path) + .Select(kv => kv.Value) + .SingleOrDefault(); + + return crit?.ToSqlString(GetCriteria(path), this); + } + + public SqlString GetWithClause(string path, string pathAlias) + { + var key = new AliasKey(pathAlias, path); + withClauseMap.TryGetValue(key, out var crit); + + return crit?.ToSqlString(GetCriteria(path, key.Alias), this); } #region NH specific diff --git a/src/NHibernate/Loader/JoinWalker.cs b/src/NHibernate/Loader/JoinWalker.cs index fe3ea666811..160fd39ea32 100644 --- a/src/NHibernate/Loader/JoinWalker.cs +++ b/src/NHibernate/Loader/JoinWalker.cs @@ -134,32 +134,51 @@ protected JoinWalker(ISessionFactoryImplementor factory, IDictionary private void AddAssociationToJoinTreeIfNecessary(IAssociationType type, string[] aliasedLhsColumns, - string alias, string path, int currentDepth, JoinType joinType) + string alias, string path, string subPathAlias, int currentDepth, JoinType joinType) { if (joinType >= JoinType.InnerJoin) { - AddAssociationToJoinTree(type, aliasedLhsColumns, alias, path, currentDepth, joinType); + AddAssociationToJoinTree(type, aliasedLhsColumns, alias, path, subPathAlias, currentDepth, joinType); } } + // Since v5.2 + [Obsolete("Use or override the overload taking a pathAlias additional parameter")] protected virtual SqlString GetWithClause(string path) { return SqlString.Empty; } + protected virtual SqlString GetWithClause(string path, string pathAlias) + { + // 6.0 TODO: inline the call +#pragma warning disable 618 + return GetWithClause(path); +#pragma warning restore 618 + } + /// /// Add on association (one-to-one, many-to-one, or a collection) to a list /// of associations to be fetched by outerjoin /// private void AddAssociationToJoinTree(IAssociationType type, string[] aliasedLhsColumns, string alias, - string path, int currentDepth, JoinType joinType) + string path, string subPathAlias, int currentDepth, JoinType joinType) { IJoinable joinable = type.GetAssociatedJoinable(Factory); - string subalias = GenerateTableAlias(associations.Count + 1, path, joinable); + string subalias = GenerateTableAlias(associations.Count + 1, path, subPathAlias, joinable); - OuterJoinableAssociation assoc = - new OuterJoinableAssociation(type, alias, aliasedLhsColumns, subalias, joinType, GetWithClause(path), Factory, enabledFilters, GetSelectMode(path)); + var assoc = + new OuterJoinableAssociation( + type, + alias, + aliasedLhsColumns, + subalias, + joinType, + GetWithClause(path, subPathAlias), + Factory, + enabledFilters, + GetSelectMode(path)); assoc.ValidateJoin(path); AddAssociation(subalias, assoc); @@ -175,7 +194,7 @@ private void AddAssociationToJoinTree(IAssociationType type, string[] aliasedLhs { IQueryableCollection qc = joinable as IQueryableCollection; if (qc != null) - WalkCollectionTree(qc, subalias, path, nextDepth); + WalkCollectionTree(qc, subalias, path, subPathAlias, nextDepth); } } @@ -257,14 +276,14 @@ protected void WalkEntityTree(IOuterJoinLoadable persister, string alias) /// protected void WalkCollectionTree(IQueryableCollection persister, string alias) { - WalkCollectionTree(persister, alias, string.Empty, 0); + WalkCollectionTree(persister, alias, string.Empty, string.Empty, 0); //TODO: when this is the entry point, we should use an INNER_JOIN for fetching the many-to-many elements! } /// /// For a collection role, return a list of associations to be fetched by outerjoin /// - private void WalkCollectionTree(IQueryableCollection persister, string alias, string path, int currentDepth) + private void WalkCollectionTree(IQueryableCollection persister, string alias, string path, string subPathAlias, int currentDepth) { if (persister.IsOneToMany) { @@ -278,7 +297,7 @@ private void WalkCollectionTree(IQueryableCollection persister, string alias, st // a many-to-many // decrement currentDepth here to allow join across the association table // without exceeding MAX_FETCH_DEPTH (i.e. the "currentDepth - 1" bit) - IAssociationType associationType = (IAssociationType)type; + IAssociationType associationType = (IAssociationType) type; string[] aliasedLhsColumns = persister.GetElementColumnNames(alias); string[] lhsColumns = persister.ElementColumnNames; @@ -287,16 +306,37 @@ private void WalkCollectionTree(IQueryableCollection persister, string alias, st // an inner join... bool useInnerJoin = currentDepth == 0; - JoinType joinType = - GetJoinType(associationType, persister.FetchMode, path, persister.TableName, lhsColumns, !useInnerJoin, - currentDepth - 1, null); - - AddAssociationToJoinTreeIfNecessary(associationType, aliasedLhsColumns, alias, path, currentDepth - 1, joinType); + var joinType = + GetJoinType( + associationType, + persister.FetchMode, + path, + subPathAlias, + persister.TableName, + lhsColumns, + !useInnerJoin, + currentDepth - 1, + null); + + AddAssociationToJoinTreeIfNecessary( + associationType, + aliasedLhsColumns, + alias, + path, + subPathAlias, + currentDepth - 1, + joinType); } else if (type.IsComponentType) { - WalkCompositeElementTree((IAbstractComponentType)type, persister.ElementColumnNames, persister, alias, path, - currentDepth); + WalkCompositeElementTree( + (IAbstractComponentType) type, + persister.ElementColumnNames, + persister, + alias, + path, + subPathAlias, + currentDepth); } } } @@ -305,7 +345,8 @@ internal void AddExplicitEntityJoinAssociation( IOuterJoinLoadable persister, string tableAlias, JoinType joinType, - string path) + string path, + string alias) { OuterJoinableAssociation assoc = new OuterJoinableAssociation( @@ -314,7 +355,7 @@ internal void AddExplicitEntityJoinAssociation( Array.Empty(), tableAlias, joinType, - GetWithClause(path), + GetWithClause(path, alias), Factory, enabledFilters, GetSelectMode(path)); @@ -331,10 +372,30 @@ private void WalkEntityAssociationTree(IAssociationType associationType, IOuterJ string subpath = SubPath(path, persister.GetSubclassPropertyName(propertyNumber)); - JoinType joinType = GetJoinType(associationType, persister.GetFetchMode(propertyNumber), subpath, lhsTable, - lhsColumns, nullable, currentDepth, persister.GetCascadeStyle(propertyNumber)); - - AddAssociationToJoinTreeIfNecessary(associationType, aliasedLhsColumns, alias, subpath, currentDepth, joinType); + // Obtain children aliases for the current path and alias + var subPathAliases = GetChildAliases(alias, subpath); + foreach (var subPathAlias in subPathAliases) + { + var joinType = GetJoinType( + associationType, + persister.GetFetchMode(propertyNumber), + subpath, + subPathAlias, + lhsTable, + lhsColumns, + nullable, + currentDepth, + persister.GetCascadeStyle(propertyNumber)); + + AddAssociationToJoinTreeIfNecessary( + associationType, + aliasedLhsColumns, + alias, + subpath, + subPathAlias, + currentDepth, + joinType); + } } /// @@ -382,11 +443,30 @@ protected void WalkComponentTree(IAbstractComponentType componentType, int begin string subpath = SubPath(path, propertyNames[i]); bool[] propertyNullability = componentType.PropertyNullability; - JoinType joinType = GetJoinType(associationType, componentType.GetFetchMode(i), subpath, lhsTable, lhsColumns, - propertyNullability == null || propertyNullability[i], currentDepth, - componentType.GetCascadeStyle(i)); - - AddAssociationToJoinTreeIfNecessary(associationType, aliasedLhsColumns, alias, subpath, currentDepth, joinType); + // Obtain related aliases to the current path + var subPathAliases = GetChildAliases(alias, subpath); + foreach (var subPathAlias in subPathAliases) + { + var joinType = GetJoinType( + associationType, + componentType.GetFetchMode(i), + subpath, + subPathAlias, + lhsTable, + lhsColumns, + propertyNullability == null || propertyNullability[i], + currentDepth, + componentType.GetCascadeStyle(i)); + + AddAssociationToJoinTreeIfNecessary( + associationType, + aliasedLhsColumns, + alias, + subpath, + subPathAlias, + currentDepth, + joinType); + } } else if (types[i].IsComponentType) { @@ -402,7 +482,7 @@ protected void WalkComponentTree(IAbstractComponentType componentType, int begin /// For a composite element, add to a list of associations to be fetched by outerjoin /// private void WalkCompositeElementTree(IAbstractComponentType compositeType, string[] cols, - IQueryableCollection persister, string alias, string path, int currentDepth) + IQueryableCollection persister, string alias, string path, string subPathAlias, int currentDepth) { IType[] types = compositeType.Subtypes; string[] propertyNames = compositeType.PropertyNames; @@ -422,16 +502,38 @@ private void WalkCompositeElementTree(IAbstractComponentType compositeType, stri string subpath = SubPath(path, propertyNames[i]); bool[] propertyNullability = compositeType.PropertyNullability; - JoinType joinType = - GetJoinType(associationType, compositeType.GetFetchMode(i), subpath, persister.TableName, lhsColumns, - propertyNullability == null || propertyNullability[i], currentDepth, compositeType.GetCascadeStyle(i)); - - AddAssociationToJoinTreeIfNecessary(associationType, aliasedLhsColumns, alias, subpath, currentDepth, joinType); + var joinType = + GetJoinType( + associationType, + compositeType.GetFetchMode(i), + subpath, + alias, + persister.TableName, + lhsColumns, + propertyNullability == null || propertyNullability[i], + currentDepth, + compositeType.GetCascadeStyle(i)); + + AddAssociationToJoinTreeIfNecessary( + associationType, + aliasedLhsColumns, + alias, + subpath, + subPathAlias, + currentDepth, + joinType); } else if (types[i].IsComponentType) { string subpath = SubPath(path, propertyNames[i]); - WalkCompositeElementTree((IAbstractComponentType)types[i], lhsColumns, persister, alias, subpath, currentDepth); + WalkCompositeElementTree( + (IAbstractComponentType) types[i], + lhsColumns, + persister, + alias, + subpath, + subPathAlias, + currentDepth); } begin += length; } @@ -452,6 +554,8 @@ protected static string SubPath(string path, string property) /// association should not be joined. Override on /// subclasses. /// + // Since v5.2 + [Obsolete("Use or override the overload taking a pathAlias additional parameter")] protected virtual JoinType GetJoinType(IAssociationType type, FetchMode config, string path, string lhsTable, string[] lhsColumns, bool nullable, int currentDepth, CascadeStyle cascadeStyle) { @@ -468,6 +572,32 @@ protected virtual JoinType GetJoinType(IAssociationType type, FetchMode config, return GetJoinType(nullable, currentDepth); } + /// + /// Get the join type (inner, outer, etc) or -1 if the + /// association should not be joined. Override on + /// subclasses. + /// + protected virtual JoinType GetJoinType(IAssociationType type, FetchMode config, string path, string pathAlias, + string lhsTable, string[] lhsColumns, bool nullable, int currentDepth, CascadeStyle cascadeStyle) + { + // 6.0 TODO: inline the call +#pragma warning disable 618 + return GetJoinType(type, config, path, lhsTable, lhsColumns, nullable, currentDepth, cascadeStyle); +#pragma warning restore 618 + } + + // By default, multiple aliases for a child are not supported. There is only one and its value + // does not matter for default implementation. + private static readonly IReadOnlyCollection DefaultChildAliases = new[] { string.Empty }; + + /// + /// Returns the child criteria aliases for a parent SQL alias and a child path. + /// + protected virtual IReadOnlyCollection GetChildAliases(string parentSqlAlias, string childPath) + { + return DefaultChildAliases; + } + /// /// Use an inner join if it is a non-null association and this /// is the "first" join in a series @@ -535,11 +665,21 @@ protected virtual bool IsJoinedFetchEnabled(IAssociationType type, FetchMode con return type.IsEntityType && IsJoinedFetchEnabledInMapping(config, type); } + // Since v5.2 + [Obsolete("Use or override the overload taking a pathAlias additional parameter")] protected virtual string GenerateTableAlias(int n, string path, IJoinable joinable) { return StringHelper.GenerateAlias(joinable.Name, n); } + protected virtual string GenerateTableAlias(int n, string path, string pathAlias, IJoinable joinable) + { + // 6.0 TODO: inline the call +#pragma warning disable 618 + return GenerateTableAlias(n, path, joinable); +#pragma warning restore 618 + } + protected virtual string GenerateRootAlias(string description) { return StringHelper.GenerateAlias(description, 0);