【问题标题】:AutoMapper SQL optimization for mapping projection generation(NonNullable To Nullable)映射投影生成的 AutoMapper SQL 优化(NonNullable To Nullable)
【发布时间】:2020-10-10 19:33:41
【问题描述】:

这是反映问题的单元测试代码:

 public class NonNullableToNullable : AutoMapperSpecBase
    {
        public class Customer
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }

            public int? CategoryId { get; set; }

            public virtual CustomerCategory Category { get; set; }
        }


        public class CustomerCategory
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; }
            public string Name { get; set; }

            public int GradeId { get; set; }

            public virtual CustomerGrade Grade { get; set; }
        }


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

            public string Name { get; set; }
        }

        public class CustomerViewModel
        {

            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }

            public int? CategoryId { get; set; }

            public string CategoryName { get; set; }

            public int? GradeId { get; set; }

            public string GradeName { get; set; }
        }

        public class Context : DbContext
        {
            public Context()
            {
                Database.SetInitializer<Context>(new DatabaseInitializer());
            }

            public DbSet<Customer> Customers { get; set; }

            public DbSet<CustomerCategory> CustomerCategories { get; set; }

            public DbSet<CustomerGrade> CustomerGrades { get; set; }

            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Customer>()
                    .HasOptional(a => a.Category).WithMany().HasForeignKey(a => a.CategoryId);

                modelBuilder.Entity<CustomerCategory>().HasKey(a => a.Id)
                    .HasRequired(a => a.Grade).WithMany().HasForeignKey(a => a.GradeId);

                modelBuilder.Entity<CustomerGrade>().HasKey(a => a.Id);

                base.OnModelCreating(modelBuilder);

            }
        }

        public class DatabaseInitializer : DropCreateDatabaseAlways<Context>
        {
            protected override void Seed(Context context)
            {
                context.CustomerGrades.Add(new CustomerGrade() { Id = 1, Name = "A" });

                context.CustomerCategories.Add(new CustomerCategory() { Id = 10, GradeId = 1, Name = "Category 1" });


                context.Customers.Add(new Customer
                {
                    Id = 100,
                    FirstName = "Bob",
                    LastName = "Smith",
                });

                context.Customers.Add(new Customer
                {
                    Id = 101,
                    FirstName = "Tom",
                    LastName = "Thomas",
                    CategoryId = 10
                });

                base.Seed(context);
            }
        }

        protected override MapperConfiguration Configuration => new MapperConfiguration(cfg =>
        {
           
            cfg.CreateMap<Customer, CustomerViewModel>()
                .ForMember(d => d.CategoryName, opt => opt.MapFrom(e => e.Category.Name))
                // If you forget to cast to nullable, the resulting SQL will look terrible
                //.ForMember(d => d.GradeId, opt => opt.MapFrom(e => e.Category.GradeId))
                .ForMember(d => d.GradeId, opt => opt.MapFrom(e => (int?)e.Category.GradeId))
                
                .ForMember(d => d.GradeName, opt => opt.MapFrom(e => e.Category.Grade.Name))

                ;

            
        });

        [Fact]
        public void Can_map_with_projection()
        {
            

            using (var context = new Context())
            {
                var query = ProjectTo<CustomerViewModel>(context.Customers).Where(a => a.GradeId == 1);


                var model = query.First();

                model.Id.ShouldBe(101);

                model.FirstName.ShouldBe("Tom");
                model.LastName.ShouldBe("Thomas");
            }
        }
    }

可读的地图表达式:

    ObjectQuery<NonNullableToNullable.Customer>
    .MergeAs(MergeOption.AppendOnly)
    .Select(
        dtoCustomer => new NonNullableToNullable.CustomerViewModel
        {
            CategoryId = dtoCustomer.CategoryId,
            CategoryName = dtoCustomer.Category.Name,
            FirstName = dtoCustomer.FirstName,
            GradeId = (dtoCustomer.Category == null) ? null : (int?)dtoCustomer.Category.GradeId,
            GradeName = dtoCustomer.Category.Grade.Name,
            Id = dtoCustomer.Id,
            LastName = dtoCustomer.LastName
        })
    .Where(a => a.GradeId == ((int?)1))

错误的 sql(不能使用索引):

SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[CategoryId] AS [CategoryId], 
    [Extent2].[Name] AS [Name], 
    [Extent1].[FirstName] AS [FirstName], 
--The generated SQL is a little redundant
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE [Extent2].[GradeId] END AS [C1], 
    [Extent3].[Name] AS [Name1], 
    CASE WHEN (1 = 0) THEN cast(0 as bigint) ELSE [Extent1].[Id] END AS [C2], 
    [Extent1].[LastName] AS [LastName]
    FROM   [dbo].[Customers] AS [Extent1]
    LEFT OUTER JOIN [dbo].[CustomerCategories] AS [Extent2] ON [Extent1].[CategoryId] = [Extent2].[Id]
    LEFT OUTER JOIN [dbo].[CustomerGrades] AS [Extent3] ON [Extent2].[GradeId] = [Extent3].[Id]
 --The WHERE condition cannot use an index
    WHERE 1 = (CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE [Extent2].[GradeId] END)

如果在映射时强制转换为可空

 .ForMember(d => d.GradeId, opt => opt.MapFrom(e => (int?)e.Category.GradeId))

生成的 SQL 将符合预期:

SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[CategoryId] AS [CategoryId], 
    [Extent2].[Name] AS [Name], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent2].[GradeId] AS [GradeId], 
    [Extent3].[Name] AS [Name1], 
    [Extent1].[LastName] AS [LastName]
    FROM   [dbo].[Customers] AS [Extent1]
    INNER JOIN [dbo].[CustomerCategories] AS [Extent2] ON [Extent1].[CategoryId] = [Extent2].[Id]
    INNER JOIN [dbo].[CustomerGrades] AS [Extent3] ON [Extent2].[GradeId] = [Extent3].[Id]
    WHERE 1 = [Extent2].[GradeId]


    ObjectQuery<NonNullableToNullable.Customer>
    .MergeAs(MergeOption.AppendOnly)
    .Select(
        dtoCustomer => new NonNullableToNullable.CustomerViewModel
        {
            CategoryId = dtoCustomer.CategoryId,
            CategoryName = dtoCustomer.Category.Name,
            FirstName = dtoCustomer.FirstName,
            GradeId = (int?)dtoCustomer.Category.GradeId,
            GradeName = dtoCustomer.Category.Grade.Name,
            Id = dtoCustomer.Id,
            LastName = dtoCustomer.LastName
        })
    .Where(a => a.GradeId == ((int?)1))

所以我的问题是,当目标类型可以为空时,为什么 automapper 默认不能为空,或者是否有任何配置可以在不手动转换的情况下做到这一点?

【问题讨论】:

    标签: sql optimization automapper projection


    【解决方案1】:

    ProjectTo 必须是链中的最后一个调用。 EF 适用于实体,而不是 DTO。因此,对实体应用任何过滤和排序,最后一步,投影到 DTO。

    【讨论】:

    • 谢谢你的回答,有些场景是对的,但是有些场景基于DTO过滤更方便。
    • 您在滥用ProjectTo,然后抱怨结果。这不是关于你想要什么,而是关于什么是可能的。您也可以尝试表达式映射,但最终 EF 会做同样的事情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-27
    • 2021-10-22
    • 1970-01-01
    相关资源
    最近更新 更多