簡體   English   中英

如何在 LINQ 中使用 group-by 進行多重加入

[英]How to do multiple join with group-by in LINQ

我想在一行中顯示一本書的類別。 我可以在 LINQPad 6 應用程序中執行此操作,但是當我通過 C# 執行此操作時出現錯誤,我不明白我的錯誤。

Table     Books                 Categories                 Books_Categories
   
      BookId | Name        CategoryId |   Name        Id  | BookId | CategoryId
      --------------       ----------------------     -------------------------
       1     | BookA         1        | CategoryA      1  |   1    |    1
       2     | BookB         2        | CategoryB      2  |   1    |    2
                                                       3  |   2    |    2

首先,我嘗試了 LINQPad 6 應用程序中的代碼。 以下代碼在那里運行良好:

from bc in Books_Categories
join b in Books on bc.BookId equals b.Id
join category in Categories on bc.CategoryId equals category.Id
group category by new {b.Id, b.Name} into g

select new {
    g.Key.Id,
    g.Key.Name,
    CategoryName = g.Select(x => x.Name)
}

運行代碼后的結果:

BookA | List<String> (CategoryA  CategoryB)     
BookB | List<String> (CategoryA)

我已將代碼集成到應用程序中,但出現我不理解的錯誤。 當我運行這個應用程序時:

public List<BookDetail> GetBookDetail()
{
    using (EBookContext context = new EBookContext())
    {
        var result = (from bc in context.Books_Categories
                      join b in context.Books on bc.BookId equals b.Id
                      join c in context.Categories on bc.CategoryId equals c.Id
                      group c by new { b.Id, b.Name } into g
                      select new BookDetail
                      {
                          BookId = g.Key.Id,
                          BookName = g.Key.Name,
                          CategoryName = string.Join(",",g.Select(x => x.Name).ToList()) // CategoryName is a string variable.
                      });

        return result.ToList();
    }
}

我收到以下錯誤:

Select(x => x.Name)' 無法翻譯。 以可翻譯的形式重寫查詢,或通過插入對 AsEnumerable()、AsAsyncEnumerable()、ToList() 或 ToListAsync() 的調用顯式切換到客戶端評估。 有關詳細信息,請參閱https://go.microsoft.com/fwlink/?linkid=2101038

我怎么解決這個問題?

要了解問題的原因,您應該了解IEnumerableIQueryable之間的區別。

IEnumerable

實現IEnumerable<...>的 object 表示一系列相似對象。 您可以獲得序列的第一項,只要您有一項,您就可以要求下一項。

IEnumerable 擁有執行此操作的所有內容。 通常 IEnumerable 應該由本地進程執行,因此它可以使用本地進程可以調用的所有方法。

枚舉序列 GetEnumerator 被調用並重復 MoveNext() / Current:

IEnumerable<Customer> customers = ...
using (IEnumerator<Customer> enumerator = customers.GetEnumerator()
{
    while (enumerator.MoveNext())
    {
         // There is still a Customer
         Customer customer = enumerator.Current;
         ProcessCustomer(customer);
    }
}

通常您不會使用這些低級方法進行枚舉。 使用諸如 foreach 之類的方法或使用不返回IEnumerable<...>的 LINQ 方法(如 ToList、TDictionary、FirstOrDefault、Count、Any 等)枚舉該序列。

可查詢的

盡管 IQueryable 看起來與 IEnumerable 非常相似,但它並不代表一個序列,它代表了獲得可枚舉序列的潛力

為此, IQueryable 包含一個Expression和一個Provider 表達式以通用格式保存必須獲取的數據。 Provider 知道誰必須提供數據(通常是數據庫管理系統),以及用於與 DBMS 通信的語言(通常是 SQL)。

當您調用將枚舉 IQueryable 的方法時,將調用 GetEnumerator() / MoveNext() / Current 的深處。 表達式被發送給提供商,提供商會將其翻譯成 SQL 並從 DBMS 獲取數據。 返回的數據表示為IEnumerator<...> ,您可以調用其中的 MoveNext() / Current。

如上所述,只要調用 GetEnumeator / MoveNext,Expression 就會發送到 Provider,Provider 會嘗試將數據轉換為 SQL。

唉,Provider不知道你的本地函數,因此無法將它們翻譯成SQL。 盡管實體框架的開發人員做得很聰明,但幾個 .NET 方法也無法轉換為 SQL。 事實上,甚至有幾種 LINQ 方法不受支持。 請參閱支持和不支持的 LINQ 方法(LINQ 到實體)

這和我的問題有什么關系?

在您的 Select 中,您使用String.Join 您的提供商不知道如何將其翻譯成 SQL。 編譯器不知道你的 Provider 有多聰明,所以編譯器不能抱怨。 您將在運行時看到問題。

我應該怎么辦?

考慮使用AsEnumerable 這會將選定的數據作為 Enumerable 序列移動到您可以使用 String.Join 的本地進程。

但是,使用 AsEnumerable 時要小心。 數據庫管理系統在搜索和組合表格方面得到了極大的優化。 將所選數據傳輸到本地進程是查詢中較慢的部分之一。 所以重寫你的查詢,這樣你就不會傳輸比實際使用更多的數據。

因此盡量避免在Where之前使用AsEnumerable ,尤其是在 Where 過濾掉大部分獲取的項目時。

在您的查詢中,獲取每個 BookDetail 的所有類別並在本地加入它們似乎不是一個大問題:數據庫String.Join不會限制已傳輸的字節數。

要獲取所有書籍,每本書都有其類別,我使用Queryable.GroupJoin的重載之一

var booksWithCategories = dbContext.Books.GroupJoin(dbContext.BooksCategories,

    book => book.Id,                      // from every Book take the Id
    bookCategory => bookCategory.BookId,  // from every BookCategory take the foreign key

    // Parameter resultSelector: from every Book, and all matching BookCategories
    // make one new:
    (book, bookCategories) => new
    {
        Id = book.Id,
        Name = book.Name,
        Categories = dbContext.Categories
            .Where(category => bookCategories
                               .Select(bookcategory => bookcategory.CategoryId)
                               .Contains(category.Id))
            .Select(category => new
            {
                // select the category properties that you want, for example:
                Id = category.Id,
                Name = category.Name,
            }
            .ToList(),
    })

換句話說:從 DbContext.Books 序列中的每一本書中,獲取 Id。 從 DbContext.BookCategories 序列中的每個 bookcategory 中,獲取 BookId。 當它們匹配時,使用這本書及其所有匹配的 BookCategories 制作一個新的 object。 從書中獲取 ID 和名稱。

要獲取圖書的類別,請從屬於這本書的所有 BookCategories 中提取 CategoryId。 結果:屬於這本書的 CategoryId 序列。

現在獲取 DbContext.Categories 中的所有類別,並僅保留那些 ID 位於此 CategoryId 序列中的類別。 使用這些類別來 select 您想要的類別屬性。

在您的具體示例中,您只需要名稱,因此您可以將屬性類別更改為:

CategorieNames = dbContext.Categories
            .Where(...)                                       // same as above
            .Select(category => caegory.Name).ToList(),

但我想加入這些字符串!

只要這些數據在內部使用,我不建議為它創建一個字符串。 這只會使重用獲取的數據變得更加困難。

因此,我建議盡可能長時間地將其保留為類別名稱列表,並且僅在您決定顯示它之前加入它:

繼續查詢

.AsEnumerable()
.Select(book => new
{
    ... // Book properties
    CategoryNames = String.Join(", ", book.Categories);
}

嘗試.AsEnumerable()

var result = (from bc in context.Books_Categories.AsEnumerable()
             join b in context.Books on bc.BookId equals b.Id
             join c in context.Categories on bc.CategoryId equals c.Id
             group c by new { b.Id, b.Name } into g
             select new BookDetail
             {
                 BookId = g.Key.Id,
                 BookName = g.Key.Name,
                 CategoryName = string.Join(",",g.Select(x => x.Name).ToList()) // CategoryName is a string variable.
             });

暫無
暫無

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

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