Skip to content

Commit 0fd3b80

Browse files
committed
Query: Entity splitting support for regular entities
Part of #620
1 parent 50300fb commit 0fd3b80

22 files changed

+426
-65
lines changed

src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType
9494
return baseCondition;
9595
}
9696

97-
var table = entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
97+
var table = entityType.GetViewOrTableMappings().SingleOrDefault(e => e.IsSplitEntityTypePrincipal ?? true)?.Table
9898
?? entityType.GetDefaultMappings().Single().Table;
9999
if (table.IsOptional(entityType))
100100
{

src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
13201320
return propertyAccess;
13211321
}
13221322

1323-
var table = entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
1323+
var table = entityType.GetViewOrTableMappings().SingleOrDefault(e => e.IsSplitEntityTypePrincipal ?? true)?.Table
13241324
?? entityType.GetDefaultMappings().Single().Table;
13251325
if (!table.IsOptional(entityType))
13261326
{

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs

+11-11
Original file line numberDiff line numberDiff line change
@@ -320,26 +320,26 @@ private sealed class ColumnExpressionFindingExpressionVisitor : ExpressionVisito
320320
// Always skip the table of ColumnExpression since it will traverse into deeper subquery
321321
return columnExpression;
322322

323-
case LeftJoinExpression leftJoinExpression:
324-
var leftJoinTableAlias = leftJoinExpression.Table.Alias!;
323+
case PredicateJoinExpressionBase predicateJoinExpressionBase:
324+
var predicateJoinTableAlias = predicateJoinExpressionBase.Table.Alias!;
325325
// Visiting the join predicate will add some columns for join table.
326326
// But if all the referenced columns are in join predicate only then we can remove the join table.
327327
// So if there are no referenced columns yet means there is still potential to remove this table,
328328
// In such case we moved the columns encountered in join predicate to other dictionary and later merge
329329
// if there are more references to the join table outside of join predicate.
330-
// We currently do this only for LeftJoin since that is the only predicate join table we remove.
331330
// We should also remove references to the outer if this column gets removed then that subquery can also remove projections
332-
// But currently we only remove table for TPT scenario in which there are all table expressions which connects via joins.
333-
var joinOnSameLevel = _columnReferenced!.ContainsKey(leftJoinTableAlias);
334-
var noReferences = !joinOnSameLevel || _columnReferenced[leftJoinTableAlias] == null;
335-
base.Visit(leftJoinExpression);
331+
// But currently we only remove table for TPT & entity splitting scenario
332+
// in which there are all table expressions which connects via joins.
333+
var joinOnSameLevel = _columnReferenced!.ContainsKey(predicateJoinTableAlias);
334+
var noReferences = !joinOnSameLevel || _columnReferenced[predicateJoinTableAlias] == null;
335+
base.Visit(predicateJoinExpressionBase);
336336
if (noReferences && joinOnSameLevel)
337337
{
338-
_columnsUsedInJoinCondition![leftJoinTableAlias] = _columnReferenced[leftJoinTableAlias];
339-
_columnReferenced[leftJoinTableAlias] = null;
338+
_columnsUsedInJoinCondition![predicateJoinTableAlias] = _columnReferenced[predicateJoinTableAlias];
339+
_columnReferenced[predicateJoinTableAlias] = null;
340340
}
341341

342-
return leftJoinExpression;
342+
return predicateJoinExpressionBase;
343343

344344
default:
345345
return base.Visit(expression);
@@ -930,7 +930,7 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor
930930
};
931931
newSelectExpression._mutable = selectExpression._mutable;
932932

933-
newSelectExpression._tptLeftJoinTables.AddRange(selectExpression._tptLeftJoinTables);
933+
newSelectExpression._removableJoinTables.AddRange(selectExpression._removableJoinTables);
934934

935935
foreach (var kvp in selectExpression._tpcDiscriminatorValues)
936936
{

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs

+104-35
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public sealed partial class SelectExpression : TableExpressionBase
4545

4646
private readonly List<(ColumnExpression Column, ValueComparer Comparer)> _identifier = new();
4747
private readonly List<(ColumnExpression Column, ValueComparer Comparer)> _childIdentifiers = new();
48-
private readonly List<int> _tptLeftJoinTables = new();
48+
private readonly List<int> _removableJoinTables = new();
4949
private readonly Dictionary<TpcTablesExpression, (ColumnExpression, List<string>)> _tpcDiscriminatorValues
5050
= new(ReferenceEqualityComparer.Instance);
5151

@@ -165,7 +165,7 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre
165165
.Aggregate((l, r) => sqlExpressionFactory.AndAlso(l, r));
166166

167167
var joinExpression = new LeftJoinExpression(tableExpression, joinPredicate);
168-
_tptLeftJoinTables.Add(_tables.Count);
168+
_removableJoinTables.Add(_tables.Count);
169169
AddTable(joinExpression, tableReferenceExpression);
170170
}
171171

@@ -187,7 +187,7 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre
187187
if (entityTypes.Length == 1)
188188
{
189189
// For single entity case, we don't need discriminator.
190-
var table = GetTableBase(entityTypes[0]);
190+
var table = entityTypes[0].GetViewOrTableMappings().Single().Table;
191191
var tableExpression = new TableExpression(table);
192192

193193
var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!);
@@ -212,7 +212,7 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre
212212
}
213213
else
214214
{
215-
var tables = entityTypes.Select(e => GetTableBase(e)).ToArray();
215+
var tables = entityTypes.Select(e => e.GetViewOrTableMappings().Single().Table).ToArray();
216216
var properties = GetAllPropertiesInHierarchy(entityType).ToArray();
217217
var propertyNamesMap = new Dictionary<IProperty, string>();
218218
for (var i = 0; i < entityTypes.Length; i++)
@@ -314,47 +314,106 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre
314314
default:
315315
{
316316
// Also covers TPH
317-
ITableBase table;
318-
TableExpressionBase tableExpression;
319317
if (entityType.GetFunctionMappings().SingleOrDefault(e => e.IsDefaultFunctionMapping) is IFunctionMapping functionMapping)
320318
{
321319
var storeFunction = functionMapping.Table;
322320

323-
table = storeFunction;
324-
tableExpression = new TableValuedFunctionExpression((IStoreFunction)storeFunction, Array.Empty<SqlExpression>());
321+
GenerateNonHierarchyNonSplittingEntityType(
322+
storeFunction, new TableValuedFunctionExpression((IStoreFunction)storeFunction, Array.Empty<SqlExpression>()));
325323
}
326324
else
327325
{
328-
table = GetTableBase(entityType);
329-
tableExpression = new TableExpression(table);
330-
}
326+
var mappings = entityType.GetViewOrTableMappings().ToList();
327+
if (mappings.Count == 1)
328+
{
329+
var table = mappings[0].Table;
331330

332-
var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!);
333-
AddTable(tableExpression, tableReferenceExpression);
331+
GenerateNonHierarchyNonSplittingEntityType(table, new TableExpression(table));
332+
}
333+
else
334+
{
335+
// table splitting
336+
var keyProperties = entityType.FindPrimaryKey()!.Properties;
337+
List<ColumnExpression> joinColumns = default!;
338+
var columns = new Dictionary<IProperty, ColumnExpression>();
339+
var tableReferenceExpressionMap = new Dictionary<ITableBase, TableReferenceExpression>();
340+
foreach (var mapping in mappings)
341+
{
342+
var table = mapping.Table;
343+
var tableExpression = new TableExpression(table);
344+
var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias);
345+
tableReferenceExpressionMap[table] = tableReferenceExpression;
334346

335-
var propertyExpressions = new Dictionary<IProperty, ColumnExpression>();
336-
foreach (var property in GetAllPropertiesInHierarchy(entityType))
337-
{
338-
propertyExpressions[property] = CreateColumnExpression(property, table, tableReferenceExpression, nullable: false);
339-
}
347+
if (_tables.Count == 0)
348+
{
349+
AddTable(tableExpression, tableReferenceExpression);
350+
joinColumns = new List<ColumnExpression>();
351+
foreach (var property in keyProperties)
352+
{
353+
var columnExpression = CreateColumnExpression(property, table, tableReferenceExpression, nullable: false);
354+
columns[property] = columnExpression;
355+
joinColumns.Add(columnExpression);
356+
_identifier.Add((columnExpression, property.GetKeyValueComparer()));
357+
}
358+
}
359+
else
360+
{
361+
var innerColumns = keyProperties.Select(
362+
p => CreateColumnExpression(p, table, tableReferenceExpression, nullable: false));
340363

341-
var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions);
342-
_projectionMapping[new ProjectionMember()] = entityProjection;
364+
var joinPredicate = joinColumns.Zip(innerColumns, (l, r) => sqlExpressionFactory.Equal(l, r))
365+
.Aggregate((l, r) => sqlExpressionFactory.AndAlso(l, r));
343366

344-
var primaryKey = entityType.FindPrimaryKey();
345-
if (primaryKey != null)
346-
{
347-
foreach (var property in primaryKey.Properties)
348-
{
349-
_identifier.Add((propertyExpressions[property], property.GetKeyValueComparer()));
367+
var joinExpression = new InnerJoinExpression(tableExpression, joinPredicate);
368+
_removableJoinTables.Add(_tables.Count);
369+
AddTable(joinExpression, tableReferenceExpression);
370+
}
371+
}
372+
373+
foreach (var property in entityType.GetProperties())
374+
{
375+
if (property.IsPrimaryKey())
376+
{
377+
continue;
378+
}
379+
380+
var columnBase = mappings.Select(e => e.Table.FindColumn(property)).First(e => e != null)!;
381+
columns[property] = CreateColumnExpression(
382+
property, columnBase, tableReferenceExpressionMap[columnBase.Table], nullable: false);
383+
}
384+
385+
var entityProjection = new EntityProjectionExpression(entityType, columns);
386+
_projectionMapping[new ProjectionMember()] = entityProjection;
350387
}
351388
}
352389
}
353390

354391
break;
355392
}
356393

357-
static ITableBase GetTableBase(IEntityType entityType) => entityType.GetViewOrTableMappings().Single().Table;
394+
void GenerateNonHierarchyNonSplittingEntityType(ITableBase table, TableExpressionBase tableExpression)
395+
{
396+
var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!);
397+
AddTable(tableExpression, tableReferenceExpression);
398+
399+
var propertyExpressions = new Dictionary<IProperty, ColumnExpression>();
400+
foreach (var property in GetAllPropertiesInHierarchy(entityType))
401+
{
402+
propertyExpressions[property] = CreateColumnExpression(property, table, tableReferenceExpression, nullable: false);
403+
}
404+
405+
var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions);
406+
_projectionMapping[new ProjectionMember()] = entityProjection;
407+
408+
var primaryKey = entityType.FindPrimaryKey();
409+
if (primaryKey != null)
410+
{
411+
foreach (var property in primaryKey.Properties)
412+
{
413+
_identifier.Add((propertyExpressions[property], property.GetKeyValueComparer()));
414+
}
415+
}
416+
}
358417

359418
static ITableBase GetTableBaseFiltered(IEntityType entityType, List<ITableBase> existingTables)
360419
=> entityType.GetViewOrTableMappings().Single(m => !existingTables.Contains(m.Table)).Table;
@@ -1713,8 +1772,8 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi
17131772
_projectionMapping.Clear();
17141773
select1._identifier.AddRange(_identifier);
17151774
_identifier.Clear();
1716-
select1._tptLeftJoinTables.AddRange(_tptLeftJoinTables);
1717-
_tptLeftJoinTables.Clear();
1775+
select1._removableJoinTables.AddRange(_removableJoinTables);
1776+
_removableJoinTables.Clear();
17181777
foreach (var kvp in _tpcDiscriminatorValues)
17191778
{
17201779
select1._tpcDiscriminatorValues[kvp.Key] = kvp.Value;
@@ -2902,8 +2961,8 @@ private SqlRemappingVisitor PushdownIntoSubqueryInternal()
29022961
Having = null;
29032962
Offset = null;
29042963
Limit = null;
2905-
subquery._tptLeftJoinTables.AddRange(_tptLeftJoinTables);
2906-
_tptLeftJoinTables.Clear();
2964+
subquery._removableJoinTables.AddRange(_removableJoinTables);
2965+
_removableJoinTables.Clear();
29072966
foreach (var kvp in _tpcDiscriminatorValues)
29082967
{
29092968
subquery._tpcDiscriminatorValues[kvp.Key] = kvp.Value;
@@ -3213,14 +3272,17 @@ private SelectExpression Prune(IReadOnlyCollection<string>? referencedColumns)
32133272
var columnExpressionFindingExpressionVisitor = new ColumnExpressionFindingExpressionVisitor();
32143273
var columnsMap = columnExpressionFindingExpressionVisitor.FindColumns(this);
32153274
var removedTableCount = 0;
3275+
// Start at 1 because we don't drop main table.
3276+
// Dropping main table is more complex because other tables need to unwrap joins to be main
32163277
for (var i = 0; i < _tables.Count; i++)
32173278
{
32183279
var table = _tables[i];
32193280
var tableAlias = GetAliasFromTableExpressionBase(table);
32203281
if (columnsMap[tableAlias] == null
32213282
&& (table is LeftJoinExpression
3222-
|| table is OuterApplyExpression)
3223-
&& _tptLeftJoinTables?.Contains(i + removedTableCount) == true)
3283+
|| table is OuterApplyExpression
3284+
|| table is InnerJoinExpression) // This is only valid for removable join table which are from entity splitting
3285+
&& _removableJoinTables?.Contains(i + removedTableCount) == true)
32243286
{
32253287
_tables.RemoveAt(i);
32263288
_tableReferences.RemoveAt(i);
@@ -3341,7 +3403,14 @@ private static ConcreteColumnExpression CreateColumnExpression(
33413403
ITableBase table,
33423404
TableReferenceExpression tableExpression,
33433405
bool nullable)
3344-
=> new(property, table.FindColumn(property)!, tableExpression, nullable);
3406+
=> CreateColumnExpression(property, table.FindColumn(property)!, tableExpression, nullable);
3407+
3408+
private static ConcreteColumnExpression CreateColumnExpression(
3409+
IProperty property,
3410+
IColumnBase columnBase,
3411+
TableReferenceExpression tableExpression,
3412+
bool nullable)
3413+
=> new(property, columnBase, tableExpression, nullable);
33453414

33463415
private ConcreteColumnExpression GenerateOuterColumn(
33473416
TableReferenceExpression tableReferenceExpression,
@@ -3578,7 +3647,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
35783647
_usedAliases = _usedAliases,
35793648
};
35803649
newSelectExpression._mutable = false;
3581-
newSelectExpression._tptLeftJoinTables.AddRange(_tptLeftJoinTables);
3650+
newSelectExpression._removableJoinTables.AddRange(_removableJoinTables);
35823651
foreach (var kvp in newTpcDiscriminatorValues)
35833652
{
35843653
newSelectExpression._tpcDiscriminatorValues[kvp.Key] = kvp.Value;

test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ protected EntitySplittingTestBase(ITestOutputHelper testOutputHelper)
1111
//TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
1212
}
1313

14-
[ConditionalFact(Skip = "Entity splitting query Issue #620")]
14+
[ConditionalFact]
1515
public virtual async Task Can_roundtrip()
1616
{
17-
await InitializeAsync(OnModelCreating, sensitiveLogEnabled: false);
17+
await InitializeAsync(OnModelCreating, sensitiveLogEnabled: true);
1818

1919
await using (var context = CreateContext())
2020
{

0 commit comments

Comments
 (0)