【问题标题】:How could I make this EF Core query better?我怎样才能使这个 EF Core 查询更好?
【发布时间】:2021-07-29 14:33:03
【问题描述】:

我需要从数据库中获取这个:

  • 机架
  • 是类型
  • 带有所有盒子及其盒子类型的单个货架
  • 前一个架子上方的单架子,不带箱子,带架子类型

货架上有VerticalPosition,距离地面几厘米——当我查询时架子上的第二个架子,我需要订购它们并选择索引 1 上的架子。

我现在有这个丑陋的 EF 查询:

var targetShelf = await _warehouseContext.Shelves
    .Include(s => s.Rack)
        .ThenInclude(r => r.Shelves)
            .ThenInclude(s => s.Type)
    .Include(s => s.Rack)
        .ThenInclude(r => r.Type)
    .Include(s => s.Rack)
        .ThenInclude(r => r.Shelves)
    .Include(s => s.Boxes)
        .ThenInclude(b => b.BoxType)
    .Where(s => s.Rack.Aisle.Room.Number == targetPosition.Room)
    .Where(s => s.Rack.Aisle.Letter == targetPosition.Aisle)
    .Where(s => s.Rack.Position == targetPosition.Rack)
    .OrderBy(s => s.VerticalPosition)
    .Skip(targetPosition.ShelfNumber - 1)
    .FirstOrDefaultAsync();

但这会从所有货架上获取所有盒子,并且还会显示警告

Compiling a query which loads related collections for more than one collection navigation, either via 'Include' or through projection, but no 'QuerySplittingBehavior' has been configured. By default, Entity Framework will use 'QuerySplittingBehavior.SingleQuery', which can potentially result in slow query performance.

我也想使用AsNoTracking(),因为我不需要这些数据的更改跟踪器。

第一件事:对于AsNoTracking(),我需要查询Racks,因为它抱怨循环包含。

第二件事:我尝试了这样的条件包含:

.Include(r => r.Shelves)
    .ThenInclude(s => s.Boxes.Where(b => b.ShelfId == b.Shelf.Rack.Shelves.OrderBy(sh => sh.VerticalPosition).Skip(shelfNumberFromGround - 1).First().Id))

但这甚至不会转化为 SQL。

我也想到了两个查询——一个是检索带有货架的货架,第二个是箱子,但我仍然想知道是否有一些单一的调用命令。

实体:

public class Rack
{
    public Guid Id { get; set; }
    public Guid RackTypeId { get; set; }

    public RackType Type { get; set; }
    public ICollection<Shelf> Shelves { get; set; }
}

public class RackType
{
    public Guid Id { get; set; }

    public ICollection<Rack> Racks { get; set; }
}

public class Shelf
{
    public Guid Id { get; set; }
    public Guid ShelfTypeId { get; set; }
    public Guid RackId { get; set; }
    public int VerticalPosition { get; set; }

    public ShelfType Type { get; set; }
    public Rack Rack { get; set; }
    public ICollection<Box> Boxes { get; set; }
}

public class ShelfType
{
    public Guid Id { get; set; }

    public ICollection<Shelf> Shelves { get; set; }
}

public class Box
{
    public Guid Id { get; set; }
    public Guid ShelfId { get; set; }
    public Guid BoxTypeId { get; set; }

    public BoxType BoxType { get; set; }
    public Shelf Shelf { get; set; }
}

public class BoxType
{
    public Guid Id { get; set; }

    public ICollection<Box> Boxes { get; set; }
}

我希望我解释得足够好。

【问题讨论】:

  • 不要尝试在“数据库”查询中做所有事情。使用 c# 加载所需的数据并构建所需的结果。
  • 愚蠢的问题 - 为什么不对您列出的这 4 件事进行 4 次查询?
  • 我觉得做一个更大的查询比做多个小的查询更好(而且更快?)。我也很好奇 EF 是否(如何)能够处理像这样的复杂查询。
  • 有时最好的解决办法是换桌子,给货架一个编号,这样您就不需要按顺序订购了。提高某些条件/查询的性能可能需要您创建数据库函数或过程。
  • 第二杰里米的建议;也许您需要离地厘米以进行一些与健康和安全相关的呼叫,但是有一个架子有一个离地的序号位置会使“下一个架子向上”更容易

标签: c# entity-framework entity-framework-core


【解决方案1】:

查询拆分

首先,我建议在决定是否尝试任何优化之前对查询进行基准测试。

执行多个查询比执行一个包含许多连接的大型查询要快。虽然您避免了单个复杂查询,但如果您的数据库不在同一台机器上,并且某些数据库(例如未启用 MARS 的 SQL Server)一次仅支持一个活动查询,则您需要额外的网络往返。您的里程可能会因实际表现而异。

数据库通常不保证单独查询之间的一致性(SQL Server 允许您通过可序列化或快照事务的性能昂贵选项来缓解这种情况)。如果可以进行干预数据修改,则应谨慎使用多查询策略。

要拆分特定查询,请使用AsSplitQuery() 扩展方法。

要对给定数据库上下文的所有查询使用拆分查询,

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0",
            o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}

Reference.

无法翻译的查询

.Include(r => r.Shelves)
    .ThenInclude(s => s.Boxes.Where(b => b.ShelfId == b.Shelf.Rack.Shelves.OrderBy(sh => sh.VerticalPosition).Skip(shelfNumberFromGround - 1).First().Id))

你的表情

s.Boxes.Where(b => b.ShelfId == b.Shelf.Rack.Shelves.OrderBy(sh => sh.VerticalPosition).Skip(shelfNumberFromGround - 1).First().Id

解析为IdThenInclude() 需要一个最终指定集合导航(即表格)的表达式。

【讨论】:

    【解决方案2】:

    好的,根据您的问题,我假设您有一种方法需要这些信息:

    • 带有所有盒子及其盒子类型的单个货架
    • 前一个架子上方的单架子,不带箱子,带架子类型
    • 机架和它的类型

    无论是 EF 分解查询还是您分解查询,在性能方面并没有太大的区别。重要的是代码在以后被理解的程度以及在需求发生变化时能够适应的程度。

    我建议的第一步是确定您实际需要的详细信息范围。您提到您不需要跟踪,因此我希望您打算提供这些结果或以其他方式使用信息而不进行持久更改。将其投影到您需要由 DTO 或 ViewModel 提供服务的各种表中的详细信息,或者如果数据确实不需要传输,则使用匿名类型。例如,您将有一个货架和货架类型,它实际上是多对一的,因此货架类型的详细信息可能是货架结果的一部分。与 Box 和 BoxType 详细信息相同。然后,货架将具有一组可选的适用框详细信息。机架和机架类型的详细信息可以通过货架查询之一返回。

    [Serializable]
    public class RackDTO
    {
        public int RackId { get; set; }
        public int RackTypeId { get; set; }
        public string RackTypeName { get; set; }
    }
    
    [Serializable]
    public class ShelfDTO
    {
        public int ShelfId { get; set; }
        public int VerticalPosition { get; set; }
        public int ShelfTypeId { get; set; }
        public string ShelfTypeName { get; set; } 
        public ICollection<BoxDTO> Boxes { get; set; } = new List<BoxDTO>();
        public RackDTO Rack { get; set; }
    }
    
    [Serializable]
    public class BoxDTO
    {
        public int BoxId { get; set; }
        public int BoxTypeId { get; set; }
        public string BoxTypeName { get; set; }
    }
    

    然后在阅读信息时,我可能会将其拆分为两个查询。一个用于获取“主”架子,然后是第二个可选的用于获取“前一个”架子(如果适用)。

    ShelfDTO shelf = await _warehouseContext.Shelves
        .Where(s => s.Rack.Aisle.Room.Number == targetPosition.Room
            && s.Rack.Aisle.Letter == targetPosition.Aisle
            && s.Rack.Position == targetPosition.Rack)
        .Select(s => new ShelfDTO
        {
            ShelfId = s.ShelfId,
            VerticalPosition = s.VerticalPosition,
            ShelfTypeId = s.ShelfType.ShelfTypeId,
            ShelfTypeName = s.ShelfType.Name,
            Rack = s.Rack.Select(r => new RackDTO
            {
                RackId = r.RackId,
                RackTypeId = r.RackType.RackTypeId,
                RackTypeName = r.RackType.Name
            }).Single(),
            Boxes = s.Boxes.Select(b => new BoxDTO
            {
                BoxId = b.BoxId,
                BoxTypeId = b.BoxType.BoxTypeId,
                BoxTypeName = b.BoxType.Name
            }).ToList()
         }).OrderBy(s => s.VerticalPosition)
        .Skip(targetPosition.ShelfNumber - 1)
        .FirstOrDefaultAsync();
    
    ShelfDTO previousShelf = null;
    if (targetPosition.ShelfNumber > 1 && shelf != null)
    {
        previousShelf = await _warehouseContext.Shelves
            .Where(s => s.Rack.RackId == shelf.RackId
                && s.VerticalPosition < shelf.VerticalPosition)
            .Select(s => new ShelfDTO
            {
                ShelfId = s.ShelfId,
                VerticalPosition = s.VerticalPosition,
                ShelfTypeId = s.ShelfType.ShelfTypeId,
                ShelfTypeName = s.ShelfType.Name,
                Rack = s.Rack.Select(r => new RackDTO
                {
                    RackId = r.RackId,
                    RackTypeId = r.RackType.RackTypeId,
                    RackTypeName = r.RackType.Name
            }).Single()
         }).OrderByDescending(s => s.VerticalPosition)
        .FirstOrDefaultAsync();   
    }
    

    两个相当简单易读的查询应该可以毫无问题地返回您需要的内容。因为我们向下投影到 DTO,所以如果我们想加载整个分离图,我们不需要担心急切加载和潜在的循环引用。显然,这需要充实,以包括与使用代码/视图相关的架子、盒子和机架的详细信息。这可以通过利用 Automapper 来进一步减少,它是 ProjectTo 方法来代替整个 Select 投影作为单行。

    【讨论】:

      【解决方案3】:

      在原始 SQL 中它可能看起来像

      WITH x AS(
          SELECT 
            r.*, s.Id as ShelfId, s.Type as ShelfType
            ROW_NUMBER() OVER(ORDER BY s.verticalposition) as shelfnum
          FROM 
            rooms 
            JOIN aisles on aisles.RoomId = rooms.Id
            JOIN racks r on r.AisleId = aisles.Id
            JOIN shelves s ON s.RackId = r.Id
          WHERE
            rooms.Number = @roomnum AND
            aisles.Letter = @let AND
            r.Position = @pos
      )
      
      SELECT *
      FROM 
        x
        LEFT JOIN boxes b
        ON
          b.ShelfId = x.ShelfId AND x.ShelfNum = @shelfnum
      WHERE
        x.ShelfNum BETWEEN @shelfnum AND @shelfnum+1
      

      WITH 使用房间/过道/机架连接来定位机架;你似乎有这些标识符。货架离地高度越来越高。在 WITH 之外,仅当它们在您想要的架子上时才将它们连接起来,但会返回两个架子;您想要的架子及其所有盒子和上面的架子,但盒子数据将为空,因为左连接失败

      【讨论】:

        【解决方案4】:

        作为一种意见,如果您的查询达到这种深度,您可能需要考虑使用视图作为数据库中的快捷方式,或者使用 No-SQL 作为读取存储。

        我会尽量避免在运行时使用 LINQ 进行大量连接,以及像 order by 这样的繁重操作。

        所以我会将其视为设计问题,而不是代码/查询问题。

        【讨论】:

          猜你喜欢
          • 2013-08-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-06-04
          • 1970-01-01
          • 1970-01-01
          • 2021-08-01
          • 1970-01-01
          相关资源
          最近更新 更多