Skip to content

Commit

Permalink
Query: Optimize usage of GetViewOrTableMappings (#28019)
Browse files Browse the repository at this point in the history
Resolves #23572

Following rules define if given entity type is a potential candidate for optional dependent in table sharing case
- A keyless entity cannot be in table sharing
- Only root type of hierarchy can be dependent
- The dependent can be TPH/TPT (or no hierarchy) but no TPC
- Principal can be any type except non-leaf type in TPC (this is only applied in model validation, no impact on query)

If above conditions indicate it is optional dependent then we need to find the ITableBase it is mapped to find if it is optional dependent
We find ITableBase for IEntityType (which is also root type)
- For new generated SelectExpression, first table's ITableBase is the mapping table (regardless of type of TableExpressionBase, if no mapping then we use default)
- For other scenario the single mapped view/table mapping for the entity type (because of root type, there will only be one mapping) or default mapping in case entity type is not mapped to any view/table

Once we get table and verify it is optional dependent,
We use required non-PK columns which are not shared with principal entity types to add check for existence. This is necessary and sufficient condition for existence
  • Loading branch information
smitpatel committed May 13, 2022
1 parent cf92261 commit 1d47398
Show file tree
Hide file tree
Showing 16 changed files with 3,984 additions and 7,529 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ public static IReadOnlyList<string> GetTptDiscriminatorValues(this IReadOnlyEnti
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static IReadOnlyList<IProperty> GetNonPrincipalSharedNonPkProperties(this IEntityType entityType, ITableBase table)
public static IEnumerable<IProperty> GetNonPrincipalSharedNonPkProperties(this IEntityType entityType, ITableBase table)
{
var nonPrincipalSharedProperties = new List<IProperty>();
var principalEntityTypes = new HashSet<IEntityType>();
PopulatePrincipalEntityTypes(table, entityType, principalEntityTypes);
foreach (var property in entityType.GetProperties())
Expand All @@ -65,11 +64,9 @@ public static IReadOnlyList<IProperty> GetNonPrincipalSharedNonPkProperties(this
continue;
}

nonPrincipalSharedProperties.Add(property);
yield return property;
}

return nonPrincipalSharedProperties;

static void PopulatePrincipalEntityTypes(ITableBase table, IEntityType entityType, HashSet<IEntityType> entityTypes)
{
foreach (var linkingFk in table.GetRowInternalForeignKeys(entityType))
Expand Down
70 changes: 25 additions & 45 deletions src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ protected RelationalEntityShaperExpression(
protected override LambdaExpression GenerateMaterializationCondition(IEntityType entityType, bool nullable)
{
LambdaExpression baseCondition;
if (entityType.FindDiscriminatorProperty() == null
// Generate discriminator condition
var containsDiscriminatorProperty = entityType.FindDiscriminatorProperty() != null;
if (!containsDiscriminatorProperty
&& entityType.GetDirectlyDerivedTypes().Any())
{
// TPT
// TPT/TPC
var valueBufferParameter = Parameter(typeof(ValueBuffer));
var discriminatorValueVariable = Variable(typeof(string), "discriminator");
var expressions = new List<Expression>
Expand Down Expand Up @@ -84,51 +86,29 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType
baseCondition = base.GenerateMaterializationCondition(entityType, nullable);
}

if (entityType.FindPrimaryKey() != null)
if (containsDiscriminatorProperty
|| entityType.FindPrimaryKey() == null
|| entityType.GetRootType() != entityType
|| entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy)
{
var table = entityType.GetViewOrTableMappings().FirstOrDefault()?.Table;
if (table != null
&& table.IsOptional(entityType))
{
// Optional dependent
var body = baseCondition.Body;
var valueBufferParameter = baseCondition.Parameters[0];
Expression? condition = null;
var requiredNonPkProperties = entityType.GetProperties().Where(p => !p.IsNullable && !p.IsPrimaryKey()).ToList();
if (requiredNonPkProperties.Count > 0)
{
condition = requiredNonPkProperties
.Select(
p => NotEqual(
valueBufferParameter.CreateValueBufferReadValueExpression(typeof(object), p.GetIndex(), p),
Constant(null)))
.Aggregate((a, b) => AndAlso(a, b));
}

var allNonPrincipalSharedNonPkProperties = entityType.GetNonPrincipalSharedNonPkProperties(table);
// We don't need condition for nullable property if there exist at least one required property which is non shared.
if (allNonPrincipalSharedNonPkProperties.Count != 0
&& allNonPrincipalSharedNonPkProperties.All(p => p.IsNullable))
{
var atLeastOneNonNullValueInNullablePropertyCondition = allNonPrincipalSharedNonPkProperties
.Select(
p => NotEqual(
valueBufferParameter.CreateValueBufferReadValueExpression(typeof(object), p.GetIndex(), p),
Constant(null)))
.Aggregate((a, b) => OrElse(a, b));

condition = condition == null
? atLeastOneNonNullValueInNullablePropertyCondition
: AndAlso(condition, atLeastOneNonNullValueInNullablePropertyCondition);
}

if (condition != null)
{
body = Condition(condition, body, Default(typeof(IEntityType)));
}
return baseCondition;
}

return Lambda(body, valueBufferParameter);
}
var table = entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
?? entityType.GetDefaultMappings().Single().Table;
if (table.IsOptional(entityType))
{
// Optional dependent
var valueBufferParameter = baseCondition.Parameters[0];
var condition = entityType.GetNonPrincipalSharedNonPkProperties(table)
.Where(e => !e.IsNullable)
.Select(
p => NotEqual(
valueBufferParameter.CreateValueBufferReadValueExpression(typeof(object), p.GetIndex(), p),
Constant(null)))
.Aggregate((a, b) => AndAlso(a, b));

return Lambda(Condition(condition, baseCondition.Body, Default(typeof(IEntityType))), valueBufferParameter);
}

return baseCondition;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
{
var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
entityProjectionExpression,
targetEntityType.GetViewOrTableMappings().Single().Table,
navigation);

var innerShapedQuery = CreateShapedQueryExpression(
Expand Down Expand Up @@ -1137,7 +1138,7 @@ outerKey is NewArrayExpression newArrayExpression
if (innerShaper == null)
{
// Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630
// So there is no handling for dependent having TPT
// So there is no handling for dependent having TPT/TPC
// If navigation is defined on derived type and entity type is part of TPT then we need to get ITableBase for derived type.
// TODO: The following code should also handle Function and SqlQuery mappings
var table = navigation.DeclaringEntityType.BaseType == null
Expand Down Expand Up @@ -1177,6 +1178,7 @@ outerKey is NewArrayExpression newArrayExpression
table = targetEntityType.GetViewOrTableMappings().Single().Table;
var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
entityProjectionExpression,
table,
navigation);

var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression);
Expand Down Expand Up @@ -1237,6 +1239,7 @@ outerKey is NewArrayExpression newArrayExpression

SelectExpression BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
EntityProjectionExpression entityProjectionExpression,
ITableBase targetTable,
INavigation navigation)
{
// just need any column - we use it only to extract the table it originated from
Expand All @@ -1247,7 +1250,7 @@ SelectExpression BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
: foreignKey.PrincipalKey.Properties[0]);

var sourceTable = FindRootTableExpressionForColumn(sourceColumn);
var ownedTable = new TableExpression(targetEntityType.GetTableMappings().Single().Table);
var ownedTable = new TableExpression(targetTable);

foreach (var annotation in sourceTable.GetAnnotations())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1093,51 +1093,35 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
var propertyAccess = entityProjectionExpression.BindProperty(property);

var entityType = entityReferenceExpression.EntityType;
var table = entityType.GetViewOrTableMappings().FirstOrDefault()?.Table;
if ((table?.IsOptional(entityType)) != true)
if (entityType.FindDiscriminatorProperty() != null
|| entityType.FindPrimaryKey() == null
|| entityType.GetRootType() != entityType
|| entityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy)
{
return propertyAccess;
}

// this is optional dependent sharing table
var nonPrincipalSharedNonPkProperties = entityType.GetNonPrincipalSharedNonPkProperties(table);
if (nonPrincipalSharedNonPkProperties.Contains(property))
var table = entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
?? entityType.GetDefaultMappings().Single().Table;
if (!table.IsOptional(entityType))
{
// The column is not being shared with principal side so we can always use directly
return propertyAccess;
}

SqlExpression? condition = null;
// Property is being shared with principal side, so we need to make it conditional access
var allRequiredNonPkPropertiesCondition =
entityType.GetProperties().Where(p => !p.IsNullable && !p.IsPrimaryKey()).ToList();
if (allRequiredNonPkPropertiesCondition.Count > 0)
{
condition = allRequiredNonPkPropertiesCondition.Select(p => entityProjectionExpression.BindProperty(p))
.Select(c => (SqlExpression)_sqlExpressionFactory.NotEqual(c, _sqlExpressionFactory.Constant(null)))
.Aggregate((a, b) => _sqlExpressionFactory.AndAlso(a, b));
}

if (nonPrincipalSharedNonPkProperties.Count != 0
&& nonPrincipalSharedNonPkProperties.All(p => p.IsNullable))
{
// If all non principal shared properties are nullable then we need additional condition
var atLeastOneNonNullValueInNullableColumnsCondition = nonPrincipalSharedNonPkProperties
.Select(p => entityProjectionExpression.BindProperty(p))
.Select(c => (SqlExpression)_sqlExpressionFactory.NotEqual(c, _sqlExpressionFactory.Constant(null)))
.Aggregate((a, b) => _sqlExpressionFactory.OrElse(a, b));

condition = condition == null
? atLeastOneNonNullValueInNullableColumnsCondition
: _sqlExpressionFactory.AndAlso(condition, atLeastOneNonNullValueInNullableColumnsCondition);
}

if (condition == null)
// this is optional dependent sharing table
var nonPrincipalSharedNonPkProperties = entityType.GetNonPrincipalSharedNonPkProperties(table).ToList();
if (nonPrincipalSharedNonPkProperties.Contains(property))
{
// if we cannot compute condition then we just return property access (and hope for the best)
// The column is not being shared with principal side so we can always use directly
return propertyAccess;
}

var condition = nonPrincipalSharedNonPkProperties
.Where(e => !e.IsNullable)
.Select(p => entityProjectionExpression.BindProperty(p))
.Select(c => (SqlExpression)_sqlExpressionFactory.NotEqual(c, _sqlExpressionFactory.Constant(null)))
.Aggregate((a, b) => _sqlExpressionFactory.AndAlso(a, b));

return _sqlExpressionFactory.Case(
new List<CaseWhenClause> { new(condition, propertyAccess) },
elseResult: null);
Expand Down Expand Up @@ -1192,7 +1176,7 @@ private bool ProcessOrderByThenBy(
{
enumerableExpression.ApplyOrdering(orderingExpression);
}

return true;
}

Expand Down Expand Up @@ -1406,36 +1390,29 @@ private bool TryRewriteEntityEquality(
|| IsNullSqlConstantExpression(right))
{
var nonNullEntityReference = (IsNullSqlConstantExpression(left) ? rightEntityReference : leftEntityReference)!;
var entityType1 = nonNullEntityReference.EntityType;
var table = entityType1.GetViewOrTableMappings().FirstOrDefault()?.Table;
if (table?.IsOptional(entityType1) == true)
var nullComparedEntityType = nonNullEntityReference.EntityType;
var nullComparedEntityTypePrimaryKeyProperties = nullComparedEntityType.FindPrimaryKey()?.Properties;
if (nullComparedEntityTypePrimaryKeyProperties == null)
{
Expression? condition = null;
// Optional dependent sharing table
var requiredNonPkProperties = entityType1.GetProperties().Where(p => !p.IsNullable && !p.IsPrimaryKey()).ToList();
if (requiredNonPkProperties.Count > 0)
{
condition = requiredNonPkProperties.Select(
p =>
{
var comparison = Expression.Call(
ObjectEqualsMethodInfo,
Expression.Convert(CreatePropertyAccessExpression(nonNullEntityReference, p), typeof(object)),
Expression.Convert(Expression.Constant(null, p.ClrType.MakeNullable()), typeof(object)));
return nodeType == ExpressionType.Equal
? (Expression)comparison
: Expression.Not(comparison);
})
.Aggregate((l, r) => nodeType == ExpressionType.Equal ? Expression.OrElse(l, r) : Expression.AndAlso(l, r));
}
throw new InvalidOperationException(
CoreStrings.EntityEqualityOnKeylessEntityNotSupported(
nodeType == ExpressionType.Equal
? equalsMethod ? nameof(object.Equals) : "=="
: equalsMethod
? "!" + nameof(object.Equals)
: "!=",
nullComparedEntityType.DisplayName()));
}

var allNonPrincipalSharedNonPkProperties = entityType1.GetNonPrincipalSharedNonPkProperties(table);
// We don't need condition for nullable property if there exist at least one required property which is non shared.
if (allNonPrincipalSharedNonPkProperties.Count != 0
&& allNonPrincipalSharedNonPkProperties.All(p => p.IsNullable))
if (nullComparedEntityType.GetRootType() == nullComparedEntityType
&& nullComparedEntityType.GetMappingStrategy() != RelationalAnnotationNames.TpcMappingStrategy)
{
var table = nullComparedEntityType.GetViewOrTableMappings().SingleOrDefault()?.Table
?? nullComparedEntityType.GetDefaultMappings().Single().Table;
if (table.IsOptional(nullComparedEntityType))
{
var atLeastOneNonNullValueInNullablePropertyCondition = allNonPrincipalSharedNonPkProperties
var condition = nullComparedEntityType.GetNonPrincipalSharedNonPkProperties(table)
.Where(e => !e.IsNullable)
.Select(
p =>
{
Expand All @@ -1448,40 +1425,15 @@ private bool TryRewriteEntityEquality(
? (Expression)comparison
: Expression.Not(comparison);
})
.Aggregate((l, r) => nodeType == ExpressionType.Equal ? Expression.AndAlso(l, r) : Expression.OrElse(l, r));

condition = condition == null
? atLeastOneNonNullValueInNullablePropertyCondition
: nodeType == ExpressionType.Equal
? Expression.OrElse(condition, atLeastOneNonNullValueInNullablePropertyCondition)
: Expression.AndAlso(condition, atLeastOneNonNullValueInNullablePropertyCondition);
}
.Aggregate((l, r) => nodeType == ExpressionType.Equal ? Expression.OrElse(l, r) : Expression.AndAlso(l, r));

if (condition != null)
{
result = Visit(condition);
return true;
}

result = null;
return false;
}

var primaryKeyProperties1 = entityType1.FindPrimaryKey()?.Properties;
if (primaryKeyProperties1 == null)
{
throw new InvalidOperationException(
CoreStrings.EntityEqualityOnKeylessEntityNotSupported(
nodeType == ExpressionType.Equal
? equalsMethod ? nameof(object.Equals) : "=="
: equalsMethod
? "!" + nameof(object.Equals)
: "!=",
entityType1.DisplayName()));
}

result = Visit(
primaryKeyProperties1.Select(
nullComparedEntityTypePrimaryKeyProperties.Select(
p =>
{
var comparison = Expression.Call(
Expand Down Expand Up @@ -1702,19 +1654,19 @@ public Expression Convert(Type type)
}

private sealed class SqlTypeMappingVerifyingExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitExtension(Expression extensionExpression)
{
if (extensionExpression is SqlExpression sqlExpression
&& extensionExpression is not SqlFragmentExpression)
protected override Expression VisitExtension(Expression extensionExpression)
{
if (sqlExpression.TypeMapping == null)
if (extensionExpression is SqlExpression sqlExpression
&& extensionExpression is not SqlFragmentExpression)
{
throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print()));
if (sqlExpression.TypeMapping == null)
{
throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print()));
}
}
}

return base.VisitExtension(extensionExpression);
return base.VisitExtension(extensionExpression);
}
}
}
}
Loading

0 comments on commit 1d47398

Please sign in to comment.