【问题标题】:Automapper ProjectTo<> not working with Count()Automapper ProjectTo<> 不适用于 Count()
【发布时间】:2021-02-12 08:56:44
【问题描述】:

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&lt;&gt;

编辑
执行计划:

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; }
}

ViewModel -

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], [c].[Name], [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], [c].[Name], [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 的帮助。

【问题讨论】:

  • 好问题,我不包括 5 版本。 Microsoft.EntityFrameworkCore:3.1.12,Microsoft.EntityFrameworkCore.SqlServer:3.1.12,Microsoft.EntityFrameworkCore.Tools:3.1.12
  • 由于技术要求,我无法迁移到 5.0。我将尝试将自动映射器回滚几个版本。
  • 我不认为 AM 有问题 :) 在 5.0 上进行测试至少会告诉您问题是否已修复。
  • @Rtype 因此,如果您不使用 ProjectTo&lt;&gt; 并使用带有 .Select() 的手动投影,您的第二个查询会产生预期的结果?如果是这样的话,你能分享那个手动投影查询吗?
  • 问题是你让 EF 认为 Status 是必需的,这意味着 [c].StatusId 不能为空并且内部连接是合理的。因此,要么使StatusId 不为空,要么添加一个谓词来获取具有状态的Custormers。永远不要将连接用作隐蔽谓词。

标签: sql-server automapper asp.net-core-3.1 entity-framework-core-3.1


【解决方案1】:

我已经用Entity Framework Core 3.1AutoMapper 10.1.1.NET Core 3.1 中测试了你的代码。还有-

  1. 您的第一个查询会生成 LEFT JOIN,而不是您发布的 INNER JOIN。因此,该查询的结果不会排除任何具有空 StatusId 的客户。并且,生成的 SQL 与 ProjectTo&lt;&gt; 和手动 EF 投影相同。我建议再次检查您的查询并生成 SQL 以确保。

  2. 您的第二个查询生成相同的 SQL,即您发布的 SQL,带有 ProjectTo&lt;&gt; 和手动 EF 投影。

为您提供解决方案:
如果我理解正确,您正在尝试获取 -

  1. Customer的列表,在指定范围内,谁有相关的Status
  2. 数据库中所有此类客户的计数。

试试下面的 -

  1. 在您的 Customer 模型中添加可为空的外键属性 -
public Guid? StatusId { get; set; }

这将有助于简化您的查询及其生成的 SQL。

  1. 要获得预期的列表,请将第一个查询修改为 -
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
  1. 要获得预期计数,请将第二个查询修改为 -
var count = Db.Customers
            .Where(p => p.StatusId != null)
            .Count();

它将生成以下SQL -

SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[StatusId] IS NOT NULL

【讨论】:

  • 你仍然可以使用ProjectTo,然后你就不需要Include了。
  • @Atiyar,很棒的实际例子,谢谢!我同意你可以做那里所做的事情,即手动排除状态。会更多地研究它。我认为这是一种变通方法,而不是一种解决方案。
  • 在深入研究之后,似乎这是 EF 团队的设计选择。感谢@atiyar 花时间提供解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-09
  • 2017-04-25
  • 2019-07-24
  • 2021-07-29
  • 1970-01-01
相关资源
最近更新 更多