Ho notato che quando navigo verso lo stesso oggetto entità tramite un diverso "percorso membro" ottengo un oggetto diverso. (Uso i proxy di rilevamento delle modifiche, quindi ottengo un oggetto proxy di rilevamento delle modifiche diverso).
Ecco un esempio per mostrare cosa intendo.
var joesInfo1 = context.People.Single(p => p.Name == "Joe").Info;
var joesInfo2 = context.People.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info;
Anche se joesInfo1 & joesInfo2 si riferiscono allo stesso record nel DB (la stessa entità), sono oggetti diversi. Pensavo che Entity Framework si assicurasse di usare lo stesso oggetto in questi casi.
Domanda n. 1: è davvero così com'è? O la mia osservazione è sbagliata?
Questo è un problema quando si caricano ansiosi tramite Includi. Per esempio,
IQueryable<Person> allPeople = null;
using(context)
{
allPeople = context.People
//.AsNoTracking()
.Include(p => p.Info)
.Include(p => p.Children)
.Include(p => p.Parent)
.ToList();
}
var joesInfo1 = allPeople.Single(p => p.Name == "Joe").Info; // OK, Info is already there because it was eagerly loaded
var joesInfo2 = allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info;
// ERROR: "Object context disposed...", Info is not in the Person object, even though the Person object refers to the same entity (Joe) as above.
Quindi, sembra che il caricamento entri a buon fine, devi specificare tutti i possibili "percorsi di accesso dei membri" che farai nel tuo programma. Questo non è possibile in alcuni casi come questo. Poiché il tuo oggetto Person potrebbe essere fluttuante nel tuo programma e le proprietà di navigazione "Parent" o "Children" potrebbero essere richiamate su di esso (ed è genitori / figli) un numero qualsiasi di volte.
Domanda n. 2: Esiste un modo per farlo funzionare senza specificare tutti i "percorsi di accesso dei membri" che verranno inseriti nel programma?
Grazie.
RISPOSTA:
Quindi, ecco cosa ho concluso, basato sulla risposta di bubi.
È possibile ottenere diversi "oggetti entità" se si utilizza AsNoTracking (). (In altre parole, nell'esempio sopra, a seconda del percorso che si prende per arrivare all'entità "Joe" Person, è possibile che si otterrà un oggetto diverso.) Se non si utilizza AsNoTracking tutti i Joes saranno stesso oggetto.
Ecco cosa significa:
È possibile caricare con impazienza un intero grafo gerarchico o ricorsivo dell'oggetto e utilizzarlo al di fuori di un contesto. Come? NON USARE AsNoTracking ().
Informazioni sul codice, nella seconda domanda viene eseguita la prima query (allPeople è un IQueryable)
var joesInfo1 = allPeople.Single(p => p.Name == "Joe").Info; // OK, Info is already there because it was eagerly loaded
con il contesto già disposto in modo che non venga eseguito.
Ad ogni modo, suppongo che questo sia il tuo modello
[Table("People67")]
public class Person
{
public Person()
{
Children = new List<Person>();
}
public int Id { get; set; }
[MaxLength(50)]
public string Name { get; set; }
public virtual Info Info { get; set; }
public virtual ICollection<Person> Children { get; set; }
}
public class Info
{
public int Id { get; set; }
[MaxLength(50)]
public string Description { get; set; }
}
Dopo aver seminato il database questo codice funziona (guarda l'asserzione)
using (var context = new Context(GetConnection()))
{
var joes1 = context.People.Single(p => p.Name == "Joe");
var joes2 = context.People.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe");
Assert.IsTrue(object.ReferenceEquals(joes1, joes2);
Assert.IsTrue(object.ReferenceEquals(joes1.Info.GetType(), joes2.Info.GetType()));
Assert.IsTrue(object.ReferenceEquals(joes1.Info, joes2.Info));
}
quindi sulla tua prima domanda i proxy sono dello stesso tipo e il riferimento è lo stesso.
Un po 'più profondo, se dai un'occhiata alle domande
ExecuteDbDataReader==========
SELECT TOP 2
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Person_Id] AS [Person_Id],
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE 'Joe' = [Extent1].[Name]
ExecuteDbDataReader==========
SELECT TOP 2
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Person_Id] AS [Person_Id],
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE 'Joe''s Dad' = [Extent1].[Name]
ExecuteDbDataReader==========
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Person_Id] AS [Person_Id],
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE ([Extent1].[Person_Id] IS NOT NULL) AND ([Extent1].[Person_Id] = @EntityKeyValue1)
EntityKeyValue1 = 1
ExecuteDbDataReader==========
SELECT
[Extent2].[Id] AS [Id],
[Extent2].[Description] AS [Description]
FROM ( [People67] AS [Extent1]
INNER JOIN [Infoes] AS [Extent2] ON ([Extent1].[Info_Id] = [Extent2].[Id]))
WHERE ([Extent1].[Info_Id] IS NOT NULL) AND ([Extent1].[Id] = @EntityKeyValue1)
EntityKeyValue1 = 2
puoi capire che EF unisce le entità in memoria (guarda la terza query).
Ora, per essere più precisi, questo comportamento non cambia se si aggiunge anche una proprietà Parent_Id
a Person. La terza query viene eseguita anche se EF deve sapere che Joe è già in memoria.
===================
Ora la seconda parte
Come ho detto all'inizio della risposta, il tuo codice non funziona affatto perché stai accedendo a un IQueryable con un contesto disponibile anche nella prima query.
In questo caso suppongo che questo sia il tuo codice.
List<Person> allPeople;
using (var context = new Context(GetConnection()))
{
allPeople = context.People
.Include(_ => _.Info)
.Include(_ => _.Children)
.ToList();
}
// This is an in memory query because to the previous ToList
// Take care of == because is an in memory case sensitive query!
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe").Info);
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info);
Assert.IsTrue(object.ReferenceEquals(allPeople.Single(p => p.Name == "Joe").Info, allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info));
Se attivi il profiler vedrai che EF non esegue query dopo ToList()
.
===================
Quindi, cosa non funziona? Diverse cose se si inserisce AsNoTracking()
. In questo caso il comportamento EF è diverso, le entità non sono nel contesto (non sono tracciate) e EF deve accedere al database per recuperare le entità che dovrebbe avere in memoria.
Ad esempio, questo codice non funziona.
List<Person> allPeople;
using (var context = new Context(GetConnection()))
{
allPeople = context.People
.Include(_ => _.Info)
.Include(_ => _.Children)
.AsNoTracking()
.ToList();
}
// This throw an exception
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info);
MODIFICARE
Puoi risolvere i diversi problemi che trovi utilizzando AsNoTracking in diversi modi. Non so se c'è "la soluzione".
Di solito implemento ==
(e Equals
GetHashCode
!=
, GetHashCode
e così via) occupandomi del carattere del case (i DBMS spesso non fanno distinzione tra maiuscole e minuscole, quindi ==
deve essere insensibile alle maiuscole / minuscole) per evitare problemi "==" (riferimenti diversi allo stesso db entità).
Quindi, se ho bisogno, metto in cache le entità in memoria e cerco le entità in memoria invece di navigare nelle proprietà.
Alla fine il codice non è così pulito come usare le proprietà di navigazione ma funziona (Knuth ha detto, "l'ottimizzazione è la radice di tutti i mali").
Risposta alla domanda 1: Sì in questo modo entrambe le chiamate causano un round trip nel database e per quanto ne so risultano in oggetti diversi. Solo usando 'Trova' puoi prevenire più round trip. Poiché funziona con le chiavi primarie, EF controllerà innanzitutto se un oggetto con quella chiave primaria è già caricato e lo restituisce, altrimenti interrogare il db. https://msdn.microsoft.com/en-us/library/jj573936(v=vs.113).aspx
Risposta alla domanda 2: Nel tuo esempio la chiamata al database si verifica quando si chiama "Single", ovvero dopo che il contesto è stato eliminato. Funzionerebbe se aggiungessi .ToList () alla tua query ma ciò significherebbe anche che stai caricando tutti i record.