c# - 如何在 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。
我怎么解决这个问题?
解决方案
要了解问题的原因,您应该了解 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);
}
推荐阅读
- javascript - 如何写一个时间条件?
- mysql - 如何使用MySql选择当前日期减去一个间隔日的列日期
- autohotkey - vps上的自动热键颜色/图像机器人?
- java - 更新单行列表视图的问题以及如何存储和加载列表视图的多项选择
- c++ - 在 pre-C++17 模式下无法使用静态 constexpr 编译
- reactjs - 如何在 React Native 中使用正确的导入格式?
- reactjs - 如何在 Reactjs 中更新 json 对象
- flutter - 定时器和索引
- python - 在 python 中使用 pcapy 模块捕获 udp 数据包。pcapy.next() 是我正在使用的属性之一
- c - 无法在 Debian 上的 C 中获得正确的文件输出