Entity Framework 创建核心 LINQ 问题(select 存在案例)查询

Entity Framework Core LINQ trouble creating (select case exists) queries

我正在尝试列出所有项目,并在其中添加一个额外的列来描述它是否为当前用户所有。

所以我正在寻找一个生成如下内容的 Linq 查询 SQL:

SELECT *,
   CASE WHEN
     EXISTS (
       SELECT NULL FROM OwnedItems
       WHERE OwnedItems.UserId = @UserId AND OwnedItems.ItemId = Items.Id
   )
   THEN 'true'
   ELSE 'false'
   END AS Owned
FROM Items;

根据互联网以及成功的 LinqPad 实验,此代码应该有效。

from item in Items
select new
{
   Owned = OwnedItems.Any(own => own.UserId == userId && own.ItemId == item.Id),
   Item = item
}

在 LinqPad 中,此代码生成与我想要的完全相同的 SQL。但在我的项目中,它做了完全不同的事情。

我的代码是一个使用 Entity Framework Core 2.1 的 .Net Core 2.1 项目。由于它是一个核心项目,我不能直接在 LinqPad 中测试它,因为它还不受支持。

在我的项目中,此代码生成一个未过滤的 SELECT 语句来查询每个项目,然后对每个项目进行单独的查询以检查它是否存在于 OwnedItems table 中。像这样:

此查询的 1 个实例 运行s:

Executed DbCommand (68ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
  SELECT *
  FROM [Items] AS [item]

紧随其后的是数百个这样的查询,这些查询花费了数秒的时间 运行:

 Executed DbCommand (32ms) [Parameters=[@__userId_0='?' (DbType = Int32), @_outer_Id='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
  SELECT CASE
      WHEN EXISTS (
          SELECT 1
          FROM [OwnedItems] AS [ownedItems]
          WHERE ([ownedItems].[UserId] = @__userId_0) AND ([ownedItems].[ItemId] = @_outer_Id))
      THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
  END

一些进一步的信息,也许有帮助: 如果我使用同一行作为 where 子句的一部分,它会完美地工作。

var q = from item in Items
    where OwnedItems.Any(o => o.UserId == userId && o.ItemId == item.Id)
    select item;

上面的 linq 结果很好 sql:

SELECT *
  FROM [Items] AS [item]
  WHERE EXISTS (
      SELECT 1
      FROM [OwnedItems] AS [o]
      WHERE ([o].[UserId] = @__userId_0) AND ([o].[ItemId] = [item].[Id]))

备注:

解决方案更新

正如@KorsG 所指出的,如果项目未具体化,则会生成正确的查询。 我发现即使我写了以下内容,也没有具体化项目:

from item in Items
select new
{
   Owned = OwnedItems.Any(own => own.UserId == userId && own.ItemId == item.Id),
   // Item = item  //THIS LINE GENERATES BAD QUERY
   Item = new Item {
       Id = item.Id,
       Name = item.Name,
       ...
       [Literally every single property listed one by one] = item.CorrespondingProperty
       ...
   }
}

所以我实际上可以实现完整的项目,我只需要明确地键入每个最后的 属性。有趣!

您可能需要为查询中的 "OwnedItems" 导航 属性 启用预先加载: https://docs.microsoft.com/en-us/ef/core/querying/related-data#eager-loading

如果我要举个例子,请 post 你的完整 linq 查询。

更新 1

似乎子查询在 EF Core 中有 N+1 个问题,它可能会在版本 3 中得到修复。

参考:https://github.com/aspnet/EntityFrameworkCore/issues/10001

更新 2

如果你不需要完全实现 "Items" 你应该能够做这样的事情,你可以创建一个匿名对象而不是 "trick" EF 到你想要的东西:

from item in Items
select new
{
   Owned = OwnedItems.Any(own => own.UserId == userId && own.ItemId == item.Id),
   Item = new { Id = item.Id, Name = item.Name }
}

参考:https://github.com/aspnet/EntityFrameworkCore/issues/11186

您需要告诉 EF 加载相关数据,在本例中为 OwnedItems table。

解决此问题的一种方法是 包括 相关的 table。如果有链接 tables 的外键,可以像这样轻松完成:

var dataWithRelatedData = db_context.Items.Include(x => x.OwnedItems).Select ...

另一种避免大量往返数据库的方法是在单独的查询中加载两个数据集,然后将它们合并到内存中。因此,您首先要对 Items 进行查询,然后将数据返回到 OwnedItems 的另一个查询,最后将它们合并到一个对象列表中。这将只对数据库进行 2 次调用,从而提高性能。