KEMBAR78
Thanksgiving project: lookup entities by primary key, alternate key, or foreign key by ajcvickers · Pull Request #29686 · dotnet/efcore · GitHub
Skip to content

Conversation

@ajcvickers
Copy link
Contributor

Fixes #29685.

@ajcvickers ajcvickers requested a review from a team November 27, 2022 13:15
@ajcvickers
Copy link
Contributor Author

Benchmarks:

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; }
}

Comment on lines +605 to +608
/// <para>
/// Call <see cref="ChangeTracker.DetectChanges" /> before calling this method to ensure all entries returned reflect
/// up-to-date state.
/// </para>
Copy link
Member

@AndriySvyryd AndriySvyryd Dec 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need something in the API to indicate whether a DetectChanges is needed for any particular call. Or perhaps we just call it always ourselves unless the user opts-out temporarily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do this in another PR so that it can be reviewed.

@ajcvickers ajcvickers merged commit 4dded73 into main Dec 3, 2022
@ajcvickers ajcvickers deleted the FindThatTurkey1123 branch December 3, 2022 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants