Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SaveChangesAsync is slow due to cascade delete #34718

Open
petrpavlov opened this issue Sep 19, 2024 · 1 comment
Open

SaveChangesAsync is slow due to cascade delete #34718

petrpavlov opened this issue Sep 19, 2024 · 1 comment

Comments

@petrpavlov
Copy link

petrpavlov commented Sep 19, 2024

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
Copy link
Member

Related to #9422
But it might also be possible to fix this by only doing a local detect changes in most cases

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants