簡體   English   中英

我怎樣才能使這個 EF Core 查詢更好?

[英]How could I make this EF Core query better?

我需要從數據庫中獲取:

  • 架子
  • 它的類型
  • 單個架子及其所有盒子及其盒子類型
  • 前一個貨架上方的單個貨架,沒有盒子,有貨架類型

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

我希望我解釋得足夠好。

查詢拆分

首先,我建議在決定是否嘗試任何優化之前,按原樣對查詢進行基准測試。

執行多個查詢比執行多個連接的大型查詢更快。 雖然您避免了單個復雜查詢,但如果您的數據庫不在同一台機器上,並且某些數據庫(例如未啟用 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));
}

參考

不會翻譯的查詢

.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

解析為Id ThenInclude()需要一個最終指定集合導航(換句話說,一個表)的表達式。

好的,根據您的問題,我假設您有一種方法需要這些信息:

  • 單個架子及其所有盒子及其盒子類型
  • 前一個貨架上方的單個貨架,沒有盒子,有貨架類型
  • 機架和它的類型

無論是 EF 分解查詢還是您分解查詢,在性能方面並沒有太大的區別。 重要的是以后對代碼的理解程度以及在需求發生變化時能夠適應的程度。

我建議的第一步是確定您實際需要的細節范圍。 您提到您不需要跟蹤,因此我希望您打算提供這些結果或以其他方式使用信息而不保留更改。 將其投影到您需要由 DTO 或 ViewModel 提供服務的各種表中的詳細信息,或者如果數據並不真正需要傳輸,則投影到匿名類型。 例如,您將擁有一個實際上是多對一的架子和架子類型,因此架子類型詳細信息可能是架子結果的一部分。 與 Box 和 BoxType 詳細信息相同。 然后,貨架將具有一組可選的適用框詳細信息。 Rack & Racktype 詳細信息可以通過貨架查詢之一返回。

[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投影作為單行投影。

在 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 之外,只有當它們在你想要的架子上時,盒子才會被連接起來,但返回兩個架子; 你想要的架子和它所有的盒子和上面的架子,但盒子數據將為空,因為左連接失敗

作為一種意見,如果您的查詢達到這種深度級別,您可能需要考慮使用視圖作為數據庫中的快捷方式或使用 No-SQL 作為讀取存儲。

必須進行大量連接,並在運行時使用 LINQ 執行諸如order by類的繁重操作,這是我盡力避免的事情。

因此,我會將其視為設計問題,而不是代碼/查詢問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM