Skip to content

Commit a49c739

Browse files
committed
Support to join not associated entities in Criteria API (aka Entity Join)
1 parent 8d2b123 commit a49c739

15 files changed

+1140
-51
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: 20 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,7 @@ 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+
4546
private readonly string entityOrClassName;
4647

4748
// Projection Fields
@@ -71,7 +72,7 @@ public CriteriaImpl(string entityOrClassName, string alias, ISessionImplementor
7172
rootAlias = alias;
7273
subcriteriaByAlias[alias] = this;
7374
}
74-
75+
7576
public ISessionImplementor Session
7677
{
7778
get { return session; }
@@ -371,6 +372,11 @@ public ICriteria CreateAlias(string associationPath, string alias, JoinType join
371372
return this;
372373
}
373374

375+
public ICriteria CreateEntityCriteria(string alias, JoinType joinType, ICriterion withClause, string entityName)
376+
{
377+
return new Subcriteria(this, this, alias, alias, joinType, withClause, entityName);
378+
}
379+
374380
public ICriteria Add(ICriteria criteriaInst, ICriterion expression)
375381
{
376382
criteria.Add(new CriterionEntry(expression, criteriaInst));
@@ -647,14 +653,15 @@ public sealed partial class Subcriteria : ICriteria
647653
private readonly JoinType joinType;
648654
private ICriterion withClause;
649655

650-
internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string alias, JoinType joinType, ICriterion withClause)
656+
internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string alias, JoinType joinType, ICriterion withClause, string joinEntityName = null)
651657
{
652658
this.root = root;
653659
this.parent = parent;
654660
this.alias = alias;
655661
this.path = path;
656662
this.joinType = joinType;
657663
this.withClause = withClause;
664+
JoinEntityName = joinEntityName;
658665

659666
root.subcriteriaList.Add(this);
660667

@@ -668,6 +675,16 @@ internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, string al
668675
internal Subcriteria(CriteriaImpl root, ICriteria parent, string path, JoinType joinType)
669676
: this(root, parent, path, null, joinType) { }
670677

678+
/// <summary>
679+
/// Entity name for "Entity Join" - join for enitity with not mapped association
680+
/// </summary>
681+
public string JoinEntityName { get; }
682+
683+
/// <summary>
684+
/// Is this an Entity join for not mapped association
685+
/// </summary>
686+
public bool IsEntityJoin => JoinEntityName != null;
687+
671688
public ICriterion WithClause
672689
{
673690
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: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ public CriteriaJoinWalker(IOuterJoinLoadable persister, CriteriaQueryTranslator
7676
}
7777
}
7878

79+
protected override void AddAssociations()
80+
{
81+
base.AddAssociations();
82+
foreach (var entityJoinInfo in translator.GetEntityJoins().Values)
83+
{
84+
var tableAlias = translator.GetSQLAlias(entityJoinInfo.Criteria);
85+
var criteriaPath = entityJoinInfo.Criteria.Alias; //path for entity join is equal to alias
86+
var persister
87+
= entityJoinInfo.Persister as IOuterJoinLoadable;
88+
AddExplicitEntityJoinAssociation(persister, tableAlias, translator.GetJoinType(criteriaPath), GetWithClause(criteriaPath));
89+
IncludeInResultIfNeeded(persister, entityJoinInfo.Criteria, tableAlias);
90+
//collect mapped associations for entity join
91+
WalkEntityTree(persister, tableAlias, criteriaPath, 1);
92+
}
93+
}
94+
7995
protected override void WalkEntityTree(IOuterJoinLoadable persister, string alias, string path, int currentDepth)
8096
{
8197
// NH different behavior (NH-1476, NH-1760, NH-1785)
@@ -199,19 +215,7 @@ protected override string GenerateTableAlias(int n, string path, IJoinable joina
199215
ICriteria subcriteria = translator.GetCriteria(path);
200216
sqlAlias = subcriteria == null ? null : translator.GetSQLAlias(subcriteria);
201217

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-
}
218+
IncludeInResultIfNeeded(joinable, subcriteria, sqlAlias);
215219
}
216220

217221
if (sqlAlias == null)
@@ -220,6 +224,22 @@ protected override string GenerateTableAlias(int n, string path, IJoinable joina
220224
return sqlAlias;
221225
}
222226

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

224244
protected override string GenerateRootAlias(string tableName)
225245
{

0 commit comments

Comments
 (0)