Skip to content

Commit

Permalink
Add entity splitting support to relational model
Browse files Browse the repository at this point in the history
Add a linking FK for mapping fragments
Make ITableMappingBase.IsSharedTablePrincipal and ITableMappingBase.IsSplitEntityTypePrincipal nullable
Warn for FKs, indexes and keys split over several tables
Move more logic from RelationalModel and RelationalModelValidator to extensions to avoid duplication

Part of #620
  • Loading branch information
AndriySvyryd committed Jun 22, 2022
1 parent 037fc6f commit 5618bc6
Show file tree
Hide file tree
Showing 53 changed files with 2,288 additions and 1,050 deletions.
15 changes: 15 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalEventId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ private enum Id
DuplicateColumnOrders,
ForeignKeyTpcPrincipalWarning,
TpcStoreGeneratedIdentityWarning,
KeyUnmappedProperties,

// Update events
BatchReadyForExecution = CoreEventId.RelationalBaseId + 700,
Expand Down Expand Up @@ -799,6 +800,20 @@ private static EventId MakeValidationId(Id id)
public static readonly EventId IndexPropertiesMappedToNonOverlappingTables =
MakeValidationId(Id.IndexPropertiesMappedToNonOverlappingTables);

/// <summary>
/// A key specifies properties which don't map to a single table.
/// </summary>
/// <remarks>
/// <para>
/// This event is in the <see cref="DbLoggerCategory.Model.Validation" /> category.
/// </para>
/// <para>
/// This event uses the <see cref="KeyEventData" /> payload when used with a <see cref="DiagnosticSource" />.
/// </para>
/// </remarks>
public static readonly EventId KeyUnmappedProperties =
MakeValidationId(Id.KeyUnmappedProperties);

/// <summary>
/// A foreign key specifies properties which don't map to the related tables.
/// </summary>
Expand Down
41 changes: 41 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2774,6 +2774,47 @@ private static string NamedIndexPropertiesMappedToNonOverlappingTables(EventDefi
p.TablesMappedToProperty2.FormatTables()));
}

/// <summary>
/// Logs the <see cref="RelationalEventId.KeyUnmappedProperties" /> event.
/// </summary>
/// <param name="diagnostics">The diagnostics logger to use.</param>
/// <param name="key">The foreign key.</param>
public static void KeyUnmappedProperties(
this IDiagnosticsLogger<DbLoggerCategory.Model.Validation> diagnostics,
IKey key)
{
var definition = RelationalResources.LogKeyUnmappedProperties(diagnostics);

if (diagnostics.ShouldLog(definition))
{
definition.Log(
diagnostics,
key.Properties.Format(),
key.DeclaringEntityType.DisplayName(),
key.DeclaringEntityType.GetSchemaQualifiedTableName()!);
}

if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
{
var eventData = new KeyEventData(
definition,
KeyUnmappedProperties,
key);

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
}
}

private static string KeyUnmappedProperties(EventDefinitionBase definition, EventData payload)
{
var d = (EventDefinition<string, string, string>)definition;
var p = (KeyEventData)payload;
return d.GenerateMessage(
p.Key.Properties.Format(),
p.Key.DeclaringEntityType.DisplayName(),
p.Key.DeclaringEntityType.GetSchemaQualifiedTableName()!);
}

/// <summary>
/// Logs the <see cref="RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables" /> event.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,15 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions
[EntityFrameworkInternal]
public EventDefinitionBase? LogUnnamedIndexPropertiesMappedToNonOverlappingTables;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// 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>
[EntityFrameworkInternal]
public EventDefinitionBase? LogKeyUnmappedProperties;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
143 changes: 109 additions & 34 deletions src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,36 @@ public static void SetComment(this IMutableEntityType entityType, string? commen
public static IEnumerable<IReadOnlyEntityTypeMappingFragment> GetMappingFragments(this IReadOnlyEntityType entityType)
=> EntityTypeMappingFragment.Get(entityType) ?? Enumerable.Empty<IReadOnlyEntityTypeMappingFragment>();

/// <summary>
/// <para>
/// Returns all configured entity type mapping fragments.
/// </para>
/// <para>
/// This method is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns>The configured entity type mapping fragments.</returns>
public static IEnumerable<IMutableEntityTypeMappingFragment> GetMappingFragments(this IMutableEntityType entityType)
=> EntityTypeMappingFragment.Get(entityType)?.Cast<IMutableEntityTypeMappingFragment>()
?? Enumerable.Empty<IMutableEntityTypeMappingFragment>();

/// <summary>
/// <para>
/// Returns all configured entity type mapping fragments.
/// </para>
/// <para>
/// This method is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns>The configured entity type mapping fragments.</returns>
public static IEnumerable<IConventionEntityTypeMappingFragment> GetMappingFragments(this IConventionEntityType entityType)
=> EntityTypeMappingFragment.Get(entityType)?.Cast<IConventionEntityTypeMappingFragment>()
?? Enumerable.Empty<IConventionEntityTypeMappingFragment>();

/// <summary>
/// <para>
/// Returns all configured entity type mapping fragments.
Expand All @@ -857,6 +887,75 @@ public static IEnumerable<IEntityTypeMappingFragment> GetMappingFragments(this I
=> EntityTypeMappingFragment.Get(entityType)?.Cast<IEntityTypeMappingFragment>()
?? Enumerable.Empty<IEntityTypeMappingFragment>();

/// <summary>
/// <para>
/// Returns all configured entity type mapping fragments of the given type.
/// </para>
/// <para>
/// This method is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="storeObjectType">The type of store object to get the mapping fragments for.</param>
/// <returns>The configured entity type mapping fragments.</returns>
public static IEnumerable<IReadOnlyEntityTypeMappingFragment> GetMappingFragments(
this IReadOnlyEntityType entityType, StoreObjectType storeObjectType)
{
var fragments = EntityTypeMappingFragment.Get(entityType);
return fragments == null
? Enumerable.Empty<IReadOnlyEntityTypeMappingFragment>()
: fragments.Where(f => f.StoreObject.StoreObjectType == storeObjectType);
}

/// <summary>
/// <para>
/// Returns all configured entity type mapping fragments of the given type.
/// </para>
/// <para>
/// This method is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="storeObjectType">The type of store object to get the mapping fragments for.</param>
/// <returns>The configured entity type mapping fragments.</returns>
public static IEnumerable<IMutableEntityTypeMappingFragment> GetMappingFragments(
this IMutableEntityType entityType, StoreObjectType storeObjectType)
=> GetMappingFragments((IReadOnlyEntityType)entityType, storeObjectType).Cast<IMutableEntityTypeMappingFragment>();

/// <summary>
/// <para>
/// Returns all configured entity type mapping fragments of the given type.
/// </para>
/// <para>
/// This method is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="storeObjectType">The type of store object to get the mapping fragments for.</param>
/// <returns>The configured entity type mapping fragments.</returns>
public static IEnumerable<IConventionEntityTypeMappingFragment> GetMappingFragments(
this IConventionEntityType entityType, StoreObjectType storeObjectType)
=> GetMappingFragments((IReadOnlyEntityType)entityType, storeObjectType).Cast<IConventionEntityTypeMappingFragment>();

/// <summary>
/// <para>
/// Returns all configured entity type mapping fragments of the given type.
/// </para>
/// <para>
/// This method is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="storeObjectType">The type of store object to get the mapping fragments for.</param>
/// <returns>The configured entity type mapping fragments.</returns>
public static IEnumerable<IEntityTypeMappingFragment> GetMappingFragments(
this IEntityType entityType, StoreObjectType storeObjectType)
=> GetMappingFragments((IReadOnlyEntityType)entityType, storeObjectType).Cast<IEntityTypeMappingFragment>();

/// <summary>
/// <para>
/// Returns the entity type mapping for a particular table-like store object.
Expand Down Expand Up @@ -1019,46 +1118,22 @@ public static IEnumerable<IReadOnlyForeignKey> FindRowInternalForeignKeys(

foreach (var foreignKey in entityType.GetForeignKeys())
{
var principalEntityType = foreignKey.PrincipalEntityType;
if (!foreignKey.PrincipalKey.IsPrimaryKey()
|| principalEntityType == foreignKey.DeclaringEntityType
|| !foreignKey.IsUnique
#pragma warning disable EF1001 // Internal EF Core API usage.
|| !PropertyListComparer.Instance.Equals(foreignKey.Properties, primaryKey.Properties))
#pragma warning restore EF1001 // Internal EF Core API usage.
|| foreignKey.PrincipalEntityType.IsAssignableFrom(foreignKey.DeclaringEntityType)
|| !foreignKey.Properties.SequenceEqual(primaryKey.Properties)
|| !IsMapped(foreignKey, storeObject))
{
continue;
}

switch (storeObject.StoreObjectType)
{
case StoreObjectType.Table:
if (storeObject.Name == principalEntityType.GetTableName()
&& storeObject.Schema == principalEntityType.GetSchema())
{
yield return foreignKey;
}

break;
case StoreObjectType.View:
if (storeObject.Name == principalEntityType.GetViewName()
&& storeObject.Schema == principalEntityType.GetViewSchema())
{
yield return foreignKey;
}

break;
case StoreObjectType.Function:
if (storeObject.Name == principalEntityType.GetFunctionName())
{
yield return foreignKey;
}

break;
default:
throw new NotSupportedException(storeObject.StoreObjectType.ToString());
}
yield return foreignKey;
}

static bool IsMapped(IReadOnlyForeignKey foreignKey, StoreObjectIdentifier storeObject)
=> (StoreObjectIdentifier.Create(foreignKey.DeclaringEntityType, storeObject.StoreObjectType) == storeObject
|| foreignKey.DeclaringEntityType.GetMappingFragments(storeObject.StoreObjectType).Any(f => f.StoreObject == storeObject))
&& (StoreObjectIdentifier.Create(foreignKey.PrincipalEntityType, storeObject.StoreObjectType) == storeObject
|| foreignKey.PrincipalEntityType.GetMappingFragments(storeObject.StoreObjectType).Any(f => f.StoreObject == storeObject));
}

/// <summary>
Expand Down
87 changes: 3 additions & 84 deletions src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -44,18 +45,7 @@ public static class RelationalForeignKeyExtensions
this IReadOnlyForeignKey foreignKey,
in StoreObjectIdentifier storeObject,
in StoreObjectIdentifier principalStoreObject)
{
if (storeObject.StoreObjectType != StoreObjectType.Table
|| principalStoreObject.StoreObjectType != StoreObjectType.Table)
{
return null;
}

var annotation = foreignKey.FindAnnotation(RelationalAnnotationNames.Name);
return annotation != null
? (string?)annotation.Value
: foreignKey.GetDefaultName(storeObject, principalStoreObject);
}
=> foreignKey.GetConstraintName(storeObject, principalStoreObject, null);

/// <summary>
/// Returns the default constraint name that would be used for this foreign key.
Expand Down Expand Up @@ -101,78 +91,7 @@ public static class RelationalForeignKeyExtensions
this IReadOnlyForeignKey foreignKey,
in StoreObjectIdentifier storeObject,
in StoreObjectIdentifier principalStoreObject)
{
if (storeObject.StoreObjectType != StoreObjectType.Table
|| principalStoreObject.StoreObjectType != StoreObjectType.Table)
{
return null;
}

var propertyNames = foreignKey.Properties.GetColumnNames(storeObject);
var principalPropertyNames = foreignKey.PrincipalKey.Properties.GetColumnNames(principalStoreObject);
if (propertyNames == null
|| principalPropertyNames == null)
{
return null;
}

var rootForeignKey = foreignKey;

// Limit traversal to avoid getting stuck in a cycle (validation will throw for these later)
// Using a hashset is detrimental to the perf when there are no cycles
for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++)
{
IReadOnlyForeignKey? linkedForeignKey = null;
foreach (var otherForeignKey in rootForeignKey.DeclaringEntityType
.FindRowInternalForeignKeys(storeObject)
.SelectMany(fk => fk.PrincipalEntityType.GetForeignKeys()))
{
if (principalStoreObject.Name == otherForeignKey.PrincipalEntityType.GetTableName()
&& principalStoreObject.Schema == otherForeignKey.PrincipalEntityType.GetSchema())
{
var otherColumnNames = otherForeignKey.Properties.GetColumnNames(storeObject);
var otherPrincipalColumnNames = otherForeignKey.PrincipalKey.Properties.GetColumnNames(principalStoreObject);
if (otherColumnNames != null
&& otherPrincipalColumnNames != null
&& propertyNames.SequenceEqual(otherColumnNames)
&& principalPropertyNames.SequenceEqual(otherPrincipalColumnNames))
{
linkedForeignKey = otherForeignKey;
break;
}
}
}

if (linkedForeignKey == null)
{
break;
}

rootForeignKey = linkedForeignKey;
}

if (rootForeignKey != foreignKey)
{
return rootForeignKey.GetConstraintName(storeObject, principalStoreObject);
}

if (foreignKey.PrincipalEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy
&& foreignKey.PrincipalEntityType.GetDerivedTypes().Any(et => StoreObjectIdentifier.Create(et, StoreObjectType.Table) != null))
{
return null;
}

var baseName = new StringBuilder()
.Append("FK_")
.Append(storeObject.Name)
.Append('_')
.Append(principalStoreObject.Name)
.Append('_')
.AppendJoin(propertyNames, "_")
.ToString();

return Uniquifier.Truncate(baseName, foreignKey.DeclaringEntityType.Model.GetMaxIdentifierLength());
}
=> foreignKey.GetDefaultName(storeObject, principalStoreObject, null);

/// <summary>
/// Sets the foreign key constraint name.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.



// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

Expand Down
Loading

0 comments on commit 5618bc6

Please sign in to comment.