EF Linq group by ICollection of objects

时间:2016-03-04 12:25:42

标签: c# entity-framework linq group-by

所有

我有一个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();

如何调整上面的查询,每个艺术家只能获得一个事件。我需要进行某种分组,以查看事件是否由同一组(艺术家)执行。

我正在考虑使用Artist的主键,但因为它是一个集合我无法让它工作。我已经尝试过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

2 个答案:

答案 0 :(得分:2)

具有挑战性的任务,但现在是:

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

我们的想法是排除同一组头条新闻的后期事件。应该这样阅读:

如果之前有另一个可用事件,并且任何一个事件中没有至少一个艺术家不是另一个事件的头条新闻(即他们设置了相同的头条新闻),则排除该事件。

答案 1 :(得分:1)

修改

一个相当不错的部分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中,并在List中填入您需要的元素数量(例如,10元素):

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

<强>优点:

上面的解决方案懒洋洋地执行并且在你真正分解逻辑和检查执行的意义上也相当不错逐件没有打破表现。

请注意,使用上述方法除了单独的元素evArtists之外,您不需要引用ev(这是您的大型查询)。使用完整的LINQ解决方案可能,但您可能需要参考evArtists.Any来查找重复的艺术家集(因为您之前已经记录了之前选择过的集合)原始有序查询本身(而不是简单地逐个使用其元素(ev)。

这是可能的,因为您创建了一个临时内存topEvList,它记录之前选择的集合,只需要检查下一个元素(ev)是否不在已选择的艺术家集合中。因此,每次都会针对整个有序查询检查一组艺术家,从而会影响您的表现。

<强>原始

你几乎就在那里。您还需要LINQ GroupByFirst,并将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);

你完成了!