问题描述
我对 AutoMapper
有一个奇怪的问题(我使用的是 .NET core 3.1 和 AutoMapper 10.1.1)
var data = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.ProjectTo<Customerviewmodel>(Mapper.ConfigurationProvider)
.ToList();
var count = Db.Customers
.ProjectTo<Customerviewmodel>(Mapper.ConfigurationProvider)
.Count();
第一行创建预期的 sql:
exec sp_executesql N'SELECT [c].[Code],[c].[Id],[c].[Name],[c].[Website],[s].Name
FROM [Customers] AS [c]
INNER JOIN [Status] AS [s] ON [s].id = [c].StatusId
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY',N'@__p_0 int,@__p_1 int',@__p_0=0,@__p_1=25
第二行,Count()。似乎完全忽略了投影:
SELECT COUNT(*)
FROM [Customers] AS [c]
这样做的结果是,任何具有 null StatusId
的客户都将被排除在第一个查询之外,但包含在第二个查询中的计数中。这会破坏分页。
我本以为该项目应该创建如下内容:
SELECT COUNT(*)
FROM [Customers] AS [c]
INNER JOIN [Status] AS [s] ON [s].id = [c].StatusId
有人知道为什么 Count() 会忽略 ProjectTo<>
吗?
编辑
执行计划:
value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Domain.Customer]).Select(dtoCustomer => new Customerviewmodel() { Code = dtoCustomer.Code,Id = dtoCustomer.Id,Name = dtoCustomer.Name,StatusName = dtoCustomer.Status.Name,网站 = dtoCustomer.Website})
编辑 2021/02/19
映射计划:
EF 实体 -
public class Customer
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public string Code { get; private set; }
public string Website { get; private set; }
public CustomerStatus Status { get; private set; }
public Customer() { }
}
public class CustomerStatus
{
public Guid Id { get; private set; }
public string Name { get; private set; }
}
视图模型 -
public class Customerviewmodel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string Website { get; set; }
public string StatusName { get; set; }
}
映射 -
CreateMap<Customer,Customerviewmodel>();
编辑 2021/02/20 - 手动排除状态
正如@atiyar 回答中所指出的,您可以手动排除状态。这让我觉得是一种解决方法。我的推理是这样的:
Db.Customers.ProjectTo<Customerviewmodel>(_mapper.ConfigurationProvider)
你得到:
exec sp_executesql N'SELECT TOP(@__p_0) [c].[Id],[c0].[Name]
AS [StatusName]
FROM [Customers] AS [c]
INNER JOIN [CustomerStatus] AS [c0] ON [c].[StatusId] = [c0].[Id]',N'@__p_0
int',@__p_0=5
这表明 automapper 理解并可以看到 Status 和 Customer 之间存在所需的关系。但是当你应用计数机制时:
Db.Customers.ProjectTo<Customerviewmodel>(_mapper.ConfigurationProvider).Count()
突然之间,Status 和 Customer 之间的理解关系丢失了。
SELECT COUNT(*)
FROM [Customers] AS [c]
根据我使用 Linq 的经验,每个查询步骤都以可预测的方式修改前一步。我本来希望计数建立在第一个命令的基础上,并将计数作为其中的一部分。
有趣的是,如果您执行此操作:
_context.Customers.ProjectTo<Customerviewmodel>(_mapper.ConfigurationProvider).Take(int.MaxValue).Count()
Automapper 应用了关系,结果正是我所期望的:
exec sp_executesql N'SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [c].[Id],[c0].[Name] AS [Name0],[c0].[Id]
AS [Id0]
FROM [Customers] AS [c]
INNER JOIN [CustomerStatus] AS [c0] ON [c].[StatusId] = [c0].[Id]
) AS [t]',N'@__p_0 int',@__p_0=2147483647
编辑 2021/02/20 - 最新版本
在最新版本中似乎行为相同。
仅供参考:我们有一个场景,记录是从另一个应用程序定期导入的。我们希望使用内连接来排除在另一个表中没有匹配记录的记录。然后这些记录将在稍后由导入过程更新。
但从应用程序的角度来看,它应该始终忽略这些记录,因此内部连接和状态是强制性的。但是我们必须使用 where 手动排除它们(根据 atiyar 的解决方案),以防止分页返回溢出的页数。
编辑 2021/02/20 - 进一步挖掘 这似乎是 EF 团队的设计选择和优化。这里的假设是,如果关系是非空的。然后连接不会作为性能提升包括在内。解决这个问题的方法是@atiyar 所建议的。感谢大家@atiyar 和@Lucian-Bargaoanu 的帮助。
解决方法
我已使用 .NET Core 3.1
和 Entity Framework Core 3.1
在 AutoMapper 10.1.1
中测试了您的代码。还有 -
-
您的第一个查询生成的是
LEFT JOIN
,而不是您发布的INNER JOIN
。因此,该查询的结果不会排除任何StatusId
为空的客户。并且,生成的 SQL 与ProjectTo<>
和手动 EF 投影相同。我建议再次检查您的查询并生成 SQL 以确保。 -
您的第二个查询生成相同的 SQL,即您发布的 SQL,带有
ProjectTo<>
和手动 EF 投影。
适合您的解决方案:
如果我理解正确,您正在尝试获取 -
- 在指定范围内具有相关
Customer
的Status
列表 - 数据库中所有此类客户的数量。
尝试以下操作 -
- 在您的
Customer
模型中添加可为空的外键属性 -
public Guid? StatusId { get; set; }
这将有助于简化您的查询及其生成的 SQL。
- 要获得预期的列表,请将第一个查询修改为 -
var viewModels = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.Where(p => p.StatusId != null)
.ProjectTo<CustomerViewModel>(_Mapper.ConfigurationProvider)
.ToList();
它会生成以下SQL -
exec sp_executesql N'SELECT [t].[Code],[t].[Id],[t].[Name],[s].[Name] AS [StatusName],[t].[Website]
FROM (
SELECT [c].[Id],[c].[Code],[c].[Name],[c].[StatusId],[c].[Website]
FROM [Customers] AS [c]
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
) AS [t]
LEFT JOIN [Statuses] AS [s] ON [t].[StatusId] = [s].[Id]
WHERE [t].[StatusId] IS NOT NULL',N'@__p_0 int,@__p_1 int',@__p_0=0,@__p_1=25
- 要获得预期的计数,请将第二个查询修改为 -
var count = Db.Customers
.Where(p => p.StatusId != null)
.Count();
它会生成以下SQL -
SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[StatusId] IS NOT NULL