Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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)))
Expand Down
86 changes: 86 additions & 0 deletions test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IStateManager>();

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<IStateManager>();

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<AbstractPrincipal>()
.UseTpcMappingStrategy()
.ToTable((string)null)
.Property(e => e.Id)
.ValueGeneratedNever();

Comment thread
andrewraper-Sage marked this conversation as resolved.
modelBuilder.Entity<ConcretePrincipal>()
.ToTable(nameof(ConcretePrincipal));

Comment thread
andrewraper-Sage marked this conversation as resolved.
modelBuilder.Entity<TpcDependent>(b =>
{
b.HasOne<AbstractPrincipal>()
.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; }
}
}
Loading