首页 > 解决方案 > 如何在 LINQ 中使用 group-by 进行多重连接

问题描述

我想在一行中显示一本书的类别。我可以在 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

我怎么解决这个问题?

标签: c#linq.net-core

解决方案


要了解问题的原因,您应该了解 anIEnumerable和 an之间的区别IQueryable

IEnumerable

实现的对象IEnumerable<...>表示一系列相似的对象。您可以获得序列的第一项,只要您有一项,您就可以要求下一项。

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);
    }
}

通常您不会使用这些低级方法进行枚举。IEnumerable<...>该序列使用诸如 foreach 之类的方法进行枚举,或者使用诸如 ToList、TDictionary、FirstOrDefault、Count、Any 等不返回的 LINQ 方法进行枚举。

可查询的

尽管 IQueryable 看起来与 IEnumerable 非常相似,但它并不代表一个序列,它代表了获得可枚举序列的潜力。

为此, IQueryable 包含 anExpression和 a 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 to entity)

这和我的问题有什么关系?

在您的选择中,您使用String.Join. 您的 Provider 不知道如何将其转换为 SQL。编译器不知道你的 Provider 有多聪明,所以编译器不能抱怨。您将在运行时看到问题。

我该怎么办?

考虑使用AsEnumerable. 这会将选定的数据作为 Enumerable 序列移动到您可以使用 String.Join 的本地进程。

但是,使用 AsEnumerable 时要小心。数据库管理系统在搜索和组合表格方面得到了极大的优化。将所选数据传输到本地进程是查询中较慢的部分之一。所以重写你的查询,这样你就不会传输比实际使用更多的数据。

因此尽量避免AsEnumerable在 a 之前使用Where,特别是如果 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 来制作一个新对象。从书中获取 ID 和名称。

要获取图书的类别,请从属于这本书的所有 BookCategories 中提取 CategoryId。结果:属于这本书的 CategoryId 序列。

现在获取 DbContext.Categories 中的所有类别,并仅保留那些 ID 位于此 CategoryId 序列中的类别。使用这些类别来选择所需的类别属性。

在您的具体示例中,您只需要名称,因此您可以将属性类别更改为:

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

但我想加入这些字符串!

只要这些数据在内部使用,我不建议为它创建一个字符串。这只会使重用获取的数据变得更加困难。

因此,我建议尽可能长时间地将其保留为类别名称列表,并且仅在您决定显示它之前加入它:

继续查询

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

推荐阅读