【问题标题】:EF: Include with where clause, + SubIncludesEF:包含 where 子句,+ SubIncludes
【发布时间】:2019-02-06 21:43:43
【问题描述】:

这是对此处问题的跟进:Include with where clause。这个问题想找到所有驾驶公共汽车的清醒乘客

没有关于乘客的 WHERE 子句,这很简单,就像这样:

var result = Context.Busses.Where(x => x.IsDriving)
    .Include(x => x.Passengers);

如果没有关于乘客的 WHERE 子句,包含来自乘客的子关系也很简单,如下所示:

var result = Context.Busses.Where(x => x.IsDriving)
    .Include(x => x.Passengers.CarryOns)
    .Include(x => x.Passengers.Luggage);

但是这个问题需要在导航属性上使用 WHERE 子句。 前一个问题的答案非常完美,没有子关系:

var result = Context.Busses.Where(x => x.IsDriving)
    .Select(bus => new {bus, Passengers = bus.Passengers.Where(x => x.Awake)})
    .AsEnumerable().Select(x => x.bus).ToList();

但是,您如何使用此方法还包括乘客的随身行李和行李?乘客不可查询,因此此时您不能Include。我尝试过这样的事情,但第一部分刚刚被第二部分覆盖:

var bussesQuery = Context.Busses.Where(x => x.IsDriving)
    .Include(x => x.Passengers.CarryOns)
    .Include(x => x.Passengers.Luggage);
// var check = bussesQuery.ToList(); 
// The Sub-Relationship data is included here, but the Passengers are not filtered.
var result = bussesQuery
    .Select(bus => new {bus, Passengers = bus.Passengers.Where(x => x.Awake)})
    .AsEnumerable().Select(x => x.bus).ToList();
// The Sub-Relationship data is missing, but the Passengers are filtered

【问题讨论】:

    标签: c# entity-framework include where-clause


    【解决方案1】:

    通过查看您之前的查询,我看到您从 N+1 的极端到单个查询的极端。你曾经有很多查询,现在你想要一个,但请考虑一下幕后发生的事情。要获取数据实体框架需要交叉连接所有实体,因此对于每个包含的实体,您会在结果中获得额外的列,并且结果与该包含交叉连接。

    假设您有 5 辆正在行驶的巴士,每辆巴士有 30 名醒着的乘客和 15 件行李,因此您将获得巴士 x 行李 x 乘客 = 2250 条记录,每条记录都包含乘客和行李数据`。如果您使用单独的查询来查询乘客和行李,您将拥有更少的记录(5 * 30 + 5 * 15 = 225)并且每个实体将被提取一次。

    执行一个返回所有内容的大查询并不是一个好主意——它更慢、更难维护并且不值得你花时间。只查询醒着的乘客,然后查询行李。

    【讨论】:

    • 当然,如果您的查询急切地加载整个数据库并且您在内存中进行过滤。这里的梦想是完成与原始 SQL 相同的事情,但使用 LinqToEntities,其中子句在 DB 中过滤。但是您正确地说急切加载以过滤内存是不好的。我绝对不想那样。
    【解决方案2】:

    对于答案,向下滚动到答案部分。

    免责声明:我爱英孚。对于在我的系统中进行的 99.999% 的调用,我可以最快地编写代码 (LINQ),而 OR-Mapping 是最快的系统。此外,生成的查询(虽然看起来令人困惑)具有比手写 SQL 更快的执行计划。但这里不是这样。

    研究科

    顺便说一句:查看我的最终请求的原始 SQL 是这样的:

    SELECT * FROM [Busses] [bus]
    LEFT JOIN [Passengers] [passenger] ON [passenger].[BusID] = [bus].[BusID] AND [passenger].[Awake] <> 1
        LEFT JOIN [CarryOns] [carryOn] ON [carryOn].[PassengerID] = [passenger].[PassengerID]
        LEFT JOIN [Luggages] [luggage] ON [luggage].[PassengerID] = [passenger].[PassengerID]
    WHERE [bus].[IsDriving] = 1
    

    当然,如果 EF 要为这些结果生成某些内容,则需要嵌套和关键字段才能知道如何映射它们。没什么大不了的。

    不幸的是,为了通过一次访问数据库来实现这一点,我必须执行以下操作:

    var busses = context.Set<BusEntity>().Where(x => x.IsDriving);
    var passengers = context.Set<PassengerEntity>().Where(x => x.Awake);
    var carryOns = context.Set<CarryOnEntity>();
    var luggages = context.Set<LuggageEntity>();
    
    var passengerJoins = passengers.GroupJoin(
            carryOns,
            x => x.PassengerID,
            y => y.PassengerID,
            (x, y) => new { Passenger = x, CarryOns = y }
        )
        .SelectMany(
            x => x.CarryOns.DefaultIfEmpty(),
            (x, y) => new { Passenger = x.Passenger, CarryOns = x.CarryOns }
        ).GroupJoin(
            luggages,
            x => x.Passenger.PassengerID,
            y => y.PassengerID,
            (x, y) => new { Passenger = x.Passenger, CarryOns = x.CarryOns, Luggages = y }
        )
        .SelectMany(
            x => x.Luggages.DefaultIfEmpty(),
            (x, y) => new { Passenger = x.Passenger, CarryOns = x.CarryOns, Luggages = x.Luggages }
        );
    
    var bussesToPassengers = busses.GroupJoin(
            passengerJoins,
            x => x.BusID,
            y => y.Passenger.BusID,
            (x, y) => new { Bus = x, Passengers = y }
        )
        .SelectMany(
            x => x.Passengers.DefaultIfEmpty(),
            (x, y) => new { Bus = x.Bus, Passengers = x.Passengers }
        )
        .GroupBy(x => x.Bus);
    
    var rez = bussesToPassengers.ToList()
        .Select(x => x.First().Bus)
        .ToList();
    

    我不抱怨 EF 生成的 SQL,但单个 SQL 语句有几百行。我破解了它,删除了 SELECT 列,并更改了一些 ID 以匹配这个问题,它是这样的:

    SELECT *
    FROM ( SELECT *
        FROM   (SELECT *
            FROM ( SELECT DISTINCT *
                FROM  [dbo].[Bus] AS [Extent1]
                LEFT OUTER JOIN  (SELECT *
                    FROM    [dbo].[Passenger] AS [Extent2]
                    LEFT OUTER JOIN [dbo].[CarryOns] AS [Extent3] ON [Extent2].[PassengerId] = [Extent3].[PassengerId]
                    LEFT OUTER JOIN [dbo].[Luggages] AS [Extent4] ON [Extent2].[PassengerId] = [Extent4].[PassengerId]
                WHERE [Extent1].[IsDriving] = 1
            )  AS [Distinct1] ) AS [Project2]
        OUTER APPLY  (SELECT *
            FROM   (SELECT *
                FROM  [dbo].[Bus] AS [Extent6]
                LEFT OUTER JOIN  (SELECT *
                    FROM    [dbo].[Passenger] AS [Extent7]
                    LEFT OUTER JOIN [dbo].[CarryOns] AS [Extent8] ON [Extent7].[PassengerId] = [Extent8].[PassengerId]
                    LEFT OUTER JOIN [dbo].[Luggages] AS [Extent9] ON [Extent7].[PassengerId] = [Extent9].[PassengerId]
                WHERE ([Extent6].[IsDriving] = 1) AND ([Project2].[BusId] = [Extent6].[BusId]) ) AS [Project3]
            OUTER APPLY  (SELECT *
                FROM     [dbo].[Passenger] AS [Extent11]
                LEFT OUTER JOIN [dbo].[CarryOns] AS [Extent12] ON [Extent11].[PassengerId] = [Extent12].[PassengerId]
                LEFT OUTER JOIN [dbo].[Luggages] AS [Extent13] ON [Extent11].[PassengerId] = [Extent13].[PassengerId]
                LEFT OUTER JOIN [dbo].[CarryOns] AS [Extent15] ON [Extent11].[PassengerId] = [Extent15].[PassengerId]
                WHERE ([Extent11].[IsAwake] = 1) AND ([Project3].[BusId] = [Extent11].[BusId])
            UNION ALL
                SELECT *
                FROM     [dbo].[Passenger] AS [Extent16]
                LEFT OUTER JOIN [dbo].[CarryOns] AS [Extent17] ON [Extent16].[PassengerId] = [Extent17].[PassengerId]
                LEFT OUTER JOIN [dbo].[Luggages] AS [Extent18] ON [Extent16].[PassengerId] = [Extent18].[PassengerId]
                WHERE ([Extent16].[IsAwake] = 1) AND ([Project3].[BusId] = [Extent16].[BusId])
    )  AS [Project7]
    ORDER BY ........................
    

    对于我的个人测试数据,My Hand-Written SQL Query 返回 54 行,EF Generated Query 返回大约 30,000 行。因此,如果您只考虑数据的 Over-The-Wire 传输时间的增加,这是不可接受的。

    回答部分

    答案是:您可以使用 Linq to Entities(在 DB 上)和 Linq to Objects(在代码中)在一次调用中实现您的结果,但它不会高效。您可以改为选择性能更好的多个调用,包括通过网络传输的数据更少、生成的查询更具可读性以及代码更易于理解。

    最好的办法是执行多个查询。我就是这样做的:

    var bus = context.Set<BusEntity>().Where(x => x.IsDriving).ToList();
    var busIDs = bus.Select(x => x.BusID).ToList();
    var passengers = context.Set<PassengerEntity>().Where(x => x.IsAwake && busIDs.Contains(x.BusID)).ToList();
    var passengerIDs = passengers.Select(x => x.PassengerID).ToList();
    var carryOns = context.Set<CarryOnEntity>().Where(x => passengerIDs.Contains(x.PassengerID)).ToList();
    var luggages = context.Set<LuggageEntity>().Where(x => passengerIDs.Contains(x.PassengerID)).ToList();
    passengers.ForEach(x => {
        x.CarryOns = carryOns.Where(y => y.PassengerID == x.PassengerID).ToList();
        x.Luggages = luggages.Where(y => y.PassengerID == x.PassengerID).ToList();
    });
    bus.ForEach(x => x.Passengers = passengers.Where(y => y.BusID == x.BusID).ToList());
    

    这产生了 4 个调用。 SQL 总共有大约 40 行。我破解了它,删除了 SELECT 列,并更改了一些 ID 以匹配这个问题,它是这样的:

    SELECT * FROM [dbo].[Busses] AS [Extent1]
        WHERE [Extent1].[IsDriving] = 1
    
    SELECT * FROM [dbo].[Passengers] AS [Extent1]
        WHERE ([Extent1].[Awake] = 1) AND ([Extent1].[BusID] IN (......................))
    
    SELECT * FROM [dbo].[CarryOns] AS [Extent1]
        WHERE [Extent1].[PassengerID] IN (......................)
    
    SELECT * FROM [dbo].[Luggages] AS [Extent1]
        WHERE [Extent1].[PassengerID] IN (......................)
    

    EF 生成的查询在 4 次往返调用中总共返回大约 100 行。所以这意味着对数据库的 4 次调用,但都非常小、可读且非常快速。

    我没有计时,但是每当我在此答案代码上方的断点处暂停,然后按 F5 到结果的另一侧,它就是即时的。当我在研究中为 Single-Call 做同样的事情时,它花了整整一秒或更长时间,明显滞后。

    【讨论】:

    • 对于这个确切的场景,CarryOns 和 Luggages 除了与乘客的关系外没有 WHERE 条款。所以这些实际上可能只是Include()ed。但是为了演示,尤其是如果您想要对子句控制进行第三、第四或更深入的了解,我将其保留在我的回答中。
    猜你喜欢
    • 1970-01-01
    • 2012-06-08
    • 2013-05-23
    • 1970-01-01
    • 1970-01-01
    • 2021-11-23
    • 2015-10-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多