【问题标题】:EF Linq group by ICollection of objectsEF Linq 按对象的 ICollection 分组
【发布时间】:2016-06-18 04:30:12
【问题描述】:

全部,

我有一个 Linq 查询,它获取一个很好用的事件列表。我面临的问题是 Events 包含一个名为 headlinersArtists 的 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 只能获得一个 Event。我需要进行某种分组以查看该事件是否由相同(一组)艺术家执行。

我正在研究使用艺术家的主键,但因为它是一个集合,我无法让它工作。我已经尝试过使用 String.Join 来获取头条新闻的唯一键。但是,实体框架不支持此功能。

Linq to 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

【问题讨论】:

  • 您能否提供您的模型以及您的具体要求。如果您提供预期的输出,那就太好了

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


【解决方案1】:

具有挑战性的任务,但这里是:

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

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

如果有另一个更早开始的可用活动并且没有来自任一活动的至少一位艺术家不是另一个活动的头条新闻(即他们有相同的头条新闻集),则排除该事件。

【讨论】:

  • 不错的解决方案,但我认为他真的应该考虑在他的模型中创建从ArtistEvent 的链接。所有这些Any 可能对性能不利(当然,这可能是也可能不是问题,这取决于他的要求)。
  • @AlexanderDerck 实际上它们并没有那么糟糕 - 它们都会导致“链接”表 PK 索引搜索。
【解决方案2】:

编辑:

可以通过这种方式完成一个相当不错的部分 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&lt;Artist&gt; 可以是无序但形成相等的集合,因此创建一个 中间函数 来检查两个 ICollection&lt;Artist&gt; 是否具有相同的成员:

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) 是否不在已选择的艺术家集合中。因此,您不会每次都对照整个有序查询检查您的艺术家集,从而影响您的表现。


原文:

实际上你快到了。你还需要LINQGroupByFirst,把你的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);

你就完成了!


【讨论】:

  • 它不起作用,因为分组:'.GroupBy(a => a.ArtistId)' 似乎不允许,因为 Headliners 是艺术家对象的 ICollection,而 'a' 是一个事件对象。
  • @Frank 在这种情况下,您的活动中有哪些字段?
  • 事件中没有 ArtistId,它包含一个名为 Headliners 的多对多(自动 EF)关系,其中包含 Artist 对象。问题在于这种关系。
  • Context.Event 有一个名为 Headliners 的 ICollection 属性。并且 Context.Artist 有一个 ICollection 有一个名为 Headlining 的属性。它是由 EF 自动生成的多对多关系。一个事件可以将 0-1 个艺术家作为标题,而一个艺术家可以将 0-1 个事件作为标题。
  • 活动应按所有表演艺术家分组(例如,活动同时包含艺术家 A 和 B - 如果只有艺术家 A 而不是 B - 或 - A 和 C- 它被视为不同的活动) .如果返回更多事件,则仅应返回最近的时间 (StartDateTimeUtc)。该列表应按最大热度/人气执行的热度/人气排序。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-02-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多