Skip to content

Commit aaaf9fc

Browse files
committed
Support to join not associated entities in Criteria API (aka Entity Join)
1 parent 956bffe commit aaaf9fc

15 files changed

+1178
-53
lines changed

src/NHibernate.Test/Async/Criteria/EntityJoinCriteriaTest.cs

Lines changed: 431 additions & 0 deletions
Large diffs are not rendered by default.

src/NHibernate.Test/Criteria/EntityJoinCriteriaTest.cs

Lines changed: 433 additions & 0 deletions
Large diffs are not rendered by default.

src/NHibernate/Async/Impl/CriteriaImpl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
namespace NHibernate.Impl
2323
{
2424
using System.Threading.Tasks;
25-
public partial class CriteriaImpl : ICriteria
25+
public partial class CriteriaImpl : ICriteria, ISupportEntityJoinCriteria
2626
{
2727

2828
public async Task<IList> ListAsync(CancellationToken cancellationToken = default(CancellationToken))
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using System.Linq.Expressions;
3+
using NHibernate.SqlCommand;
4+
5+
namespace NHibernate.Criterion
6+
{
7+
//TODO: Make interface more flexible for changes (maybe it should take only 2 params alias + EntityJoinConfig)
8+
public interface ISupportEntityJoinQueryOver
9+
{
10+
void JoinEntityAlias<U>(Expression<Func<U>> alias, JoinType joinType, ICriterion withClause, string entityName);
11+
}
12+
13+
public interface ISupportEntityJoinQueryOver<TRoot>: ISupportEntityJoinQueryOver
14+
{
15+
IQueryOver<TRoot, U> JoinEntityQueryOver<U>(Expression<Func<U>> alias, JoinType joinType, ICriterion withClause, string entityName);
16+
}
17+
}

src/NHibernate/Criterion/QueryOver.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ internal static Exception GetDirectUsageException()
6565
[Serializable]
6666
public abstract partial class QueryOver<TRoot> : QueryOver, IQueryOver<TRoot>
6767
{
68+
protected internal QueryOver<TRoot, U> Create<U>(ICriteria criteria)
69+
{
70+
return new QueryOver<TRoot, U>(impl, criteria);
71+
}
6872

6973
private IList<TRoot> List()
7074
{
@@ -280,7 +284,8 @@ IQueryOver<TRoot> IQueryOver<TRoot>.ReadOnly()
280284
/// Implementation of the <see cref="IQueryOver&lt;TRoot, TSubType&gt;"/> interface
281285
/// </summary>
282286
[Serializable]
283-
public class QueryOver<TRoot,TSubType> : QueryOver<TRoot>, IQueryOver<TRoot,TSubType>
287+
public class QueryOver<TRoot,TSubType> : QueryOver<TRoot>, IQueryOver<TRoot,TSubType>,
288+
ISupportEntityJoinQueryOver<TRoot>
284289
{
285290

286291
protected internal QueryOver()
@@ -658,6 +663,11 @@ public QueryOver<TRoot,U> JoinQueryOver<U>(Expression<Func<IEnumerable<U>>> path
658663
joinType));
659664
}
660665

666+
public QueryOver<TRoot,U> JoinEntityQueryOver<U>(Expression<Func<U>> alias, JoinType joinType, ICriterion withClause, string entityName)
667+
{
668+
return Create<U>(CreateEntityCriteria(alias, joinType, withClause, entityName));
669+
}
670+
661671
public QueryOver<TRoot,TSubType> JoinAlias(Expression<Func<TSubType, object>> path, Expression<Func<object>> alias)
662672
{
663673
return AddAlias(
@@ -758,6 +768,11 @@ private QueryOver<TRoot,TSubType> AddAlias(string path, string alias, JoinType j
758768
return this;
759769
}
760770

771+
private ICriteria CreateEntityCriteria<U>(Expression<Func<U>> alias, JoinType joinType, ICriterion withClause, string entityName)
772+
{
773+
return criteria.CreateEntityCriteria(ExpressionProcessor.FindMemberExpression(alias.Body), joinType, withClause, entityName ?? typeof(U).FullName);
774+
}
775+
761776
private QueryOver<TRoot,TSubType> Add(Expression<Func<TSubType, bool>> expression)
762777
{
763778
criteria.Add(ExpressionProcessor.ProcessExpression<TSubType>(expression));
@@ -974,6 +989,17 @@ IQueryOver<TRoot,TSubType> IQueryOver<TRoot,TSubType>.JoinAlias<U>(Expression<Fu
974989
IQueryOver<TRoot,TSubType> IQueryOver<TRoot,TSubType>.JoinAlias<U>(Expression<Func<IEnumerable<U>>> path, Expression<Func<U>> alias, JoinType joinType, ICriterion withClause)
975990
{ return JoinAlias(path, alias, joinType, withClause); }
976991

992+
IQueryOver<TRoot, U> ISupportEntityJoinQueryOver<TRoot>.JoinEntityQueryOver<U>(Expression<Func<U>> alias, JoinType joinType, ICriterion withClause, string entityName)
993+
{
994+
return JoinEntityQueryOver(alias, joinType, withClause, entityName);
995+
}
996+
997+
//6.0 TODO: to remove and merge with extension EntityJoinExtensions.JoinEntityAlias
998+
void ISupportEntityJoinQueryOver.JoinEntityAlias<U>(Expression<Func<U>> alias, JoinType joinType, ICriterion withClause, string entityName)
999+
{
1000+
CreateEntityCriteria(alias, joinType, withClause, entityName);
1001+
}
1002+
9771003
IQueryOverJoinBuilder<TRoot,TSubType> IQueryOver<TRoot,TSubType>.Inner
9781004
{ get { return new IQueryOverJoinBuilder<TRoot,TSubType>(this, JoinType.InnerJoin); } }
9791005

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Linq.Expressions;
3+
using NHibernate.Criterion;
4+
using NHibernate.Impl;
5+
using NHibernate.SqlCommand;
6+
7+
namespace NHibernate
8+
{
9+
public static class EntityJoinExtensions
10+
{
11+
// 6.0 TODO: merge into 'IQueryOver<TType, TSubType>
12+
public static TThis JoinEntityAlias<TThis, TAlias>(this TThis queryOver, Expression<Func<TAlias>> alias, JoinType joinType, ICriterion withClause, string entityName = null) where TThis : IQueryOver
13+
{
14+
var q = CastOrThrow<ISupportEntityJoinQueryOver>(queryOver);
15+
q.JoinEntityAlias(alias, joinType, withClause, entityName);
16+
return queryOver;
17+
}
18+
19+
public static IQueryOver<TRoot, U> JoinEntityQueryOver<TRoot, U>(this IQueryOver<TRoot> queryOver, Expression<Func<U>> alias, JoinType joinType, ICriterion withClause, string entityName = null)
20+
{
21+
var q = CastOrThrow<ISupportEntityJoinQueryOver<TRoot>>(queryOver);
22+
return q.JoinEntityQueryOver(alias, joinType, withClause, entityName);
23+
}
24+
25+
// 6.0 TODO: merge into 'ICriteria'
26+
public static ICriteria CreateEntityAlias(this ICriteria criteria, string alias, JoinType joinType, ICriterion withClause, string entityName)
27+
{
28+
CreateEntityCriteria(criteria, alias, joinType, withClause, entityName);
29+
return criteria;
30+
}
31+
32+
public static ICriteria CreateEntityCriteria(this ICriteria criteria, string alias, JoinType joinType, ICriterion withClause, string entityName)
33+
{
34+
var c = CastOrThrow<ISupportEntityJoinCriteria>(criteria);
35+
return c.CreateEntityCriteria(alias, joinType, withClause, entityName);
36+
}
37+
38+
private static T CastOrThrow<T>(object obj) where T: class
39+
{
40+
return obj as T
41+
?? throw new ArgumentException($"{obj.GetType().FullName} requires to implement {nameof(T)} interface to support explicit entity joins.");
42+
}
43+
}
44+
}

src/NHibernate/Impl/CriteriaImpl.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace NHibernate.Impl
1515
/// Implementation of the <see cref="ICriteria"/> interface
1616
/// </summary>
1717
[Serializable]
18-
public partial class CriteriaImpl : ICriteria
18+
public partial class CriteriaImpl : ICriteria, ISupportEntityJoinCriteria
1919
{
2020
private readonly System.Type persistentClass;
2121
private readonly List<CriterionEntry> criteria = new List<CriterionEntry>();
@@ -42,6 +42,9 @@ public partial class CriteriaImpl : ICriteria
4242

4343
private readonly Dictionary<string, ICriteria> subcriteriaByPath = new Dictionary<string, ICriteria>();
4444
private readonly Dictionary<string, ICriteria> subcriteriaByAlias = new Dictionary<string, ICriteria>();
45+
46+
private readonly Dictionary<string,string> entityJoinAliasToEntityNameMap = new Dictionary<string, string>();
47+
4548
private readonly string entityOrClassName;
4649

4750
// Projection Fields
@@ -71,6 +74,11 @@ public CriteriaImpl(string entityOrClassName, string alias, ISessionImplementor
7174
rootAlias = alias;
7275
subcriteriaByAlias[alias] = this;
7376
}
77+
78+
public IDictionary<string,string> GetEntityJoinAliasToEntityNameMap()
79+
{
80+
return entityJoinAliasToEntityNameMap;
81+
}
7482

7583
public ISessionImplementor Session
7684
{
@@ -371,6 +379,11 @@ public ICriteria CreateAlias(string associationPath, string alias, JoinType join
371379
return this;
372380
}
373381

382+
public ICriteria CreateEntityCriteria(string alias, JoinType joinType, ICriterion withClause, string entityName)
383+
{
384+
return new Subcriteria(this, this, alias, alias, joinType, withClause, entityName);
385+
}
386+
374387
public ICriteria Add(ICriteria criteriaInst, ICriterion expression)
375388
{
376389
criteria.Add(new CriterionEntry(expression, criteriaInst));
@@ -647,18 +660,23 @@ public sealed partial class Subcriteria : ICriteria
647660
private readonly JoinType joinType;
648661
private ICriterion withClause;
649662

650-
internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string alias, JoinType joinType, ICriterion withClause)
663+
internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string alias, JoinType joinType, ICriterion withClause, string joinEntityName = null)
651664
{
652665
this.root = root;
653666
this.parent = parent;
654667
this.alias = alias;
655668
this.path = path;
656669
this.joinType = joinType;
657670
this.withClause = withClause;
671+
JoinEntityName = joinEntityName;
658672

659673
root.subcriteriaList.Add(this);
660674

661-
root.subcriteriaByPath[path] = this;
675+
if (JoinEntityName == null)
676+
root.subcriteriaByPath[path] = this;
677+
else
678+
root.entityJoinAliasToEntityNameMap[alias] = JoinEntityName;
679+
662680
SetAlias(alias);
663681
}
664682

@@ -668,6 +686,16 @@ internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string al
668686
internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, JoinType joinType)
669687
: this(root, parent, path, null, joinType) { }
670688

689+
/// <summary>
690+
/// Entity name for "Entity Join" - join for enitity with not mapped association
691+
/// </summary>
692+
public string JoinEntityName { get; }
693+
694+
/// <summary>
695+
/// Is this an Entity join for not mapped association
696+
/// </summary>
697+
public bool IsEntityJoin => JoinEntityName != null;
698+
671699
public ICriterion WithClause
672700
{
673701
get { return withClause; }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using NHibernate.Criterion;
2+
using NHibernate.SqlCommand;
3+
4+
namespace NHibernate.Impl
5+
{
6+
public interface ISupportEntityJoinCriteria
7+
{
8+
//TODO: Make interface more flexible for changes (maybe it should be as simple as CreateEntityCriteria(EntityJoinConfig join)
9+
ICriteria CreateEntityCriteria(string alias, JoinType joinType, ICriterion withClause, string entityName);
10+
}
11+
}

src/NHibernate/Loader/AbstractEntityJoinWalker.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public AbstractEntityJoinWalker(string rootSqlAlias, IOuterJoinLoadable persiste
3131

3232
protected virtual void InitAll(SqlString whereString, SqlString orderByString, LockMode lockMode)
3333
{
34-
WalkEntityTree(persister, Alias);
34+
AddAssociations();
3535
IList<OuterJoinableAssociation> allAssociations = new List<OuterJoinableAssociation>(associations);
3636
allAssociations.Add(CreateAssociation(persister.EntityType, alias));
3737

@@ -48,7 +48,7 @@ protected void InitProjection(SqlString projectionString, SqlString whereString,
4848

4949
protected void InitProjection(SqlString projectionString, SqlString whereString, SqlString orderByString, SqlString groupByString, SqlString havingString, IDictionary<string, IFilter> enabledFilters, LockMode lockMode, IList<EntityProjection> entityProjections)
5050
{
51-
WalkEntityTree(persister, Alias);
51+
AddAssociations();
5252

5353
int countEntities = entityProjections.Count;
5454
if (countEntities > 0)
@@ -81,6 +81,11 @@ protected void InitProjection(SqlString projectionString, SqlString whereString,
8181
InitStatementString(projectionString, whereString, orderByString, groupByString, havingString, lockMode);
8282
}
8383

84+
protected virtual void AddAssociations()
85+
{
86+
WalkEntityTree(persister, Alias);
87+
}
88+
8489
private OuterJoinableAssociation CreateAssociation(EntityType entityType, string tableAlias)
8590
{
8691
return new OuterJoinableAssociation(

src/NHibernate/Loader/Criteria/CriteriaJoinWalker.cs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ public CriteriaJoinWalker(IOuterJoinLoadable persister, CriteriaQueryTranslator
7676
}
7777
}
7878

79+
protected override void AddAssociations()
80+
{
81+
base.AddAssociations();
82+
foreach (var x in translator.GetEntityJoins())
83+
{
84+
CriteriaQueryTranslator.EntityJoinInfo info = x.Value;
85+
AddExplicitEntityJoinAssociation(info.Persister, info.TableAlias, info.JoinType, info.WithClause);
86+
IncludeInResultIfNeeded(info.Persister, info.Criteria, info.TableAlias);
87+
//collect mapped associations for entity join
88+
WalkEntityTree(info.Persister, info.TableAlias, info.Criteria.Alias, 1);
89+
}
90+
}
91+
7992
protected override void WalkEntityTree(IOuterJoinLoadable persister, string alias, string path, int currentDepth)
8093
{
8194
// NH different behavior (NH-1476, NH-1760, NH-1785)
@@ -199,19 +212,7 @@ protected override string GenerateTableAlias(int n, string path, IJoinable joina
199212
ICriteria subcriteria = translator.GetCriteria(path);
200213
sqlAlias = subcriteria == null ? null : translator.GetSQLAlias(subcriteria);
201214

202-
if (joinable.ConsumesEntityAlias() && !translator.HasProjection)
203-
{
204-
includeInResultRowList.Add(subcriteria != null && subcriteria.Alias != null);
205-
206-
if (sqlAlias != null)
207-
{
208-
if (subcriteria.Alias != null)
209-
{
210-
userAliasList.Add(subcriteria.Alias); //alias may be null
211-
resultTypeList.Add(translator.ResultType(subcriteria));
212-
}
213-
}
214-
}
215+
IncludeInResultIfNeeded(joinable, subcriteria, sqlAlias);
215216
}
216217

217218
if (sqlAlias == null)
@@ -220,6 +221,22 @@ protected override string GenerateTableAlias(int n, string path, IJoinable joina
220221
return sqlAlias;
221222
}
222223

224+
private void IncludeInResultIfNeeded(IJoinable joinable, ICriteria subcriteria, string sqlAlias)
225+
{
226+
if (joinable.ConsumesEntityAlias() && !translator.HasProjection)
227+
{
228+
includeInResultRowList.Add(subcriteria != null && subcriteria.Alias != null);
229+
230+
if (sqlAlias != null)
231+
{
232+
if (subcriteria.Alias != null)
233+
{
234+
userAliasList.Add(subcriteria.Alias); //alias may be null
235+
resultTypeList.Add(translator.ResultType(subcriteria));
236+
}
237+
}
238+
}
239+
}
223240

224241
protected override string GenerateRootAlias(string tableName)
225242
{

0 commit comments

Comments
 (0)