简体   繁体   中英

Entity Framework Core Nested List Performance

I struggle with the entity framework core performance while I try to add an additional item to a nested list.

Let's say as an example: I have multiple projects, the project contains multiple houses, the house has multiple facades and the facade has multiple windows. If I now want to add an additional window to a specific project, house, facade I do it like that:

    public async Task SaveWindowAsync(Guid projectId, Guid houseId, Guid facadeId, WindowEntity windowEntity)
    {
        using (ProjectsDbContext context = new ProjectsDbContext())
        {
            var windowList = context.ProjectSet
                .Include(p => p.Houses)
                .ThenInclude(h => h.Facades)
                .ThenInclude(f => f.Windows)
                .First(p => p.Id == projectId).Houses
                .First(h => h.Id == houseId).Facades
                .First(f => f.Id == facadeId).Windows;

            windowList.Add(windowEntity);

            await context.SaveChangesAsync();
        }
    }

This works fine regarding the functionality. However the performance gets slower and slower when the database is increasing. Is there a more performant way to add an item to a nested list?

Update 1

I created a simple test database with this futuristic objects with 50 projects, each project has 10 houses, each house has 10 facades and each facade has 10 windows. this results in a database size of about 10Mb.

In the test I add 1000 Windows after each other (no bulk):

The solution mention above requires a total time of 145s.

The solution mentioned by @David Browne - Microsoft takes about 54s

var facadeEntity = context.Set<FacadeEntity>()
    .Include(f => f.Windows)
    .Single(f => f.Id == facadeId);

facadeEntity.Windows.Add(windowEntity);

await context.SaveChangesAsync();

Update 2

As recommended by @David Browne I added a ForeignKey to the window:

modelBuilder.Entity<FacadeEntity>()
.HasMany(f => f.Windows).WithOne()
.HasForeignKey(f => f.FacadeId)
.OnDelete(DeleteBehavior.Cascade);

The save is executed like that:

context.Entry(windowEntity).Property(nameof(WindowEntity.FacadeId)).CurrentValue = facadeId;
context.Set<WindowEntity>().Add(windowEntity);
await context.SaveChangesAsync();

This issue is the same the more windows I have the longer the add takes. The durartion for 1000 Windows is around 53s.

I currently have only a "DbSet ProjectSet" you would add an additional DbSet to the context?

If you don't have declared DbSet<T> for an entity, access it through DbContext.Set<T>() , something like:

public static async Task SaveWindowAsync(Guid projectId, Guid houseId, Guid facadeId, Window windowEntity)
{
    using (ProjectsDbContext context = new ProjectsDbContext())
    {
        var facade = context.Set<Facade>()
                            .Where(f => f.FacadeId == facadeId)
                            .Single();

        facade.Windows.Add(windowEntity);

        await context.SaveChangesAsync();
    }
}

This translates to:

SELECT TOP(2) [f].[FacadeId], [f].[HouseId]
FROM [Facade] AS [f]
WHERE [f].[FacadeId] = @__facadeId_0

and then:

INSERT INTO [Window] ([WindowId], [FacadeId])
VALUES (@p0, @p1)

Assuming Facade has a single-column primary key. If it has a compound key of (ProjectId,HouseId,FacadeId), then add those to the Where .

The best way to do this, however is to set the Foreign Key property of Window.FacadeId and not load the Facade at all. In EF Core you can do this with Shadow Properties if you don't have a Foreign Key Property. EG:

public static async Task SaveWindowAsync(Guid projectId, Guid houseId, Guid facadeId, Window windowEntity)
{
    using (ProjectsDbContext context = new ProjectsDbContext())
    {
        context.Entry(windowEntity).Property("FacadeId").CurrentValue = facadeId;
        context.Set<Window>().Add(windowEntity);

        await context.SaveChangesAsync();
    }
}

If you're trying to add a Window to a specific project/house/facade, can you just set the FacadeId on the WindowEntity directly and save that? Presumably, the Window has a FacadeId property, just as Facade has a HouseId, and House has a ProjectId. If Window has the Ids of all its parents (unnecessary), then just set the House and Project Ids as well.

public async Task SaveWindowAsync(Guid projectId, Guid houseId, Guid facadeId, WindowEntity windowEntity)
{
    using (ProjectsDbContext context = new ProjectsDbContext())
    {            
        windowEntity.FacadeId = facadeId;
        context.WindowSet.Add(windowEntity);
        await context.SaveChangesAsync();
    }
}

Update: If you don't want to set the facadeId on the entity directly, then you can load just the facade, and set its Facade property rather than the Id. You can optionally add the windowEntity to the Facade's Windows collection as well if you are going to continue working with that instance of the collection.

public async Task SaveWindowAsync(Guid facadeId, WindowEntity windowEntity)
{
    using (ProjectsDbContext context = new ProjectsDbContext())
    {
        var facade = Facades.Single(x => x.Id == facadeId);
        windowEntity.Facade = facade;
        facade.Windows.Add(windowEntity);
        await context.SaveChangesAsync();
    }
}

You should firstly select from your Windows DbSet then join Facades then join houses your query should looks like that

 var windowList = context.DbSet<Windows>().Where(w => w.Facade.Id== facadeId && w.Facade.House.Id == houseId && w.Facade.House.Project.Id == projectId)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM