diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json index 78998352236..7714ed29fce 100644 --- a/src/EFCore.Relational/EFCore.Relational.baseline.json +++ b/src/EFCore.Relational/EFCore.Relational.baseline.json @@ -7352,10 +7352,10 @@ "Member": "virtual Microsoft.EntityFrameworkCore.Query.JsonQueryExpression BindStructuralProperty(Microsoft.EntityFrameworkCore.Metadata.IPropertyBase structuralProperty);" }, { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.IRelationalJsonElement? FindJsonElement(Microsoft.EntityFrameworkCore.Metadata.IPropertyBase propertyBase);" + "Member": "override bool Equals(object? obj);" }, { - "Member": "override bool Equals(object? obj);" + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.IRelationalJsonElement? FindJsonElement(Microsoft.EntityFrameworkCore.Metadata.IPropertyBase propertyBase);" }, { "Member": "override int GetHashCode();" @@ -17425,6 +17425,9 @@ { "Member": "static string? GetContainerColumnName(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyTypeBase typeBase);" }, + { + "Member": "static string? GetContainerColumnName(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyTypeBase typeBase, in Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier storeObject);" + }, { "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetContainerColumnNameConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionTypeBase typeBase);" }, diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 65860f71242..caa17d220d1 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -230,7 +230,10 @@ public static string GetDefaultColumnName(this IReadOnlyProperty property) } else if (StoreObjectIdentifier.Create(property.DeclaringType, currentStoreObject.StoreObjectType) == currentStoreObject || property.DeclaringType.GetMappingFragments(storeObject.StoreObjectType) - .Any(f => f.StoreObject == currentStoreObject)) + .Any(f => f.StoreObject == currentStoreObject) + || (property.IsPrimaryKey() + && property.DeclaringType.ContainingEntityType.GetDerivedTypes() + .Any(e => StoreObjectIdentifier.Create(e, currentStoreObject.StoreObjectType) == currentStoreObject))) { builder = CreateComplexPrefix((IReadOnlyComplexType)property.DeclaringType, storeObject, builder); } diff --git a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs index ecf8df98a07..74bd62e891e 100644 --- a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs @@ -388,6 +388,78 @@ public static bool IsMappedToJson(this IReadOnlyTypeBase typeBase) : ((IReadOnlyComplexType)typeBase).ComplexProperty.DeclaringType.GetContainerColumnName(); } + /// + /// Gets the container column name to which the type is mapped for a particular table-like store object. + /// + /// The type to get the container column name for. + /// The identifier of the table-like store object containing the column. + /// + /// The container column name to which the type is mapped, or if the type is not mapped + /// to a container column in the given store object. + /// + public static string? GetContainerColumnName(this IReadOnlyTypeBase typeBase, in StoreObjectIdentifier storeObject) + { + var annotation = typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnName); + if (annotation != null) + { + var containerColumnName = (string?)annotation.Value; + if (string.IsNullOrEmpty(containerColumnName)) + { + return containerColumnName; + } + + if (storeObject.StoreObjectType is StoreObjectType.Function or StoreObjectType.SqlQuery) + { + return containerColumnName; + } + + var containingEntityType = typeBase.ContainingEntityType; + if (containingEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy) + { + var localStoreObject = storeObject; + return StoreObjectIdentifier.Create(containingEntityType, localStoreObject.StoreObjectType) == localStoreObject + || containingEntityType.GetDerivedTypes().Any(e => StoreObjectIdentifier.Create(e, localStoreObject.StoreObjectType) == localStoreObject) + ? containerColumnName + : null; + } + + // TODO: Support entity splitting with JSON columns. Issue #36172 + var declaringStoreObject = StoreObjectIdentifier.Create(typeBase, storeObject.StoreObjectType); + if (declaringStoreObject == null) + { + var tableFound = false; + var queue = new Queue(); + queue.Enqueue(containingEntityType); + while (queue.Count > 0 && !tableFound) + { + foreach (var derivedType in queue.Dequeue().GetDirectlyDerivedTypes()) + { + var derivedStoreObject = StoreObjectIdentifier.Create(derivedType, storeObject.StoreObjectType); + if (derivedStoreObject == null) + { + queue.Enqueue(derivedType); + continue; + } + + if (derivedStoreObject == storeObject) + { + tableFound = true; + break; + } + } + } + + return tableFound ? containerColumnName : null; + } + + return declaringStoreObject == storeObject ? containerColumnName : null; + } + + return typeBase is IReadOnlyEntityType entityType + ? entityType.FindOwnership()?.PrincipalEntityType.GetContainerColumnName(storeObject) + : ((IReadOnlyComplexType)typeBase).ComplexProperty.DeclaringType.GetContainerColumnName(storeObject); + } + /// /// Sets the name of the container column to which the type is mapped. /// diff --git a/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs index f695920f589..d425f7ed457 100644 --- a/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/EntityTypeHierarchyMappingConvention.cs @@ -88,6 +88,7 @@ public virtual void ProcessModelFinalizing( { var pk = entityType.FindPrimaryKey(); if (pk != null + && pk.Properties.All(p => p.DeclaringType is IConventionEntityType) && !entityType.FindDeclaredForeignKeys(pk.Properties) .Any(fk => fk.PrincipalKey.IsPrimaryKey() && fk.PrincipalEntityType.IsAssignableFrom(entityType) diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 8c1a058d567..821f32c76a3 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -509,7 +509,7 @@ private static void CreateTableMapping( IsSplitEntityTypePrincipal = isSplitEntityTypePrincipal }; - var containerColumnName = mappedType.GetContainerColumnName(); + var containerColumnName = mappedType.GetContainerColumnName(mappedTable); var containerColumnType = mappedType.GetContainerColumnType(); if (!string.IsNullOrEmpty(containerColumnName)) { @@ -1028,7 +1028,7 @@ private static void CreateViewMapping( IsSplitEntityTypePrincipal = isSplitEntityTypePrincipal }; - var containerColumnName = mappedType.GetContainerColumnName(); + var containerColumnName = mappedType.GetContainerColumnName(mappedView); var containerColumnType = mappedType.GetContainerColumnType(); if (!string.IsNullOrEmpty(containerColumnName)) { diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index d3d89ef8c9d..74f6c2091eb 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -3381,6 +3381,183 @@ public void Complex_property_json_column_is_nullable_in_TPH_hierarchy() Assert.IsType(jsonColumn); } + [Fact] + public void Complex_property_json_column_is_not_duplicated_in_TPT_child_tables() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity() + .UseTptMappingStrategy() + .ComplexProperty(e => e.ComplexProperty, b => b.ToJson()); + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + var relationalModel = model.GetRelationalModel(); + + var baseTable = relationalModel.Tables.Single(t => t.Name == nameof(TptBaseEntityWithComplexProperty)); + var childTable = relationalModel.Tables.Single(t => t.Name == nameof(TptDerivedEntityWithoutComplexProperty)); + + // The JSON column for the base complex property must appear only in the base table + Assert.Contains(baseTable.Columns, c => c.Name == nameof(TptBaseEntityWithComplexProperty.ComplexProperty)); + Assert.DoesNotContain(childTable.Columns, c => c.Name == nameof(TptBaseEntityWithComplexProperty.ComplexProperty)); + } + + [Fact] + public void Complex_property_columns_are_not_duplicated_in_TPT_child_tables() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity() + .UseTptMappingStrategy() + .ComplexProperty(e => e.ComplexProperty); + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + var relationalModel = model.GetRelationalModel(); + + var baseTable = relationalModel.Tables.Single(t => t.Name == nameof(TptBaseEntityWithComplexProperty)); + var childTable = relationalModel.Tables.Single(t => t.Name == nameof(TptDerivedEntityWithoutComplexProperty)); + + // Non-JSON complex property columns appear only on the base table. + var valueColumnName = nameof(TptBaseEntityWithComplexProperty.ComplexProperty) + "_" + nameof(ComplexData.Value); + var numberColumnName = nameof(TptBaseEntityWithComplexProperty.ComplexProperty) + "_" + nameof(ComplexData.Number); + Assert.Contains(baseTable.Columns, c => c.Name == valueColumnName); + Assert.Contains(baseTable.Columns, c => c.Name == numberColumnName); + Assert.DoesNotContain(childTable.Columns, c => c.Name == valueColumnName); + Assert.DoesNotContain(childTable.Columns, c => c.Name == numberColumnName); + } + + [Fact] + public void Complex_property_json_column_is_created_in_every_TPC_table() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(b => + { + b.UseTpcMappingStrategy(); + b.ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson()); + }); + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + var relationalModel = model.GetRelationalModel(); + + var baseTable = relationalModel.Tables.Single(t => t.Name == nameof(TpcBaseEntityWithComplexProperty)); + var derivedTable = relationalModel.Tables.Single(t => t.Name == nameof(TpcDerivedEntityWithoutComplexProperty)); + + // In TPC the JSON container column appears on every concrete table. + Assert.Contains(baseTable.Columns, c => c.Name == nameof(TpcBaseEntityWithComplexProperty.ComplexProperty)); + Assert.Contains(derivedTable.Columns, c => c.Name == nameof(TpcBaseEntityWithComplexProperty.ComplexProperty)); + } + + [Fact] + public void GetContainerColumnName_with_StoreObjectIdentifier_resolves_per_table() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity() + .UseTptMappingStrategy() + .ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson()); + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + var baseEntity = model.FindEntityType(typeof(TptBaseEntityWithComplexProperty))!; + var complexProperty = baseEntity.FindComplexProperty(nameof(TptBaseEntityWithComplexProperty.ComplexProperty))!; + var complexType = complexProperty.ComplexType; + + var baseTable = StoreObjectIdentifier.Table(nameof(TptBaseEntityWithComplexProperty)); + var childTable = StoreObjectIdentifier.Table(nameof(TptDerivedEntityWithoutComplexProperty)); + var unrelatedTable = StoreObjectIdentifier.Table("SomeOtherTable"); + + Assert.Equal(nameof(TptBaseEntityWithComplexProperty.ComplexProperty), complexType.GetContainerColumnName(baseTable)); + Assert.Null(complexType.GetContainerColumnName(childTable)); + Assert.Null(complexType.GetContainerColumnName(unrelatedTable)); + } + + [Fact] + public void GetContainerColumnName_with_StoreObjectIdentifier_returns_column_for_every_TPC_table() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(b => + { + b.UseTpcMappingStrategy(); + b.ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson()); + }); + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + var baseEntity = model.FindEntityType(typeof(TpcBaseEntityWithComplexProperty))!; + var complexProperty = baseEntity.FindComplexProperty(nameof(TpcBaseEntityWithComplexProperty.ComplexProperty))!; + var complexType = complexProperty.ComplexType; + + var baseTable = StoreObjectIdentifier.Table(nameof(TpcBaseEntityWithComplexProperty)); + var derivedTable = StoreObjectIdentifier.Table(nameof(TpcDerivedEntityWithoutComplexProperty)); + var unrelatedTable = StoreObjectIdentifier.Table("SomeOtherTable"); + + Assert.Equal(nameof(TpcBaseEntityWithComplexProperty.ComplexProperty), complexType.GetContainerColumnName(baseTable)); + Assert.Equal(nameof(TpcBaseEntityWithComplexProperty.ComplexProperty), complexType.GetContainerColumnName(derivedTable)); + Assert.Null(complexType.GetContainerColumnName(unrelatedTable)); + } + + [Fact] + public void Complex_type_with_PK_property_creates_column_mapping_in_TPT_child_table() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(b => + { + b.UseTptMappingStrategy(); + b.ComplexProperty(e => e.Key); + b.HasKey(e => e.Key.Id); + b.Property(e => e.Key.Id).ValueGeneratedNever(); + }); + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + var relationalModel = model.GetRelationalModel(); + + var baseTable = relationalModel.Tables.Single(t => t.Name == nameof(TptBaseWithComplexTypePK)); + var childTable = relationalModel.Tables.Single(t => t.Name == nameof(TptDerivedWithComplexTypePK)); + + // The PK column for the complex-typed key must appear in both the base table and the child table. + Assert.Contains(baseTable.Columns, c => c.Name == "Key_Id"); + Assert.Contains(childTable.Columns, c => c.Name == "Key_Id"); + + // Non-key properties of the derived entity stay on the child table. + Assert.Contains(childTable.Columns, c => c.Name == nameof(TptDerivedWithComplexTypePK.Extra)); + Assert.DoesNotContain(baseTable.Columns, c => c.Name == nameof(TptDerivedWithComplexTypePK.Extra)); + } + + [Fact] + public void Complex_property_json_column_is_not_duplicated_in_TPT_child_views() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(b => + { + b.UseTptMappingStrategy(); + b.ToTable(nameof(TptBaseEntityWithComplexProperty)); + b.ToView(nameof(TptBaseEntityWithComplexProperty) + "View"); + b.ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson()); + }); + modelBuilder.Entity(b => + { + b.ToTable(nameof(TptDerivedEntityWithoutComplexProperty)); + b.ToView(nameof(TptDerivedEntityWithoutComplexProperty) + "View"); + }); + + var model = modelBuilder.FinalizeModel(); + var relationalModel = model.GetRelationalModel(); + + var baseView = relationalModel.Views.Single(v => v.Name == nameof(TptBaseEntityWithComplexProperty) + "View"); + var childView = relationalModel.Views.Single(v => v.Name == nameof(TptDerivedEntityWithoutComplexProperty) + "View"); + + // The JSON column for the base complex property must appear only in the base view. + Assert.Contains(baseView.Columns, c => c.Name == nameof(TptBaseEntityWithComplexProperty.ComplexProperty)); + Assert.DoesNotContain(childView.Columns, c => c.Name == nameof(TptBaseEntityWithComplexProperty.ComplexProperty)); + } + [Fact] public void Json_element_tree_is_built_for_owned_entity_json_columns() { @@ -3994,6 +4171,37 @@ private class TphEntityWithComplexProperty : TphBaseEntity public ComplexData ComplexProperty { get; set; } } + private abstract class TptBaseEntityWithComplexProperty + { + public int Id { get; set; } + public ComplexData ComplexProperty { get; set; } + } + + private class TptDerivedEntityWithoutComplexProperty : TptBaseEntityWithComplexProperty; + + private class TpcBaseEntityWithComplexProperty + { + public int Id { get; set; } + public ComplexData ComplexProperty { get; set; } + } + + private class TpcDerivedEntityWithoutComplexProperty : TpcBaseEntityWithComplexProperty; + + private abstract class TptBaseWithComplexTypePK + { + public TptComplexKey Key { get; set; } = null!; + } + + private class TptComplexKey + { + public int Id { get; set; } + } + + private class TptDerivedWithComplexTypePK : TptBaseWithComplexTypePK + { + public int Extra { get; set; } + } + private class EntityWithJsonOwnedWithCollection { public int Id { get; set; }