EF Linq 组按对象的 ICollection 进行
本文关键字:ICollection 进行 对象 Linq EF | 更新日期: 2023-09-27 18:37:18
All,
我有一个 Linq 查询,它获取了一个效果很好的事件列表。我面临的问题是事件包含称为头条新闻的艺术家的 ICollection,在列表中,我只想要每个艺术家集 1 个事件。
下面的查询工作正常,但是:我需要前 10 个事件,但每个、一组艺术家只能使用一个事件来对人气最高的艺术家的受欢迎程度进行排序 - 不是我想要的。
Context.Events
.Where(x => x.Stage.Venue.AreaId == 1 && x.StartDateTimeUtc > DateTime.UtcNow && x.IsVerified)
.OrderByDescending(x => x.Headliners.Max(y => y.Popularity))
.Take(10)
.ToList();
如何调整上面的查询,每个艺术家只能获得一个事件。我需要进行某种分组以查看事件是否由同一(一组)艺术家执行。
我正在考虑使用艺术家的主键,但由于它是一个集合,我无法让它工作。我已经尝试了String.Join来获取头条新闻的单个唯一键。但是,这在实体框架中不受支持。
这是 Linq 到 EF 可以(优雅地)支持的东西吗?
下面的SQL查询几乎符合我的期望,它不适用于同一事件的多个艺术家
SELECT MAX(E.EventId), MAX(E.Name)
FROM [dbo].[Events] E
INNER JOIN [dbo].[Stages] S ON E.StageId = S.StageId
INNER JOIN [dbo].[Venues] V ON S.VenueId = V.VenueId
INNER JOIN [dbo].[Areas] A ON V.AreaId = A.AreaId
INNER JOIN [dbo].[Headliners] H ON E.EventId = H.EventId
INNER JOIN [dbo].[Artists] A2 ON A2.ArtistId = H.ArtistId
WHERE E.IsVerified = 1 AND E.StartDateTimeUtc>GETDATE() AND A.AreaId = 1
GROUP BY A2.ArtistId, A2.Name, A2.EchoNestHotttnesss
ORDER BY A2.EchoNestHotttnesss desc
具有挑战性的任务,但这里是:
var availableEvents = db.MusicEvents.Where(e =>
e.Stage.Venue.AreaId == 1 && e.StartDateTimeUtc > DateTime.UtcNow && e.IsVerified);
var topEvents =
(from e1 in availableEvents
where e1.Headliners.Any() &&
!availableEvents.Any(e2 => e2.StartDateTimeUtc < e1.StartDateTimeUtc &&
!e2.Headliners.Any(a2 => !e1.Headliners.Any(a1 => a1.Id == a2.Id)) &&
!e1.Headliners.Any(a1 => !e2.Headliners.Any(a2 => a2.Id == a1.Id)))
orderby e1.Headliners.Max(a => a.Popularity) descending
select e1)
.Take(10)
.ToList();
第一个子查询(availableEvents
)只是为了重用主查询中的"可用性"过滤器。它不会单独执行。
关键部分是条件
!availableEvents.Any(e2 => e2.StartDateTimeUtc < e1.StartDateTimeUtc &&
!e2.Headliners.Any(a2 => !e1.Headliners.Any(a1 => a1.Id == a2.Id)) &&
!e1.Headliners.Any(a1 => !e2.Headliners.Any(a2 => a2.Id == a1.Id)))
这个想法是排除同一组头条新闻的后续事件。应该这样读:
如果之前有另一个可用事件开始,并且每个事件中没有至少一个艺术家不是另一个事件的头条新闻(即他们设置了相同的头条新闻),则排除该事件。
编辑:
一个相当不错的部分 LINQ 延迟执行解决方案可以通过以下方式完成:
首先,根据受欢迎程度查询排序事件:
var evArtists = Context.Events
.Where(x => x.Stage.Venue.AreaId == 1 && x.StartDateTimeUtc > DateTime.UtcNow && x.IsVerified)
.OrderByDescending(x => x.Headliners.Max(y => y.Popularity));
其次,由于ICollection<Artist>
可以是无序的,但形成相等的集合,因此创建一个中间函数来检查两个ICollection<Artist>
是否具有相同的成员:
private bool areArtistsEqual(ICollection<Artist> arts1, ICollection<Artist> arts2) {
return arts1.Count == arts2.Count && //have the same amount of artists
arts1.Select(x => x.ArtistId)
.Except(arts2.Select(y => y.ArtistId))
.ToList().Count == 0; //when excepted, returns 0
}
第三,使用上述方法获取查询结果中设置的唯一艺术家,将结果放入List
中,并用所需的元素数量(例如10个元素)填充List
:
List<Events> topEvList = new List<Events>();
foreach (var ev in evArtists) {
if (topEvList.Count == 0 || !topEvList.Any(te => areArtistsEqual(te.Headliners, ev.Headliners)))
topEvList.Add(ev);
if (topEvList.Count >= 10) //you have had enough events
break;
}
您的结果在topEvList
中。
好处:
上面的解决方案是懒惰执行的,从某种意义上说,它也相当不错,因为您可以真正分解逻辑并逐个检查您的执行,而不会破坏性能。
请注意,使用上述方法,除了通过其单个元素ev
引用 evArtists
(这是您的大型查询)之外,您无需引用 。使用全 LINQ 解决方案是可能的,但您可能需要参考 evArtists.Any
才能从原始有序查询本身(而不是简单地使用其元素 ( ev
) 中查找重复的艺术家集(因为您确实知道以前选择了哪些集)。
这是可能的,因为您创建了一个临时内存topEvList
,该内存记录了之前选择的集合,并且只需要检查下一个元素(ev
)是否不在已选择的艺术家集合中。因此,您不会因为每次都对照整个有序查询检查您的艺术家集来损害您的性能。
源语言:
实际上,您几乎就在那里。您还需要的是LINQ
GroupBy
和First
,并将您的Take(10)
放在最后:
var query = Context.Events
.Where(x => x.Stage.Venue.AreaId == 1 && x.StartDateTimeUtc > DateTime.UtcNow && x.IsVerified)
.OrderByDescending(x => x.Headliners.Max(y => y.Popularity))
.GroupBy(a => a.ArtistId)
.Select(e => e.First())
.Take(10);
由于通过此查询,您已对头条新闻艺术家进行了排序:
.OrderByDescending(x => x.Headliners.Max(y => y.Popularity))
然后,您只需按ArtistId
对头条新闻进行分组:
.GroupBy(a => a.ArtistId)
因此,每个艺术家将有一个小组。接下来,您只需要组中的第一个元素(据说每个艺术家最受欢迎的事件):
.Select(e => e.First())
因此,您将获得每位艺术家所有最受欢迎的活动。最后,在每位艺术家最受欢迎的活动中,您只想参加其中的 10 个,因此:
.Take(10);
大功告成!