dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.18k forks source link

SaveChangesAsync is slow due to cascade delete #34718

Open petrpavlov opened 1 month ago

petrpavlov commented 1 month ago

We have an entity hierarchy with a root entity that has several collections of dependent entities (the collections are big enough), some of the dependents may have their own dependent entities, and so on. When some entities are removed from the root's collections, the call of SaveChangesAsync is extremely slow. It seems that the slowdown is caused by change detection during cascade delete (because all parents and their navigation collections are checked). But it's possible to disable auto change detection, manually call DetectChanges before SaveChangesAsync and drastically increase performance:

db.ChangeTracker.AutoDetectChangesEnabled = false;
db.ChangeTracker.DetectChanges();
await db.SaveChangesAsync();

Steps to reproduce

using System.Diagnostics;
using Microsoft.EntityFrameworkCore;

var db = new MainDbContext();
var first = new Root
{
    Id = 1,
    Children1 = Enumerable
        .Range(1, 10)
        .Select(i => new Child1
        {
            Id = i,
            RootId = 1,
            Children = Enumerable
                .Range(1, 2000)
                .Select(j => new Child11
                {
                    Id = (i - 1) * 2000 + j,
                    Child1Id = i
                })
                .ToList()
        })
        .ToList(),
    Children2 = Enumerable.Range(1, 30000).Select(i => new Child2 { Id = i, RootId = 1 }).ToList()
};

db.Add(first);
await db.SaveChangesAsync();

var watch = Stopwatch.StartNew();
first.Children1.RemoveAt(first.Children1.Count - 1);
// db.ChangeTracker.AutoDetectChangesEnabled = false;
// db.ChangeTracker.DetectChanges();
await db.SaveChangesAsync();
Console.WriteLine("{0}", watch.Elapsed);

internal class Root
{
    public int Id { get; init; }
    public List<Child1> Children1 { get; init; } = [];
    public List<Child2> Children2 { get; init; } = [];
}

internal class Child1
{
    public int Id { get; init; }
    public int RootId { get; init; }
    public List<Child11> Children { get; init; } = [];
}

internal class Child11
{
    public int Id { get; init; }
    public int Child1Id { get; init; }
}

internal class Child2
{
    public int Id { get; init; }
    public int RootId { get; init; }
}

internal class MainDbContext : DbContext
{
    public DbSet<Root> Root { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("CascadeDeleteTest");
    }
}

Version

EF Core: 8.0.8 Target framework: net8.0 Operating system: Ubuntu 24.04

AndriySvyryd commented 1 month ago

Related to https://github.com/dotnet/efcore/issues/9422 But it might also be possible to fix this by only doing a local detect changes in most cases