diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index cba40a8ebbd..e5ee05eed0c 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -2189,7 +2189,7 @@ private IEnumerable GetDataOperations( var commandBatches = new CommandBatchPreparer(CommandBatchPreparerDependencies) .BatchCommands(entries, updateAdapter); - foreach (var commandBatch in commandBatches) + foreach (var (commandBatch, _) in commandBatches) { InsertDataOperation? batchInsertOperation = null; foreach (var command in commandBatch.ModificationCommands) diff --git a/src/EFCore.Relational/Update/IBatchExecutor.cs b/src/EFCore.Relational/Update/IBatchExecutor.cs index e8209d5328b..20ebf876acc 100644 --- a/src/EFCore.Relational/Update/IBatchExecutor.cs +++ b/src/EFCore.Relational/Update/IBatchExecutor.cs @@ -28,17 +28,21 @@ public interface IBatchExecutor /// /// Executes the commands in the batches against the given database connection. /// - /// The batches to execute. + /// + /// A list of value tuples, each of which contains a batch to execute, and whether more batches are available. + /// /// The database connection to use. /// The total number of rows affected. int Execute( - IEnumerable commandBatches, + IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> commandBatches, IRelationalConnection connection); /// /// Executes the commands in the batches against the given database connection. /// - /// The batches to execute. + /// + /// A list of value tuples, each of which contains a batch to execute, and whether more batches are available. + /// /// The database connection to use. /// A to observe while waiting for the task to complete. /// @@ -47,7 +51,7 @@ int Execute( /// /// If the is canceled. Task ExecuteAsync( - IEnumerable commandBatches, + IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> commandBatches, IRelationalConnection connection, CancellationToken cancellationToken = default); } diff --git a/src/EFCore.Relational/Update/ICommandBatchPreparer.cs b/src/EFCore.Relational/Update/ICommandBatchPreparer.cs index 9bfdd8ca3e8..b045c936d51 100644 --- a/src/EFCore.Relational/Update/ICommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/ICommandBatchPreparer.cs @@ -32,8 +32,6 @@ public interface ICommandBatchPreparer /// /// The entries that represent the entities to be modified. /// The model data. - /// The list of batches to execute. - IEnumerable BatchCommands( - IList entries, - IUpdateAdapter updateAdapter); + /// A list of value tuples, each of which contains a batch to execute, and whether more batches are available. + IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> BatchCommands(IList entries, IUpdateAdapter updateAdapter); } diff --git a/src/EFCore.Relational/Update/Internal/BatchExecutor.cs b/src/EFCore.Relational/Update/Internal/BatchExecutor.cs index 986b0271c94..6eb20e22be9 100644 --- a/src/EFCore.Relational/Update/Internal/BatchExecutor.cs +++ b/src/EFCore.Relational/Update/Internal/BatchExecutor.cs @@ -49,7 +49,7 @@ public BatchExecutor( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual int Execute( - IEnumerable commandBatches, + IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> commandBatches, IRelationalConnection connection) { using var batchEnumerator = commandBatches.GetEnumerator(); @@ -59,8 +59,7 @@ public virtual int Execute( return 0; } - var currentBatch = batchEnumerator.Current; - var nextBatch = batchEnumerator.MoveNext() ? batchEnumerator.Current : null; + var (batch, hasMoreBatches) = batchEnumerator.Current; var rowsAffected = 0; var transaction = connection.CurrentTransaction; @@ -74,7 +73,7 @@ public virtual int Execute( && transactionEnlistManager?.CurrentAmbientTransaction is null && CurrentContext.Context.Database.AutoTransactionsEnabled // Don't start a transaction if we have a single batch which doesn't require a transaction (single command), for perf. - && (nextBatch is not null || currentBatch.RequiresTransaction)) + && (hasMoreBatches || batch.RequiresTransaction)) { transaction = connection.BeginTransaction(); beganTransaction = true; @@ -91,14 +90,13 @@ public virtual int Execute( } } - while (currentBatch is not null) + do { - currentBatch.Execute(connection); - rowsAffected += currentBatch.ModificationCommands.Count; - - currentBatch = nextBatch; - nextBatch = batchEnumerator.MoveNext() ? batchEnumerator.Current : null; + batch = batchEnumerator.Current.Batch; + batch.Execute(connection); + rowsAffected += batch.ModificationCommands.Count; } + while (batchEnumerator.MoveNext()); if (beganTransaction) { @@ -158,7 +156,7 @@ public virtual int Execute( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual async Task ExecuteAsync( - IEnumerable commandBatches, + IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> commandBatches, IRelationalConnection connection, CancellationToken cancellationToken = default) { @@ -169,8 +167,7 @@ public virtual async Task ExecuteAsync( return 0; } - var currentBatch = batchEnumerator.Current; - var nextBatch = batchEnumerator.MoveNext() ? batchEnumerator.Current : null; + var (batch, hasMoreBatches) = batchEnumerator.Current; var rowsAffected = 0; var transaction = connection.CurrentTransaction; @@ -184,7 +181,7 @@ public virtual async Task ExecuteAsync( && transactionEnlistManager?.CurrentAmbientTransaction is null && CurrentContext.Context.Database.AutoTransactionsEnabled // Don't start a transaction if we have a single batch which doesn't require a transaction (single command), for perf. - && (nextBatch is not null || currentBatch.RequiresTransaction)) + && (hasMoreBatches || batch.RequiresTransaction)) { transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); beganTransaction = true; @@ -201,14 +198,13 @@ public virtual async Task ExecuteAsync( } } - while (currentBatch is not null) + do { - await currentBatch.ExecuteAsync(connection, cancellationToken).ConfigureAwait(false); - rowsAffected += currentBatch.ModificationCommands.Count; - - currentBatch = nextBatch; - nextBatch = batchEnumerator.MoveNext() ? batchEnumerator.Current : null; + batch = batchEnumerator.Current.Batch; + await batch.ExecuteAsync(connection, cancellationToken).ConfigureAwait(false); + rowsAffected += batch.ModificationCommands.Count; } + while (batchEnumerator.MoveNext()); if (beganTransaction) { diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index e463bcc41e6..947dcd565c3 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -50,7 +50,7 @@ public CommandBatchPreparer(CommandBatchPreparerDependencies dependencies) /// 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. /// - public virtual IEnumerable BatchCommands( + public virtual IEnumerable<(ModificationCommandBatch Batch, bool HasMore)> BatchCommands( IList entries, IUpdateAdapter updateAdapter) { @@ -58,8 +58,10 @@ public virtual IEnumerable BatchCommands( var commands = CreateModificationCommands(entries, updateAdapter, parameterNameGenerator.GenerateNext); var sortedCommandSets = TopologicalSort(commands); - foreach (var independentCommandSet in sortedCommandSets) + for (var commandSetIndex = 0; commandSetIndex < sortedCommandSets.Count; commandSetIndex++) { + var independentCommandSet = sortedCommandSets[commandSetIndex]; + independentCommandSet.Sort(Dependencies.ModificationCommandComparer); var batch = Dependencies.ModificationCommandBatchFactory.Create(); @@ -85,7 +87,7 @@ public virtual IEnumerable BatchCommands( batch.Complete(); - yield return batch; + yield return (batch, true); } else { @@ -97,7 +99,7 @@ public virtual IEnumerable BatchCommands( batch = StartNewBatch(parameterNameGenerator, command); batch.Complete(); - yield return batch; + yield return (batch, true); } } @@ -105,6 +107,8 @@ public virtual IEnumerable BatchCommands( } } + var hasMoreCommandSets = commandSetIndex < sortedCommandSets.Count - 1; + if (batch.ModificationCommands.Count == 1 || batch.ModificationCommands.Count >= _minBatchSize) { @@ -116,19 +120,19 @@ public virtual IEnumerable BatchCommands( batch.Complete(); - yield return batch; + yield return (batch, hasMoreCommandSets); } else { Dependencies.UpdateLogger.BatchSmallerThanMinBatchSize( batch.ModificationCommands.SelectMany(c => c.Entries), batch.ModificationCommands.Count, _minBatchSize); - foreach (var command in batch.ModificationCommands) + for (var commandIndex = 0; commandIndex < batch.ModificationCommands.Count; commandIndex++) { - batch = StartNewBatch(parameterNameGenerator, command); - batch.Complete(); + var singleCommandBatch = StartNewBatch(parameterNameGenerator, batch.ModificationCommands[commandIndex]); + singleCommandBatch.Complete(); - yield return batch; + yield return (singleCommandBatch, hasMoreCommandSets || commandIndex < batch.ModificationCommands.Count - 1); } } } diff --git a/test/EFCore.Relational.Tests/Update/BatchExecutorTest.cs b/test/EFCore.Relational.Tests/Update/BatchExecutorTest.cs index 4188eba9ccb..4e5cbc3f560 100644 --- a/test/EFCore.Relational.Tests/Update/BatchExecutorTest.cs +++ b/test/EFCore.Relational.Tests/Update/BatchExecutorTest.cs @@ -15,8 +15,8 @@ public async Task ExecuteAsync_calls_Commit_if_no_transaction(bool async) using var context = new TestContext(); var connection = SetupConnection(context); - context.Add( - new Foo { Id = "1" }); + context.Add(new Foo { Id = "1" }); + context.Add(new Bar { Id = "1" }); if (async) { @@ -83,10 +83,16 @@ public TestContext() } public DbSet Foos { get; set; } + public DbSet Bars { get; set; } } private class Foo { public string Id { get; set; } } + + private class Bar + { + public string Id { get; set; } + } } diff --git a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index b567116d4e8..cf221e4cac1 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -953,6 +953,7 @@ public List CreateBatches( bool sensitiveLogging = false) => CreateCommandBatchPreparer(updateAdapter: updateAdapter, sensitiveLogging: sensitiveLogging) .BatchCommands(entries, updateAdapter) + .Select(t => t.Batch) .ToList(); public ICommandBatchPreparer CreateCommandBatchPreparer(