Condividi tramite


Gestione delle relazioni tra entità

Scaricare il progetto completato

In questa sezione vengono descritti alcuni dettagli sul modo in cui Entity Framework carica le entità correlate e come gestire le proprietà di navigazione circolare nelle classi del modello. Questa sezione fornisce informazioni generali e non è necessario completare l'esercitazione. Se si preferisce, passare alla parte 5.

Eager Loading e Lazy Loading

Quando si usa Entity Framework con un database relazionale, è importante comprendere il modo in cui Entity Framework carica i dati correlati.

È anche utile visualizzare le query SQL generate da EF. Per tracciare il codice SQL, aggiungere la riga di codice seguente al BookServiceContext costruttore:

public BookServiceContext() : base("name=BookServiceContext")
{
    // New code:
    this.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
}

Se si invia una richiesta GET a /api/books, restituisce JSON simile al seguente:

[
  {
    "BookId": 1,
    "Title": "Pride and Prejudice",
    "Year": 1813,
    "Price": 9.99,
    "Genre": "Comedy of manners",
    "AuthorId": 1,
    "Author": null
  },
  ...

È possibile notare che la proprietà Author è null, anche se il libro contiene un AuthorId valido. Questo perché EF non carica le entità Author correlate. Il log di traccia della query SQL conferma quanto segue:

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId]
    FROM [dbo].[Books] AS [Extent1]

L'istruzione SELECT seleziona dalla tabella Books e non fa riferimento alla tabella Author.

Per riferimento, ecco il metodo nella BooksController classe che restituisce l'elenco dei libri.

public IQueryable<Book> GetBooks()
{
    return db.Books;
}

Vediamo come restituire l'autore come parte dei dati JSON. Esistono tre modi per caricare i dati correlati in Entity Framework: caricamento anticipato, caricamento differito e caricamento esplicito. Ci sono compromessi con ogni tecnica, quindi è importante capire come funzionano.

Caricamento ansioso

Con il caricamento anticipato, Entity Framework carica le entità correlate come parte della query iniziale al database. Per eseguire il caricamento anticipato (eager loading), usare il metodo di estensione System.Data.Entity.Include.

public IQueryable<Book> GetBooks()
{
    return db.Books
        // new code:
        .Include(b => b.Author);
}

Questo indica a EF di includere i dati dell'Autore nella query. Se si apporta questa modifica ed si esegue l'app, i dati JSON sono simili al seguente:

[
  {
    "BookId": 1,
    "Title": "Pride and Prejudice",
    "Year": 1813,
    "Price": 9.99,
    "Genre": "Comedy of manners",
    "AuthorId": 1,
    "Author": {
      "AuthorId": 1,
      "Name": "Jane Austen"
    }
  },
  ...

Il log di traccia mostra che EF ha eseguito un join nelle tabelle Book e Author.

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent2].[AuthorId] AS [AuthorId1], 
    [Extent2].[Name] AS [Name]
    FROM  [dbo].[Books] AS [Extent1]
    INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[AuthorId] = [Extent2].[AuthorId]

Caricamento pigro

Con il lazy loading, EF carica automaticamente un'entità correlata quando si accede alla proprietà di navigazione per tale entità. Per abilitare il caricamento differito, rendere virtuale la proprietà di navigazione. Ad esempio, nella classe Book:

public class Book
{
    // (Other properties)

    // Virtual navigation property
    public virtual Author Author { get; set; }
}

Si consideri ora il codice seguente:

var books = db.Books.ToList();  // Does not load authors
var author = books[0].Author;   // Loads the author for books[0]

Quando il caricamento differito è abilitato, l'accesso alla proprietà Author su books[0] fa in modo che Entity Framework esegua query sul database dell'autore.

Il caricamento differito richiede più richieste al database, perché EF invia una query ogni volta che recupera un'entità correlata. In genere, si vuole disabilitare il caricamento lazy per gli oggetti serializzati. Il serializzatore deve leggere tutte le proprietà nel modello, che attiva il caricamento delle entità correlate. Ad esempio, ecco le query SQL che EF utilizza per serializzare l'elenco dei libri con lazy loading abilitato. È possibile notare che EF esegue tre query separate per i tre autori.

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId]
    FROM [dbo].[Books] AS [Extent1]

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

Ci sono ancora momenti in cui si potrebbe voler usare il lazy loading. Il caricamento eager può causare la generazione di un join molto complesso. Oppure potrebbero essere necessarie entità correlate per un piccolo subset dei dati e il caricamento differito sarebbe più efficiente.

Un modo per evitare problemi di serializzazione consiste nel serializzare oggetti DTO (Data Transfer Objects) anziché oggetti entità. Questo approccio verrà illustrato più avanti nell'articolo.

Caricamento esplicito

Il caricamento esplicito è simile al caricamento differito, ad eccezione del fatto che si ottengono esplicitamente i dati correlati nel codice; non avviene automaticamente quando si accede a una proprietà di navigazione. Il caricamento esplicito offre maggiore controllo su quando caricare i dati correlati, ma richiede codice aggiuntivo. Per altre informazioni sul caricamento esplicito, vedere Caricamento di entità correlate.

Quando ho definito i modelli Book e Author, ho definito una proprietà di navigazione sulla classe per la Book relazione Book-Author, ma non ho definito una proprietà di navigazione nell'altra direzione.

Cosa accade se si aggiunge la proprietà di navigazione corrispondente alla Author classe ?

public class Author
{
    public int AuthorId { get; set; }
    [Required]
    public string Name { get; set; }

    public ICollection<Book> Books { get; set; }
}

Sfortunatamente, questo crea un problema quando si serializzano i modelli. Se si caricano i dati correlati, viene creato un grafico a oggetti circolari.

Diagramma che mostra la classe Book che carica la classe Author e viceversa, creando un grafico a oggetti circolari.

Quando il formattatore JSON o XML tenta di serializzare il grafico, genererà un'eccezione. I due formattatori generano messaggi di eccezione diversi. Di seguito è riportato un esempio per il formattatore JSON:

{
  "Message": "An error has occurred.",
  "ExceptionMessage": "The 'ObjectContent`1' type failed to serialize the response body for content type 
      'application/json; charset=utf-8'.",
  "ExceptionType": "System.InvalidOperationException",
  "StackTrace": null,
  "InnerException": {
    "Message": "An error has occurred.",
    "ExceptionMessage": "Self referencing loop detected with type 'BookService.Models.Book'. 
        Path '[0].Author.Books'.",
    "ExceptionType": "Newtonsoft.Json.JsonSerializationException",
    "StackTrace": "..."
     }
}

Ecco il formattatore XML:

<Error>
  <Message>An error has occurred.</Message>
  <ExceptionMessage>The 'ObjectContent`1' type failed to serialize the response body for content type 
    'application/xml; charset=utf-8'.</ExceptionMessage>
  <ExceptionType>System.InvalidOperationException</ExceptionType>
  <StackTrace />
  <InnerException>
    <Message>An error has occurred.</Message>
    <ExceptionMessage>Object graph for type 'BookService.Models.Author' contains cycles and cannot be 
      serialized if reference tracking is disabled.</ExceptionMessage>
    <ExceptionType>System.Runtime.Serialization.SerializationException</ExceptionType>
    <StackTrace> ... </StackTrace>
  </InnerException>
</Error>

Una soluzione consiste nell'usare gli oggetti DTO, descritti nella sezione successiva. In alternativa, è possibile configurare i formattatori JSON e XML per gestire i cicli del grafo. Per altre informazioni, vedere Gestione di riferimenti a oggetti circolari.

Per questa esercitazione non è necessaria la Author.Book proprietà di navigazione, quindi è possibile estrarla.