Entity Framework Include OrderBy random genera dati duplicati

Quando recupero un elenco di elementi da un database inclusi alcuni bambini (tramite .Include), e ordino a caso, EF mi dà un risultato inaspettato .. Creo / cloni gli elementi aggiuntivi ..

Per spiegarmi meglio, ho creato un progetto EF CodeFirst piccolo e semplice per riprodurre il problema. Per prima cosa ti darò il codice per questo progetto.

Il progetto

Creare un progetto MVC3 di base e aggiungere il pacchetto EntityFramework.SqlServerCompact tramite Nuget.
Ciò aggiunge le ultime versioni dei seguenti pacchetti:

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

I modelli e 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; } } }

I dati di installazione e seed del database: 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); } } }

Il controller: 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()); } } } 

La vista: Index.cshtml

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

questo dovrebbe essere tutto, e l’applicazione dovrebbe compilare 🙂


Il problema

Come puoi vedere, abbiamo 2 modelli semplici (Persona e Indirizzo) e la Persona può avere più Indirizzi.
Noi seminare il database generato 3 persone e 5 indirizzi.
Se prendiamo tutte le persone dal database, inclusi gli indirizzi e randomizziamo i risultati e stampiamo semplicemente i nomi di quelle persone, è qui che tutto va storto.

Di conseguenza, a volte ottengo 4 persone, a volte 5 e talvolta 3, e mi aspetto 3. Sempre.
per esempio:

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

Quindi … sta copiando / clonando i dati! E non è bello ..
Sembra solo che EF perde traccia di quali indirizzi sono un figlio di quale persona ..

La query SQL generata è questa:

 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 

soluzioni alternative

  1. Se rimuovo il .Include(p =>p.Addresses) dalla query, tutto va bene. ma ovviamente gli indirizzi non vengono caricati e l’accesso a tale raccolta creerà una nuova chiamata al database ogni volta.
  2. Posso prima ottenere i dati dal database e randomizzare in seguito semplicemente aggiungendo una .ToList () prima di .OrderBy .. come questo: var persons = db.Persons.Include(p => p.Addresses).ToList().OrderBy(p => Guid.NewGuid());

Qualcuno ha qualche idea del perché sta succedendo in questo modo?
Potrebbe essere un bug nella generazione SQL?

Come si può risolvere leggendo AakashM risposta e risposta Nicolae Dascalu , sembra fortemente Linq OrderBy richiede una funzione di classifica stabile, che NewID/Guid.NewGuid non è.

Quindi dobbiamo usare un altro generatore casuale che sarebbe stabile all’interno di una singola query.

Per ottenere ciò, prima di ogni interrogazione, utilizzare un generatore Random .Net per ottenere un numero casuale. Quindi combinare questo numero casuale con una proprietà unica dell’entity framework per ottenere una classificazione casuale. E per ‘randomizzare’ un po ‘il risultato, il checksum . (la checksum è una funzione di SQL Server che calcola un hash; idea originale fondata su questo blog ).

Assuming Person Id è un int , puoi scrivere la tua query in questo modo:

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

Come il trucco NewGuid , questo molto probabilmente non è un buon generatore casuale con una buona distribuzione e così via. Ma non fa in modo che le quadro vengano duplicate nei risultati.

Attenzione:
Se il tuo ordine di query non garantisce l’unicità del ranking delle tue entity framework, devi completarlo per garantirlo, quindi il ThenBy che ho aggiunto. Se la tua classifica non è univoca per la tua entity framework root interrogata, i suoi figli inclusi possono essere mescolati con figli di altre quadro che hanno lo stesso ranking. E poi il bug rimarrà qui.

Nota:
Preferirei usare il metodo .Next() per ottenere un int quindi combinarlo attraverso un xor ( ^ ) a una proprietà int quadro int , piuttosto che usare un double e moltiplicarlo. Ma SqlFunctions.Checksum purtroppo non fornisce un sovraccarico per il tipo di dati int , anche se la funzione del server SQL dovrebbe supportarlo. Puoi usare un cast per superare questo, ma per mantenerlo semplice, alla fine ho scelto di andare con il multiplo.

Non penso che ci sia un problema nella generazione di query, ma c’è sicuramente un problema quando EF cerca di convertire le righe in object.

Sembra che in questo caso sia implicito che i dati per la stessa persona in una dichiarazione congiunta vengano restituiti raggruppati per ordine o meno.

per esempio il risultato di una query unita sarà sempre

 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 

anche se ordini da qualche altra colonna, la stessa persona apparirebbe sempre una dopo l’altra.

questa ipotesi è per lo più vera per qualsiasi query unita.

Ma c’è un problema più profondo qui penso. OrderBy è per quando vuoi dati in un certo ordine (come opposto a casuale), in modo che l’assunzione sembra ragionevole.

Penso che dovresti davvero estrarre i dati e poi randomizzarli secondo altri mezzi nel tuo codice

tl; dr: C’è un’astrazione che perde qui. Include , Include è una semplice istruzione per attaccare una raccolta di cose su ogni singola riga Person restituita. Ma l’implementazione di EF di Include viene eseguita restituendo un’intera riga per ogni combinazione Person-Address e riassemblando il client. Ordinare per un valore volatile fa sì che quelle righe vengano mescolate, facendo a pezzi i gruppi di Person cui fa affidamento EF.


Quando diamo un’occhiata a ToTraceString() per questo LINQ:

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

vediamo

 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 

Quindi otteniamo n righe per ogni A , più 1 riga per ogni P senza A s.

L’aggiunta di una clausola OrderBy , tuttavia, posiziona la cosa all’ordine all’inizio delle colonne ordinate:

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

 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 

Quindi nel tuo caso, dove l’ordinamento per elemento non è una proprietà di un P , ma è invece volatile, e quindi può essere diverso per i diversi record PA dello stesso P , l’intera cosa va in pezzi.


Non sono sicuro di dove cadrà il continuum di working-as-intended ~~~ cast-iron bug . Ma almeno ora lo sappiamo.

Dalla teoria: per ordinare una lista di elementi, la funzione di confronto dovrebbe essere stabile rispetto agli oggetti; questo significa che per ogni 2 elementi x, y il risultato di x

Penso che il problema sia legato al fraintendimento delle specifiche (documentazione) del metodo OrderBy : keySelector – Una funzione per estrarre una chiave da un elemento .

EF non ha menzionato esplicitamente se la funzione fornita dovrebbe restituire lo stesso valore per lo stesso object di quante volte viene chiamato (nel tuo caso restituisce valori diversi / casuali), ma penso che il termine “chiave” usato nella documentazione suggerisse implicitamente questo .

Quando si definisce un percorso di query per definire i risultati della query, (utilizzare Includi ), il percorso della query è valido solo sull’istanza restituita di ObjectQuery. Altre istanze di ObjectQuery e il contesto dell’object stesso non sono interessati. Questa funzionalità ti consente di concatenare più “Include” per un caricamento avido.

Pertanto, la tua dichiarazione si traduce in

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

invece di ciò che intendevi.

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

Quindi la tua seconda soluzione funziona bene 🙂

Riferimento: caricamento di oggetti correlati durante la ricerca di un modello concettuale in Entity Framework – http://msdn.microsoft.com/en-us/library/bb896272.aspx