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.69k stars 3.17k forks source link

Avoid generating discriminator for TPC query returning entities of a single leaf type #28199

Open ajcvickers opened 2 years ago

ajcvickers commented 2 years ago

This is the kind of query that TPC mapping is really good for, so it would be nice to have really clean generated SQL for it. Used latest daily build as of this morning, UK time.

Query:

var results3 = context.Animals.OfType<Cat>().Where(a => a.Species.StartsWith("F")).ToList();

SQL:

      SELECT [t].[Id], [t].[Species], [t].[Name], [t].[EducationLevel], [t].[Discriminator]
      FROM (
          SELECT [c].[Id], [c].[Species], [c].[Name], [c].[EducationLevel], N'Cat' AS [Discriminator]
          FROM [Cats] AS [c]
      ) AS [t]
      WHERE [t].[Species] LIKE N'F%'

Full repro:

public abstract class Animal
{
    public int Id { get; set; }
    public string Species { get; set; }
}

public class FarmAnimal : Animal
{
    public decimal Value { get; set; }
}

public class Pet : Animal
{
    public string Name { get; set; }
}

public class Cat : Pet
{
    public string EducationLevel { get; set; }
}

public class Dog : Pet
{
    public string FavoriteToy { get; set; }
}

public class SomeDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(Your.ConnectionString)
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    public DbSet<Animal> Animals { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasSequence<int>("AnimalIds");

        modelBuilder.Entity<Animal>().UseTpcMappingStrategy().Property(e => e.Id).HasDefaultValueSql("NEXT VALUE FOR [AnimalIds]");
        modelBuilder.Entity<Pet>().ToTable("Pets");;
        modelBuilder.Entity<Cat>().ToTable("Cats");
        modelBuilder.Entity<Dog>().ToTable("Dogs");
        modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals");

    }
}

public class Program
{
    public static void Main()
    {
        using (var context = new SomeDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.AddRange(
                new Cat {Name = "Alice", Species = "Felis catus", EducationLevel = "MBA"},
                new Cat {Name = "Mac", Species = "Felis catus", EducationLevel = "BA"},
                new Dog {Name = "Toast", Species = "Canis familiaris", FavoriteToy = "Mr. Squirrel"},
                new FarmAnimal {Value = 100.0m, Species = "Ovis aries"});

            context.SaveChanges();
        }

        using (var context = new SomeDbContext())
        {
            var results3 = context.Animals.OfType<Cat>().Where(a => a.Species.StartsWith("F")).ToList();
        }
    }
}
roji commented 2 years ago

Splitting the unneeded subquery issue to #28214 as discussed.

ajcvickers commented 2 years ago

Note: this only happens when using OfType. When using a DbSet for the requested type, the query is:

SELECT [c].[Id], [c].[Species], [c].[Name], [c].[EducationLevel]
FROM [Cats] AS [c]
WHERE [c].[Species] LIKE N'F%'