我怎样才能使这个 EF Core 查询更好?

时间:2021-07-28 23:40:02

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

我需要从数据库中获取:

  • 机架
  • 它的类型
  • 带有所有盒子及其盒子类型的单个货架
  • 前一个搁板上方的单个搁板,没有盒子,有搁板类型

货架上的 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; }
}

我希望我解释得足够好。

4 个答案:

答案 0 :(得分: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() 需要一个最终指定集合导航(换句话说,表)的表达式。

答案 1 :(得分:1)

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

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

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 投影作为单行。

答案 2 :(得分:1)

在 SQL raw 中它可能看起来像

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 之外,只有当它们在你想要的架子上时,盒子才会被连接起来,但返回两个架子;你想要的架子和它的所有盒子和上面的架子,但盒子数据将为空,因为左连接失败

答案 3 :(得分:0)

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

必须进行大量连接,以及在运行时使用 LINQ 执行诸如 order by 之类的繁重操作,这是我尽力避免的事情。

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

相关问题