【问题标题】:Detect entities which have the same children检测具有相同孩子的实体
【发布时间】:2011-10-15 20:30:47
【问题描述】:

我有两个实体,ClassStudent,以多对多关系链接。

当从外部应用程序导入数据时,不幸的是有些类是重复创建的。 “重复”课程名称不同,但学科相同,学生相同。

例如:

{ Id = 341, Title = '10rs/PE1a', SubjectId = 60, Students = { Jack, Bill, Sarah } }

{ Id = 429, Title = '10rs/PE1b', SubjectId = 60, Students = { Jack, Bill, Sarah } }

匹配这些重复类的名称没有通用规则,因此识别两个类重复的唯一方法是它们具有相同的SubjectIdStudents.

我想使用 LINQ 检测所有重复项(并最终合并它们)。到目前为止,我已经尝试过:

var sb = new StringBuilder();
using (var ctx = new Ctx()) {
  ctx.CommandTimeout = 10000; // Because the next line takes so long!
  var allClasses = ctx.Classes.Include("Students").OrderBy(o => o.Id);
  foreach (var c in allClasses) {
    var duplicates = allClasses.Where(o => o.SubjectId == c.SubjectId && o.Id != c.Id && o.Students.Equals(c.Students));
    foreach (var d in duplicates)
      sb.Append(d.LongName).Append(" is a duplicate of ").Append(c.LongName).Append("<br />");
  }
}
lblResult.Text = sb.ToString();

这不好,因为我得到了错误:

NotSupportedException:无法创建“TeachEDM.Student”类型的常量值。此上下文仅支持原始类型(“例如 Int32、String 和 Guid”)。

显然它不喜欢我尝试在 LINQ 中匹配 o.SubjectId == c.SubjectId

此外,这似乎是一种可怕的方法,而且速度非常慢。调用数据库需要 5 分钟以上。

非常感谢一些建议。

【问题讨论】:

标签: c# .net linq entity-framework linq-to-entities


【解决方案1】:

SubjectId 的比较不是问题,因为c.SubjectId 是一个原始类型的值(我猜是int)。异常抱怨Equals(c.Students)c.Students 是一个常量(相对于查询 duplicates),但不是原始类型。

我也会尝试在内存中而不是在数据库中进行比较。当您开始第一个foreach 循环时,无论如何您都将整个数据加载到内存中:它执行查询allClasses。然后在循环内部将 IQueryable allClasses 扩展到 IQueryable duplicates,然后在内部 foreach 循环中执行。这是外循环的每个元素的一个数据库查询!这可以解释代码性能不佳的原因。

所以我会尝试在内存中执行第一个foreach 的内容。对于Students 列表的比较,需要逐个元素进行比较,而不是对学生集合的引用,因为它们肯定是不同的。

var sb = new StringBuilder();
using (var ctx = new Ctx())
{
    ctx.CommandTimeout = 10000; // Perhaps not necessary anymore
    var allClasses = ctx.Classes.Include("Students").OrderBy(o => o.Id)
        .ToList(); // executes query, allClasses is now a List, not an IQueryable

    // everything from here runs in memory
    foreach (var c in allClasses)
    {
        var duplicates = allClasses.Where(
           o => o.SubjectId == c.SubjectId &&
           o.Id != c.Id &&
           o.Students.OrderBy(s => s.Name).Select(s => s.Name)
            .SequenceEqual(c.Students.OrderBy(s => s.Name).Select(s => s.Name)));

        // duplicates is an IEnumerable, not an IQueryable
        foreach (var d in duplicates)
            sb.Append(d.LongName)
              .Append(" is a duplicate of ")
              .Append(c.LongName)
              .Append("<br />");
    }
}
lblResult.Text = sb.ToString();

按名称对序列进行排序是必要的,因为我相信SequenceEqual 会比较序列的长度,然后比较元素 0 和元素 0,然后比较元素 1 和元素 1,以此类推。


编辑对您的第一个查询仍然很慢的评论。

如果您有 1300 个班级,每个班级有 30 名学生,那么急切加载 (Include) 的性能可能会受到在数据库和客户端之间传输的数据倍增的影响。这在这里解释:How many Include I can use on ObjectSet in EntityFramework to retain performance?。查询很复杂,因为它需要在班级和学生之间使用JOIN,而且对象实现也很复杂,因为 EF 必须在创建对象时过滤掉重复的数据。

另一种方法是在第一个查询中只加载没有学生的类,然后在循环中显式地一个一个地加载学生。它看起来像这样:

var sb = new StringBuilder();
using (var ctx = new Ctx())
{
    ctx.CommandTimeout = 10000; // Perhaps not necessary anymore
    var allClasses = ctx.Classes.OrderBy(o => o.Id).ToList(); // <- No Include!
    foreach (var c in allClasses)
    {
        // "Explicite loading": This is a new roundtrip to the DB
        ctx.LoadProperty(c, "Students");
    }

    foreach (var c in allClasses)
    {
        // ... same code as above
    }
}
lblResult.Text = sb.ToString();

在此示例中,您将有 1 + 1300 个数据库查询,而不是只有一个,但是您不会有急切加载时发生的数据乘法,并且查询更简单(班级和学生之间没有 JOIN)。

这里解释了显式加载:

如果您使用延迟加载第一个 foreachLoadProperty 将不是必需的,因为 Students 集合将在您第一次访问它时加载。它应该会产生同样的 1300 个额外查询,例如显式加载。

【讨论】:

  • 谢谢!你的答案有效。第一条语句仍然很慢,但我想只有很多数据要检索(1300 个班级 * 30 个学生)。虽然我通过本地主机上的 MySql 运行它,但我很困惑它需要这么长时间。我将在 Code Review 上发布我的新代码(以及您的更正)以进行调整,我希望没关系。再次感谢。
  • @James:我在关于性能不佳的回答中添加了一个编辑部分。如果您应该尝试替代方法(“显式加载”),请告诉我结果,我对此非常感兴趣,因为在我在这里看到的其他几个问题之后,我怀疑急切加载更频繁一个比人们想象的糟糕的选择,尤其是在像您这样子集合相对较长的情况下。
  • 太棒了!原始代码耗时 167 秒,而改进后的代码耗时 3.4 秒。我很感动。看起来多个数据库查询对性能的影响不大,这很好,因为我现在必须添加更多的负载来更新引用(我想通过更新所有外键来合并重复项以指向我们的版本)保留)。
  • @James:哇!这是一个极大的进步!我必须为您的问题添加书签以供将来参考。人们经常害怕多个数据库查询,并试图不惜一切代价避免它。但这表明人们实际上一定更害怕急切加载。
  • @James:顺便问一下:你的Students 表中的外键列是否有一个索引,该索引从Students 表指向Classes 表(可能是ClassId)?对于这 1300 个查询,它可能有额外的好处,因为这些查询基本上是通过这个 FK 列查找的。 SQL Server 不会在 FK 列 (stackoverflow.com/questions/507179/…) 上自动创建索引。不知道 MySQL 有没有。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-03
  • 1970-01-01
  • 2016-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-14
相关资源
最近更新 更多