diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index 6590593a40e..6b2e45614fc 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -681,7 +681,8 @@ private void AddForeignKeyEdges() if (!CanCreateDependency(foreignKey, command, principal: true) || !IsModified(foreignKey.PrincipalKey.Properties, entry) || (command.Table != null - && !IsStoreGenerated(entry, foreignKey.PrincipalKey))) + && !IsStoreGenerated(entry, foreignKey.PrincipalKey) + && foreignKey.GetMappedConstraints().Any())) { continue; } @@ -725,6 +726,34 @@ private void AddForeignKeyEdges() predecessorCommands.Add(command); } } + + // Also handle FKs with no mapped constraints (e.g. TPC with abstract principal mapped to no table) + foreach (var entry in command.Entries) + { + foreach (var foreignKey in entry.EntityType.GetForeignKeys()) + { + if (!CanCreateDependency(foreignKey, command, principal: false) + || !IsModified(foreignKey.Properties, entry) + || foreignKey.GetMappedConstraints().Any()) + { + continue; + } + + var dependentKeyValue = foreignKey.GetDependentKeyValueFactory() + ?.CreateDependentEquatableKey(entry, fromOriginalValues: true); + + if (dependentKeyValue != null) + { + if (!originalPredecessorsMap.TryGetValue(dependentKeyValue, out var predecessorCommands)) + { + predecessorCommands = []; + originalPredecessorsMap.Add(dependentKeyValue, predecessorCommands); + } + + predecessorCommands.Add(command); + } + } + } } else { @@ -824,6 +853,27 @@ private void AddForeignKeyEdges() AddMatchingPredecessorEdge( originalPredecessorsMap, principalKeyValue, command, foreignKey); } + + // Also handle FKs with no mapped constraints (e.g. TPC with abstract principal mapped to no table) + // ReSharper disable once ForCanBeConvertedToForeach + for (var entryIndex = 0; entryIndex < command.Entries.Count; entryIndex++) + { + var entry = command.Entries[entryIndex]; + foreach (var foreignKey in entry.EntityType.GetReferencingForeignKeys()) + { + if (!CanCreateDependency(foreignKey, command, principal: true) + || foreignKey.GetMappedConstraints().Any()) + { + continue; + } + + var principalKeyValue = foreignKey.GetDependentKeyValueFactory() + .CreatePrincipalEquatableKey(entry, fromOriginalValues: true); + Check.DebugAssert(principalKeyValue != null, "null principalKeyValue"); + AddMatchingPredecessorEdge( + originalPredecessorsMap, principalKeyValue, command, foreignKey); + } + } } else { @@ -873,6 +923,14 @@ private static bool CanCreateDependency(IForeignKey foreignKey, IReadOnlyModific { if (command.Table != null) { + // JSON-owned entities are stored inline in their owner's column and never have separate + // modification commands, so they cannot participate in inter-command dependency ordering. + var otherEntityType = principal ? foreignKey.DeclaringEntityType : foreignKey.PrincipalEntityType; + if (otherEntityType.IsMappedToJson()) + { + return false; + } + if (foreignKey.IsRowInternal(StoreObjectIdentifier.Table(command.TableName, command.Schema)) || (foreignKey.PrincipalEntityType.IsAssignableFrom(foreignKey.DeclaringEntityType) && foreignKey.PrincipalKey.Properties.SequenceEqual(foreignKey.Properties))) diff --git a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index a312f860c53..8f0be7b7a87 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -1328,4 +1328,90 @@ private class AnotherFakeEntity public int Id { get; set; } public int? AnotherId { get; set; } } + + [ConditionalFact] + public void BatchCommands_sorts_added_entities_with_TPC_abstract_principal() + { + var configuration = CreateContextServices(CreateTpcFKModel()); + var stateManager = configuration.GetRequiredService(); + + var principalEntry = stateManager.GetOrCreateEntry( + new ConcretePrincipal { Id = 1 }); + principalEntry.SetEntityState(EntityState.Added); + + var dependentEntry = stateManager.GetOrCreateEntry( + new TpcDependent { Id = 1, PrincipalId = 1 }); + dependentEntry.SetEntityState(EntityState.Added); + + var modelData = new UpdateAdapter(stateManager); + + var batches = CreateBatches([dependentEntry, principalEntry], modelData); + var batch = Assert.Single(batches); + + Assert.Equal( + [principalEntry, dependentEntry], + batch.ModificationCommands.Select(c => c.Entries.Single())); + } + + [ConditionalFact] + public void BatchCommands_sorts_deleted_entities_with_TPC_abstract_principal() + { + var configuration = CreateContextServices(CreateTpcFKModel()); + var stateManager = configuration.GetRequiredService(); + + var principalEntry = stateManager.GetOrCreateEntry( + new ConcretePrincipal { Id = 1 }); + principalEntry.SetEntityState(EntityState.Deleted); + + var dependentEntry = stateManager.GetOrCreateEntry( + new TpcDependent { Id = 1, PrincipalId = 1 }); + dependentEntry.SetEntityState(EntityState.Deleted); + + var modelData = new UpdateAdapter(stateManager); + + var batches = CreateBatches([principalEntry, dependentEntry], modelData); + var batch = Assert.Single(batches); + + Assert.Equal( + [dependentEntry, principalEntry], + batch.ModificationCommands.Select(c => c.Entries.Single())); + } + + private static IModel CreateTpcFKModel() + { + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); + + modelBuilder.Entity() + .UseTpcMappingStrategy() + .ToTable((string)null) + .Property(e => e.Id) + .ValueGeneratedNever(); + + modelBuilder.Entity() + .ToTable(nameof(ConcretePrincipal)); + + modelBuilder.Entity(b => + { + b.HasOne() + .WithMany() + .HasForeignKey(c => c.PrincipalId); + }); + + return modelBuilder.Model.FinalizeModel(); + } + + private abstract class AbstractPrincipal + { + public int Id { get; set; } + } + + private class ConcretePrincipal : AbstractPrincipal + { + } + + private class TpcDependent + { + public int Id { get; set; } + public int PrincipalId { get; set; } + } }