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

Lookup tracked entities by primary key, alternate key, or foreign key #29685

Closed ajcvickers closed 1 year ago

ajcvickers commented 1 year ago

Using the internal indexes that the change tracker already builds for fixup. Currently this can only be done by iterating over all tracked entities.

Split off from #7391.

ajcvickers commented 1 year ago

Benchmarks after implementation in #29686:

Method Mean Error StdDev Median
Search_entries_by_primary_key 233,097.7 ns 3,968.21 ns 3,517.72 ns 231,888.2 ns
DbSet_Find_by_primary_key 190.0 ns 3.77 ns 7.18 ns 186.5 ns
DbSet_Local_FindEntryByKey 164.7 ns 3.27 ns 4.89 ns 162.5 ns
Search_entries_by_alternate_key 257,479.3 ns 2,309.13 ns 1,928.23 ns 258,148.0 ns
DbSet_Local_FindEntryByProperty_alternate_key 276.0 ns 5.54 ns 9.99 ns 272.1 ns
Search_entries_by_foreign_key 878,557.5 ns 4,196.98 ns 3,720.51 ns 879,371.1 ns
DbSet_Local_GetEntriesByProperty_foreign_key 1,493.8 ns 25.26 ns 36.23 ns 1,478.4 ns
Search_entries_by_non_key 877,076.0 ns 7,297.84 ns 6,469.35 ns 875,781.9 ns
DbSet_Local_GetEntriesByProperty_non_key 656,630.7 ns 11,402.99 ns 10,108.45 ns 655,563.9 ns
using System.ComponentModel.DataAnnotations.Schema;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;

BenchmarkRunner.Run<Benchmarks>();

public class Benchmarks
{
    private static readonly TestContext Context;
    private static readonly IProperty AlternateKeyProperty;
    private static readonly IProperty ForeignKeyProperty;
    private static readonly IProperty NonKeyProperty;

    static Benchmarks()
    {
        using (var context = new TestContext())
        {
            context.Seed();
        }

        Context = new TestContext();
        Context.Principals.Include(e => e.Dependent1s).Include(e => e.Dependent2s).Load();
        Context.ChangeTracker.AutoDetectChangesEnabled = false;

        AlternateKeyProperty = Context.Principals.EntityType.FindProperty(nameof(Principal.AltId))!;
        ForeignKeyProperty = Context.Dependent2s.EntityType.FindProperty(nameof(Dependent2.PrincipalId))!;
        NonKeyProperty = Context.Dependent2s.EntityType.FindProperty(nameof(Dependent2.NonKey))!;
    }

    [Benchmark]
    public void DbSet_Find_by_primary_key()
    {
        var entity = Context.Principals.Find(501);
    }

    [Benchmark]
    public void Search_entries_by_primary_key()
    {
        foreach (var entry in Context.ChangeTracker.Entries<Principal>())
        {
            if (entry.Entity.Id == 501)
            {
                return;
            }
        }
    }

    [Benchmark]
    public void DbSet_Local_FindEntryByKey()
    {
        var entry = Context.Principals.Local.FindEntryByKey(501);
    }

    [Benchmark]
    public void Search_entries_by_alternate_key()
    {
        foreach (var entry in Context.ChangeTracker.Entries<Principal>())
        {
            if (entry.Entity.AltId == 501)
            {
                return;
            }
        }
    }

    [Benchmark]
    public void DbSet_Local_FindEntryByProperty_alternate_key()
    {
        var entry = Context.Principals.Local.FindEntryByProperty(AlternateKeyProperty, 501);
    }

    [Benchmark]
    public void Search_entries_by_foreign_key()
    {
        var results = new List<Dependent2>();
        foreach (var entry in Context.ChangeTracker.Entries<Dependent2>())
        {
            if (entry.Entity.PrincipalId == 501)
            {
                results.Add(entry.Entity);
            }
        }
    }

    [Benchmark]
    public void DbSet_Local_GetEntriesByProperty_foreign_key()
    {
        var results = new List<Dependent2>();
        foreach (var entry in Context.Dependent2s.Local.GetEntriesByProperty(ForeignKeyProperty, 501))
        {
            if (entry.Entity.PrincipalId == 501)
            {
                results.Add(entry.Entity);
            }
        }
    }

    [Benchmark]
    public void Search_entries_by_non_key()
    {
        var results = new List<Dependent2>();
        foreach (var entry in Context.ChangeTracker.Entries<Dependent2>())
        {
            if (entry.Entity.NonKey == 501)
            {
                results.Add(entry.Entity);
            }
        }
    }

    [Benchmark]
    public void DbSet_Local_GetEntriesByProperty_non_key()
    {
        var results = new List<Dependent2>();
        foreach (var entry in Context.Dependent2s.Local.GetEntriesByProperty(NonKeyProperty, 501))
        {
            if (entry.Entity.NonKey == 501)
            {
                results.Add(entry.Entity);
            }
        }
    }
}

public class TestContext : DbContext
{
    public DbSet<Principal> Principals { get; set; } = null!;
    public DbSet<Dependent1> Dependent1s { get; set; } = null!;
    public DbSet<Dependent2> Dependent2s { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 
        => optionsBuilder.UseInMemoryDatabase("X");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Principal>()
            .HasMany(e => e.Dependent2s)
            .WithOne(e => e.Principal)
            .HasPrincipalKey(e => e.AltId);
    }

    public void Seed()
    {
        for (var i = 1; i <= 1000; i++)
        {
            var principal = new Principal { Id = i, AltId = i };
            for (var j = 1; j <= 20; j++)
            {
                principal.Dependent1s.Add(new Dependent1 { Id = (i * 20) + j, NonKey = i });
                principal.Dependent2s.Add(new Dependent2 { Id = (i * 20) + j, NonKey = i });
            }

            Add(principal);
        }

        SaveChanges();
    }
}

public class Principal
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int AltId { get; set; }

    public List<Dependent1> Dependent1s { get; } = new();
    public List<Dependent2> Dependent2s { get; } = new();
}

public class Dependent1
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public int? PrincipalId { get; set; }
    public int NonKey { get; set; }
    public Principal? Principal { get; set; }
}

public class Dependent2
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public int? PrincipalId { get; set; }
    public int NonKey { get; set; }
    public Principal? Principal { get; set; }
}