检测具有相同子项的实体
我有两个实体, Class
和Student
,以多对多关系链接。
从外部应用程序导入数据时,遗憾的是一些类是一式两份创建的。 “重复”类具有不同的名称,但是相同的主题和相同的学生。
例如:
{Id = 341,Title =’10rs / PE1a’,SubjectId = 60,Students = {Jack,Bill,Sarah}}
{Id = 429,Title =’10rs / PE1b’,SubjectId = 60,Students = {Jack,Bill,Sarah}}
匹配这些重复类的名称没有一般规则,因此识别这两个类是重复的唯一方法是它们具有相同的SubjectId和Students 。
我想使用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("
"); } } lblResult.Text = sb.ToString();
这不好,因为我得到错误:
NotSupportedException :无法创建“TeachEDM.Student”类型的常量值。 在此上下文中仅支持基本类型(例如Int32,String和Guid’)。
显然它不喜欢我在LINQ中匹配o.SubjectId == c.SubjectId
。
此外,这似乎是一种可怕的方法,而且非常慢。 对数据库的调用需要5分钟以上。
我真的很感激一些建议。
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("
"); } } lblResult.Text = sb.ToString();
按名称排序序列是必要的,因为我相信, SequenceEqual
比较SequenceEqual
长度,然后将元素0与元素0进行比较,然后将元素1与元素1进行比较,依此类推。
编辑您的评论第一个查询仍然很慢。
如果你有1300个课程,每个课程有30个学生,那么渴望加载( Include
)的表现可能会受到数据库和客户端之间传输的数据相乘的影响。 这里解释了这一点: 我可以在EntityFramework中的ObjectSet上使用多少Include以保持性能? 。 查询很复杂,因为它需要在类和学生之间进行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
)。
这里解释了明显的加载:
- http://msdn.microsoft.com/en-us/library/bb896272.aspx
- 对于POCO(也适用于
EntityObject
派生实体): http : //msdn.microsoft.com/en-us/library/dd456855.aspx - 对于
EntityObject
派生实体,您还可以使用EntityCollection
的Load
方法: http : //msdn.microsoft.com/en-us/library/bb896370.aspx
如果您使用Lazy Loading,则不需要使用LoadProperty
的第一个foreach
,因为第一次访问时将加载Students
集合。 它应该导致相同的1300个额外查询,如explicite加载。