Detectar entidades que tengan los mismos hijos.

Tengo dos entidades, Class y Student , vinculadas en una relación de muchos a muchos.

Cuando los datos se importan desde una aplicación externa, desafortunadamente algunas clases se crean por duplicado. Las clases ‘duplicadas’ tienen nombres diferentes, pero la misma materia y los mismos estudiantes.

Por ejemplo:

{Id = 341, Título = ’10rs / PE1a’, SubjectId = 60, Estudiantes = {Jack, Bill, Sarah}}

{Id = 429, Título = ’10rs / PE1b’, SubjectId = 60, Students = {Jack, Bill, Sarah}}

No hay una regla general para hacer coincidir los nombres de estas clases duplicadas, por lo que la única manera de identificar que dos clases son duplicados es que tengan el mismo SubjectId y los estudiantes .

Me gustaría usar LINQ para detectar todos los duplicados (y finalmente fusionarlos). Hasta ahora he intentado:

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

Esto no es bueno porque me sale el error:

NotSupportedException : no se puede crear un valor constante de tipo ‘TeachEDM.Student’. En este contexto, solo se admiten los tipos primitivos (‘como Int32, String y Guid’).

Evidentemente no me gusta que intente hacer coincidir o.SubjectId == c.SubjectId en LINQ.

Además, esto parece un método horrible en general y es muy lento. La llamada a la base de datos lleva más de 5 minutos.

Realmente apreciaría algún consejo.

La comparación de SubjectId no es el problema porque c.SubjectId es un valor de un tipo primitivo ( int , supongo). La excepción se queja de los Equals(c.Students) . c.Students son una constante (con respecto a los duplicates consulta) pero no un tipo primitivo.

También intentaría hacer la comparación en memoria y no en la base de datos. De todos modos, está cargando todos los datos en la memoria cuando inicia su primer bucle foreach : ejecuta la consulta allClasses . Luego, dentro del bucle, extiendes todas las allClasses a los duplicates allClasses que se ejecutan en el bucle foreach interno. Esta es una consulta de base de datos por elemento de su bucle externo! Esto podría explicar el bajo rendimiento del código.

Así que intentaría realizar el contenido del primer foreach en memoria. Para la comparación de la lista de Students es necesario comparar elemento por elemento, no las referencias a las colecciones de Estudiantes porque son, por supuesto, diferentes.

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

Ordenar las secuencias por nombre es necesario porque, creo, SequenceEqual compara la longitud de la secuencia y luego el elemento 0 con el elemento 0, luego el elemento 1 con el elemento 1 y así sucesivamente.


Editar Para su comentario que la primera consulta sigue siendo lenta.

Si tiene 1300 clases con 30 estudiantes, el rendimiento de la carga impaciente ( Include ) podría verse afectado por la multiplicación de datos que se transfieren entre la base de datos y el cliente. Esto se explica aquí: ¿Cuántos Incluir puedo usar en ObjectSet en EntityFramework para conservar el rendimiento? . La consulta es compleja porque necesita una JOIN entre clases y alumnos, y la materialización de objetos también es compleja porque EF debe filtrar los datos duplicados cuando se crean los objetos.

Un enfoque alternativo es cargar solo las clases sin los estudiantes en la primera consulta y luego cargar los estudiantes uno por uno dentro de un bucle explícitamente. Se vería así:

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

En este ejemplo, tendría 1 + 1300 consultas de base de datos en lugar de solo una, pero no tendrá la multiplicación de datos que se produce con la carga impaciente y las consultas son más simples (no hay JOIN entre clases y alumnos).

La carga explícita se explica aquí:

Si trabaja con Lazy Loading, la primera foreach con LoadProperty no sería necesaria, ya que las colecciones de Students cargarán la primera vez que acceda a ella. Debería resultar en las mismas 1300 consultas adicionales como la carga explícita.