用于映射投影生成的AutoMapper SQL优化(不可为null到可为null)
下面是反映问题的单元测试代码::用于映射投影生成的AutoMapper SQL优化(不可为null到可为null),sql,optimization,automapper,projection,Sql,Optimization,Automapper,Projection,下面是反映问题的单元测试代码:: public class NonNullableToNullable : AutoMapperSpecBase { public class Customer { [Key] [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } public str
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))
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)
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))
如果在映射时强制转换为null
.ForMember(d => d.GradeId, opt => opt.MapFrom(e => (int?)e.Category.GradeId))
生成的SQL将符合预期:
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))
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)
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))
选择
[Extent1].[Id]作为[Id],
[Extent1]。[CategoryId]作为[CategoryId],
[Extent2]。[Name]作为[Name],
[Extent1]。[FirstName]作为[FirstName],
[Extent2]。[GradeId]作为[GradeId],
[Extent3]。[Name]作为[Name1],
[Extent1].[LastName]作为[LastName]
来自[dbo].[Customers]作为[Extent1]
将[dbo].[CustomerCategories]作为[Extent1].[CategoryId]=[Extent2].[Id]上的[Extent2]进行内部联接
内部联接[dbo].[CustomerGrades]为[Extent2].[GradeId]=[Extent3].[Id]上的[Extent3]
其中1=[Extent2].[GradeId]
对象查询
.MergeAs(仅限MergeOption.AppendOnly)
.选择(
dtoCustomer=>新的非NullTableToNullable.CustomServiceWModel
{
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
})
.其中(a=>a.GradeId==((int?)1))
所以我的问题是,当目标类型可为null时,为什么automapper强制转换在默认情况下不可为null,或者是否有任何配置可以在不进行手动转换的情况下执行此操作?
ProjectTo
必须是链中的最后一个调用。EF使用实体,而不是DTO。因此,在实体上应用任何筛选和排序,并作为最后一步,投影到DTO。谢谢您的回答,有些场景是正确的,但有些场景基于DTO筛选更方便。您误用了ProjectTo
,然后抱怨结果。这不是关于你想要什么,而是关于什么是可能的。您也可以尝试表达式映射,但最终EF也会做同样的事情。