为何EF Core中某LINQ子查询报错,另一同类子查询正常?
问题分析与解决方案
为什么两个子查询表现不同?
核心差异在于返回结果的类型:
- 头像子查询:最终返回的是单个基础类型(
Url是字符串,Id是数字),EF Core可以直接翻译成SELECT TOP 1 [字段] FROM [表] ORDER BY CreatedOn DESC,不需要用到开窗函数,翻译逻辑简单直接,所以能正常执行。 - 消息子查询:要返回一个复杂的ViewModel对象,EF Core为了实现"分组取最新一条并投影成对象"的逻辑,会尝试生成
ROW_NUMBER() OVER(PARTITION BY P2pChatId ORDER BY CreatedOn DESC)的SQL开窗语句。如果你的EF Core版本不支持这种翻译,或者查询结构触发了无法翻译的逻辑,就会抛出异常。另外,你在消息ViewModel里的CreatedOn.ToString("s") + "+00:00"日期格式化操作,EF Core也无法直接翻译成SQL,这会进一步增加翻译难度。
解决方法
方法1:先取最新消息的主键,再关联投影(推荐)
先通过子查询拿到每个聊天的最新消息ID,再关联消息表构造ViewModel,避免直接在主查询的Select里嵌套复杂对象投影:
var latestMessageIds = _db.P2pChatMessages .GroupBy(m => m.P2pChatId) .Select(g => new { ChatId = g.Key, LatestId = g.Max(m => m.Id) }); // 假设Id自增,或用CreatedOn取最大 var list = await _db.P2pChats .OrderByDescending(x => x.P2pChatMessages.Any(y => !y.IsRead)) .Join(latestMessageIds, chat => chat.Id, msg => msg.ChatId, (chat, msg) => new { chat, msg.LatestId }) .Select(x => new P2pChatViewModel { Id = x.chat.Id, // 保留Person1和Person2的原有代码 Person1 = x.chat.Person1 == null ? null : new UserViewModel() { Id = x.chat.Person1.Id, Username = x.chat.Person1.UserName, Email = x.chat.Person1.Email, ProfilePicture = new ImageViewModel() { UserId = x.chat.Person1.Id, Url = x.chat.Person1.ProfilePictures.OrderByDescending(y => y.CreatedOn).Select(y => y.Image.Url).FirstOrDefault(), Id = x.chat.Person1.ProfilePictures.OrderByDescending(y => y.CreatedOn).Select(y => y.Image.Id).FirstOrDefault(), }, }, Person2 = x.chat.Person2 == null ? null : new UserViewModel() { Id = x.chat.Person2.Id, Username = x.chat.Person2.UserName, Email = x.chat.Person2.Email, ProfilePicture = new ImageViewModel() { UserId = x.chat.Person2.Id, Url = x.chat.Person2.ProfilePictures.OrderByDescending(y => y.CreatedOn).Select(y => y.Image.Url).FirstOrDefault(), Id = x.chat.Person2.ProfilePictures.OrderByDescending(y => y.CreatedOn).Select(y => y.Image.Id).FirstOrDefault(), }, }, LastMessage = x.chat.P2pChatMessages .Where(m => m.Id == x.LatestId) .Select(m => new P2pChatMessageViewModel { Id = m.Id, ChatId = m.P2pChatId, UserId = m.UserId, CreatedOn = DateTime.SpecifyKind(m.CreatedOn, DateTimeKind.Utc).ToString("o"), // 改用标准ISO格式 Text = m.Text }) .FirstOrDefault() }) .Take(50) .ToListAsync();
方法2:调整消息子查询的投影顺序
先取最新的消息实体,再投影成ViewModel,避免在排序后直接构造对象:
LastMessage = x.P2pChatMessages .OrderByDescending(y => y.CreatedOn) .Take(1) .Select(y => new P2pChatMessageViewModel() { Id = y.Id, ChatId = y.P2pChatId, UserId = y.UserId, CreatedOn = DateTime.SpecifyKind(y.CreatedOn, DateTimeKind.Utc).ToString("o"), Text = y.Text }) .FirstOrDefault()
注:部分EF Core版本对Take(1).Select(...).FirstOrDefault()的翻译支持更好,可避免触发开窗函数。
方法3:将日期格式化移到客户端
如果必须保留原日期格式化逻辑,可把消息子查询切换为客户端评估(注意:聊天数量大时会拉取所有消息到客户端,影响性能):
LastMessage = x.P2pChatMessages .OrderByDescending(y => y.CreatedOn) .AsEnumerable() // 切换到客户端评估 .Select(y => new P2pChatMessageViewModel() { Id = y.Id, ChatId = y.P2pChatId, UserId = y.UserId, CreatedOn = y.CreatedOn.ToString("s") + "+00:00", Text = y.Text }) .FirstOrDefault()
方法4:升级EF Core版本
如果你的EF Core版本低于3.0,开窗函数的翻译支持有限,升级到6.0+等较新版本可解决多数翻译问题。
内容的提问来源于stack exchange,提问作者Serhii Ka.
相关产品推荐
相关产品推荐

