EF Core 2.1 多次数据库调用

4

我能否防止 EF Core 在单个枚举函数调用中进行多次数据库往返?

考虑这个相对简单的 LINQ 表达式:

var query2 = context.CheckinTablets.Select(ct => new
            {
                Id = ct.Id,
                DeviceName = ct.Name,
                Status = ct.CheckinTabletStatuses
                    .OrderByDescending(cts => cts.TimestampUtc).FirstOrDefault()
            }).ToList();

过去的期望是,“一个枚举调用对应一个数据库调用”(如果禁用延迟加载)。在EF Core中,这种情况不再适用!
在EF 6.2.0中,此LINQ被翻译为:
SELECT [Extent1].[CheckinTabletID] AS [CheckinTabletID],
   [Limit1].[TimestampUtc] AS [TimestampUtc]
  --...
FROM [dbo].[CheckinTablet] AS [Extent1] OUTER APPLY (
SELECT TOP (1) [Project1].[CheckinTabletStatusID] AS [CheckinTabletStatusID],
               [Project1].[CheckinTabletID] AS [CheckinTabletID],
               [Project1].[TimestampUtc] AS [TimestampUtc]
FROM (
SELECT [Extent2].[CheckinTabletStatusID] AS [CheckinTabletStatusID],
       [Extent2].[CheckinTabletID] AS [CheckinTabletID],
       [Extent2].[TimestampUtc] AS [TimestampUtc]
     --...
FROM [dbo].[CheckinTabletStatus] AS [Extent2]
WHERE [Extent1].[CheckinTabletID] = [Extent2].[CheckinTabletID]
) AS [Project1] ORDER BY [Project1].[TimestampUtc] DESC
) AS [Limit1];

虽然相当丑陋,但它很好地遵循了POLA。更重要的是,我们可以使用它来优化数据库端(索引)。
使用EF Core 2.1.0,我们会得到类似于以下内容:
SELECT [ct].[CheckinTabletID] AS [Id], [ct].[strName] AS [DeviceName] FROM [CheckinTablet] AS [ct]

exec sp_executesql N'SELECT TOP(1) [cts].[CheckinTabletStatusID], [cts].[CheckinTabletID], [cts].[TimestampUtc] FROM [CheckinTabletStatus] AS [cts] WHERE @_outer_Id = [cts].[CheckinTabletID] ORDER BY [cts].[TimestampUtc] DESC',N'@_outer_Id int',@_outer_Id=1

exec sp_executesql N'SELECT TOP(1) [cts].[CheckinTabletStatusID], [cts].[CheckinTabletID], [cts].[TimestampUtc] FROM [CheckinTabletStatus] AS [cts] WHERE @_outer_Id = [cts].[CheckinTabletID] ORDER BY [cts].[TimestampUtc] DESC',N'@_outer_Id int',@_outer_Id=2

exec sp_executesql N'SELECT TOP(1) [cts].[CheckinTabletStatusID], [cts].[CheckinTabletID], [cts].[TimestampUtc] FROM [CheckinTabletStatus] AS [cts] WHERE @_outer_Id = [cts].[CheckinTabletID] ORDER BY [cts].[TimestampUtc] DESC',N'@_outer_Id int',@_outer_Id=3

exec sp_executesql N'SELECT TOP(1) [cts].[CheckinTabletStatusID], [cts].[CheckinTabletID], [cts].[TimestampUtc] FROM [CheckinTabletStatus] AS [cts] WHERE @_outer_Id = [cts].[CheckinTabletID] ORDER BY [cts].[TimestampUtc] DESC',N'@_outer_Id int',@_outer_Id=4

exec sp_executesql N'SELECT TOP(1) [cts].[CheckinTabletStatusID], [cts].[CheckinTabletID], [cts].[TimestampUtc] FROM [CheckinTabletStatus] AS [cts] WHERE @_outer_Id = [cts].[CheckinTabletID] ORDER BY [cts].[TimestampUtc] DESC',N'@_outer_Id int',@_outer_Id=5

是的,这需要先获取所有实体(CheckinTablets)的一个调用,然后对每个实体的行进行调用以获取状态...

因此,在一个调用中,ToList() Entity Framework 对数据库进行了 n+1 次调用。这是非常不可取的,有没有一种方法可以禁用此行为或解决方法?

编辑 1:

.Include() 不能解决这个问题... 它仍然会进行 n+1 次数据库请求。

编辑 2(由 @jmdon 提供信用):

不返回对象而是简单值只需要一次调用!当然,如果您不想扁平化实体,或者想从第二个表中获取多个值,则这并不真正有所帮助。尽管如此,这也是很好知道的!

var query2 = _context.CheckinTablets.Select(ct => new
{
    Id = ct.Id,
    DeviceName = ct.Name,
    Status = new CheckinTabletStatus
    {
        Id = ct.CheckinTabletStatuses.OrderByDescending(cts => cts.TimestampUtc).FirstOrDefault().Id,
        CheckinTabletId = ct.CheckinTabletStatuses.OrderByDescending(cts => cts.TimestampUtc).FirstOrDefault().CheckinTabletId,
    }
}).ToList();

产生一次数据库调用:

SELECT [ct].[intCheckinTabletID] AS [Id0],
   [ct].[strName] AS [DeviceName],
(
    SELECT TOP (1) [cts].[intCheckinTabletStatusID]
    FROM [tCheckinTabletStatus] AS [cts]
    WHERE [ct].[intCheckinTabletID] = [cts].[intCheckinTabletID]
    ORDER BY [cts].[dtmTimestampUtc] DESC
) AS [Id],
(
    SELECT TOP (1) [cts0].[intCheckinTabletID]
    FROM [tCheckinTabletStatus] AS [cts0]
    WHERE [ct].[intCheckinTabletID] = [cts0].[intCheckinTabletID]
    ORDER BY [cts0].[dtmTimestampUtc] DESC
) AS [CheckinTabletId]
FROM [tCheckinTablet] AS [ct];

你尝试使用Include()了吗?它有什么不同吗? - Hany Habib
是的,我做了...但它并没有起到帮助的作用。 - Igor
2
可能你需要等待未来的版本。在2.1中,他们优化了相关集合投影(当您使用ToList()选择时),但不是像这样的查询(使用subcollection.FirstOrDefault())。 - Ivan Stoev
是的,我认为@IvanStoev是正确的,不幸的是。如果您不想压平结果,则可能需要进行两次调用,并在内存中将两个结果连接在一起。 - jmdon
2个回答

4

我在.Net Conf 2018期间向Diego Vega和Smit Patel提出了以下问题... 这是他们的回答(经过改编)。

EF Core不仅适用于关系型数据库...客户不希望看到异常,如果某些内容无法转换为SQL..."如果需要多个查询,那没关系"...默认情况下启用每个枚举的多个查询。如果发生这种情况,将输出警告系统。他们正在考虑添加一种方法,将警告升级为异常,如果执行多个往返。他们正在努力优化(n+1)查询到基于数据结构的少量(固定大小)查询。

可以通过将此内容添加到OnConfiguring方法中来强制EF Core在客户端评估查询部分时抛出异常。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;")
        .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
}

更多信息: https://learn.microsoft.com/en-us/ef/core/querying/client-eval

2

我注意到当你尝试返回嵌套对象时,它会这样做。

您可以尝试在投影中展平Status对象,例如:


```javascript 您可以尝试在投影中展平Status对象,例如: ```
var query2 = context.CheckinTablets.Select(ct => new
        {
            Id = ct.Id,
            DeviceName = ct.Name,
            StatusName = ct.CheckinTabletStatuses
                .OrderByDescending(cts => cts.TimestampUtc).FirstOrDefault().Name
        }).ToList();

很好的发现@jmdon!如果您需要从第二个表中获取一个(几个)值以上,则无法帮助,但是非常好了解!更新OP。 - Igor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接