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

Thanksgiving project: lookup entities by primary key, alternate key, or foreign key #29686

Merged
merged 3 commits into from
Dec 3, 2022

Conversation

ajcvickers
Copy link
Member

Fixes #29685.

@ajcvickers ajcvickers requested a review from a team November 27, 2022 13:15
@ajcvickers
Copy link
Member 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
Member 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.

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