Entity Framework Include OrderBy random genera datos duplicados

Cuando recupero una lista de elementos de una base de datos que incluye algunos niños (a través de .Include) y los ordeno al azar, EF me da un resultado inesperado. Creo / clono elementos adicionales.

Para explicarme mejor, he creado un pequeño y simple proyecto EF CodeFirst para reproducir el problema. Primero le daré el código para este proyecto.

El proyecto

Cree un proyecto MVC3 básico y agregue el paquete EntityFramework.SqlServerCompact a través de Nuget.
Eso agrega las últimas versiones de los siguientes paquetes:

  • EntityFramework v4.3.0
  • SqlServerCompact v4.0.8482.1
  • EntityFramework.SqlServerCompact v4.1.8482.2
  • WebActivator v1.5

Los modelos y DbContext

using System.Collections.Generic; using System.Data.Entity; namespace RandomWithInclude.Models { public class PeopleContext : DbContext { public DbSet Persons { get; set; } public DbSet
Addresses { get; set; } } public class Person { public int ID { get; set; } public string Name { get; set; } public virtual ICollection
Addresses { get; set; } } public class Address { public int ID { get; set; } public string AdressLine { get; set; } public virtual Person Person { get; set; } } }

La configuración de base de datos y la semilla: EF.SqlServerCompact.cs

 using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Infrastructure; using RandomWithInclude.Models; [assembly: WebActivator.PreApplicationStartMethod(typeof(RandomWithInclude.App_Start.EF), "Start")] namespace RandomWithInclude.App_Start { public static class EF { public static void Start() { Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"); Database.SetInitializer(new DbInitializer()); } } public class DbInitializer : DropCreateDatabaseAlways { protected override void Seed(PeopleContext context) { var address1 = new Address {AdressLine = "Street 1, City 1"}; var address2 = new Address {AdressLine = "Street 2, City 2"}; var address3 = new Address {AdressLine = "Street 3, City 3"}; var address4 = new Address {AdressLine = "Street 4, City 4"}; var address5 = new Address {AdressLine = "Street 5, City 5"}; context.Addresses.Add(address1); context.Addresses.Add(address2); context.Addresses.Add(address3); context.Addresses.Add(address4); context.Addresses.Add(address5); var person1 = new Person {Name = "Person 1", Addresses = new List
{address1, address2}}; var person2 = new Person {Name = "Person 2", Addresses = new List
{address3}}; var person3 = new Person {Name = "Person 3", Addresses = new List
{address4, address5}}; context.Persons.Add(person1); context.Persons.Add(person2); context.Persons.Add(person3); } } }

El controlador: HomeController.cs

 using System; using System.Data.Entity; using System.Linq; using System.Web.Mvc; using RandomWithInclude.Models; namespace RandomWithInclude.Controllers { public class HomeController : Controller { public ActionResult Index() { var db = new PeopleContext(); var persons = db.Persons .Include(p => p.Addresses) .OrderBy(p => Guid.NewGuid()); return View(persons.ToList()); } } } 

The View: Index.cshtml

 @using RandomWithInclude.Models @model IList 
    @foreach (var person in Model) {
  • @person.Name
  • }

esto debería ser todo, y tu aplicación debería comstackr 🙂


El problema

Como puede ver, tenemos 2 modelos sencillos (Persona y Dirección) y la Persona puede tener múltiples Direcciones.
Sembramos la base de datos generada 3 personas y 5 direcciones.
Si obtenemos a todas las personas de la base de datos, incluidas las direcciones, aleatorizamos los resultados y solo imprimimos los nombres de esas personas, ahí es donde todo sale mal.

Como resultado, a veces tengo 4 personas, a veces 5 y a veces 3, y espero 3. Siempre.
p.ej:

  • Persona 1
  • Persona 3
  • Persona 1
  • Persona 3
  • Persona 2

Entonces … ¡está copiando / clonando datos! Y eso no es genial …
Parece que EF pierde la pista de qué direcciones son hijos de cada persona …

La consulta SQL generada es esta:

 SELECT [Project1].[ID] AS [ID], [Project1].[Name] AS [Name], [Project1].[C2] AS [C1], [Project1].[ID1] AS [ID1], [Project1].[AdressLine] AS [AdressLine], [Project1].[Person_ID] AS [Person_ID] FROM ( SELECT NEWID() AS [C1], [Extent1].[ID] AS [ID], [Extent1].[Name] AS [Name], [Extent2].[ID] AS [ID1], [Extent2].[AdressLine] AS [AdressLine], [Extent2].[Person_ID] AS [Person_ID], CASE WHEN ([Extent2].[ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2] FROM [People] AS [Extent1] LEFT OUTER JOIN [Addresses] AS [Extent2] ON [Extent1].[ID] = [Extent2].[Person_ID] ) AS [Project1] ORDER BY [Project1].[C1] ASC, [Project1].[ID] ASC, [Project1].[C2] ASC 

Soluciones provisionales

  1. Si .Include(p =>p.Addresses) de la consulta, todo va bien. pero, por supuesto, las direcciones no están cargadas y el acceso a esa colección hará una nueva llamada a la base de datos cada vez.
  2. Primero puedo obtener los datos de la base de datos y aleatorizarlos más tarde simplemente agregando un .ToList () antes de .OrderBy .. de esta manera: var persons = db.Persons.Include(p => p.Addresses).ToList().OrderBy(p => Guid.NewGuid());

¿Alguien tiene alguna idea de por qué está sucediendo así?
¿Podría ser esto un error en la generación de SQL?

Como uno puede resolverlo leyendo la respuesta de AakashM y la respuesta de Nicolae Dascalu , parece que Linq OrderBy requiere una función de clasificación estable, que no es NewID/Guid.NewGuid .

Entonces, tenemos que usar otro generador aleatorio que sería estable dentro de una sola consulta.

Para lograr esto, antes de cada consulta, use un generador .Net Random para obtener un número aleatorio. A continuación, combine este número aleatorio con una propiedad única de la entidad para que se ordene aleatoriamente. Y para “aleatorizar” un poco el resultado, checksum . (la checksum es una función del Servidor SQL que calcula una idea original basada en hash; se basa en este blog ).

Suponiendo que Person Id es un int , puede escribir su consulta de esta manera:

 var rnd = (new Random()).NextDouble(); var persons = db.Persons .Include(p => p.Addresses) .OrderBy(p => SqlFunctions.Checksum(p.Id * rnd)) // Uniqueness of ordering ranking must be ensured. .ThenBy(p => p.Id); 

Como el truco de NewGuid , probablemente este no sea un buen generador aleatorio con una buena distribución, etc. Pero no causa que las entidades se dupliquen en los resultados.

Tener cuidado:
Si el orden de su consulta no garantiza la singularidad de su clasificación de entidades, debe complementarlo para garantizarlo, por lo tanto, el ThenBy que he agregado. Si su clasificación no es exclusiva de la entidad raíz consultada, sus hijos incluidos pueden mezclarse con hijos de otras entidades que tengan el mismo ranking. Y luego el error se mantendrá aquí.

Nota:
Preferiría utilizar el método .Next() para obtener un int y combinarlo a través de un xor ( ^ ) a una propiedad int única de la entidad, en lugar de usar un double y multiplicarlo. Pero SqlFunctions.Checksum lamentablemente no proporciona una sobrecarga para el tipo de datos int , aunque se supone que la función del servidor SQL lo admite. Puedes usar un yeso para superar esto, pero para mantenerlo simple finalmente elegí ir con el multiplicador.

No creo que haya un problema en la generación de consultas, pero definitivamente es un problema cuando EF intenta convertir filas en objetos.

Parece que aquí hay una suposición inherente de que los datos para la misma persona en una statement conjunta se devolverán agrupados por orden o no.

por ejemplo, el resultado de una consulta combinada siempre será

 P.Id P.Name A.Id A.StreetLine 1 Person 1 10 --- 1 Person 1 11 2 Person 2 12 3 Person 3 13 3 Person 3 14 

incluso si ordena por alguna otra columna, la misma persona siempre aparecería una detrás de la otra.

esta suposición es mayormente cierta para cualquier consulta unida.

Pero creo que hay un problema más profundo. OrderBy es para cuando desea datos en cierto orden (como opuesto al azar), por lo que esa suposición parece razonable.

Creo que deberías sacar datos y luego aleatorizarlos de acuerdo con otros medios en tu código

tl; dr: Aquí hay una abstracción que gotea. Para nosotros, Include es una instrucción simple para pegar una colección de cosas en cada fila individual devuelta. Pero la implementación de Include de EF se lleva a cabo devolviendo una fila completa para cada combo Person-Address y volviendo a armar en el cliente. Ordenar por un valor volátil hace que esas filas se mezclen, rompiendo los grupos de Person que EF está confiando.


Cuando echamos un vistazo a ToTraceString() para este LINQ:

  var people = c.People.Include("Addresses"); // Note: no OrderBy in sight! 

vemos

 SELECT [Project1].[Id] AS [Id], [Project1].[Name] AS [Name], [Project1].[C1] AS [C1], [Project1].[Id1] AS [Id1], [Project1].[Data] AS [Data], [Project1].[PersonId] AS [PersonId] FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent2].[Id] AS [Id1], [Extent2].[PersonId] AS [PersonId], [Extent2].[Data] AS [Data], CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1] FROM [Person] AS [Extent1] LEFT OUTER JOIN [Address] AS [Extent2] ON [Extent1].[Id] = [Extent2].[PersonId] ) AS [Project1] ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC 

Así que obtenemos n filas para cada A , más 1 fila para cada P sin ninguna A s.

Sin embargo, al agregar una cláusula OrderBy , se coloca el OrderBy thing-to-order al comienzo de las columnas ordenadas:

 var people = c.People.Include("Addresses").OrderBy(p => Guid.NewGuid()); 

da

 SELECT [Project1].[Id] AS [Id], [Project1].[Name] AS [Name], [Project1].[C2] AS [C1], [Project1].[Id1] AS [Id1], [Project1].[Data] AS [Data], [Project1].[PersonId] AS [PersonId] FROM ( SELECT NEWID() AS [C1], [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent2].[Id] AS [Id1], [Extent2].[PersonId] AS [PersonId], [Extent2].[Data] AS [Data], CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2] FROM [Person] AS [Extent1] LEFT OUTER JOIN [Address] AS [Extent2] ON [Extent1].[Id] = [Extent2].[PersonId] ) AS [Project1] ORDER BY [Project1].[C1] ASC, [Project1].[Id] ASC, [Project1].[C2] ASC 

Entonces en tu caso, cuando el orden por cosa no es una propiedad de un P , sino que es volátil, y por lo tanto puede ser diferente para diferentes registros de PA del mismo P , todo se desmorona.


No estoy seguro de en qué parte de la working-as-intended ~~~ cast-iron bug este comportamiento. Pero al menos ahora lo sabemos.

De la teoría: para ordenar una lista de elementos, la función de comparación debe ser estable en relación con los elementos; esto significa que para cualquier 2 elementos x, y el resultado de x

Creo que el problema está relacionado con la mala comprensión de la especificación (documentación) del método OrderBy : keySelector – Una función para extraer una clave de un elemento .

EF no mencionó explícitamente si la función proporcionada debería devolver el mismo valor para el mismo objeto tantas veces como se llama (en su caso devuelve valores diferentes / aleatorios), pero creo que el término “clave” que utilizaron en la documentación sugiere implícitamente esto .

Cuando define una ruta de consulta para definir los resultados de la consulta, (use Incluir ), la ruta de consulta solo es válida en la instancia devuelta de ObjectQuery. Otras instancias de ObjectQuery y el contexto del objeto en sí no se ven afectados. Esta funcionalidad le permite encadenar múltiples “Incluye” para una carga ansiosa.

Por lo tanto, su statement se traduce en

 from person in db.Persons.Include(p => p.Addresses).OrderBy(p => Guid.NewGuid()) select person 

en lugar de lo que pretendías

 from person in db.Persons.Include(p => p.Addresses) select person .OrderBy(p => Guid.NewGuid()) 

Por lo tanto, su segunda solución funciona bien 🙂

Referencia: carga de objetos relacionados al consultar un modelo conceptual en Entity Framework – http://msdn.microsoft.com/en-us/library/bb896272.aspx