Tratamento de relações de entidade

Baixar Projeto Concluído

Esta seção descreve alguns detalhes de como o EF carrega entidades relacionadas e como lidar com propriedades de navegação circular em suas classes de modelo. (Esta seção fornece conhecimento em segundo plano e não é necessária para concluir o tutorial. Se preferir, pule para a Parte 5..)

Carregamento adiantado versus carregamento lento

Ao usar o EF com um banco de dados relacional, é importante entender como o EF carrega dados relacionados.

Também é útil ver as consultas SQL geradas pelo EF. Para rastrear o SQL, adicione a seguinte linha de código ao BookServiceContext construtor:

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

Se você enviar uma solicitação GET para /api/books, ela retornará JSON da seguinte maneira:

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

Você pode ver que a propriedade Author é nula, mesmo que o livro contenha uma AuthorId válida. Isso ocorre porque o EF não está carregando as entidades de Autor relacionadas. O log de rastreamento da consulta SQL confirma o seguinte:

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]

A instrução SELECT usa a tabela Books e não faz referência à tabela Author.

Para referência, aqui está o método na BooksController classe que retorna a lista de livros.

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

Vamos ver como podemos retornar o Autor como parte dos dados JSON. Há três maneiras de carregar dados relacionados no Entity Framework: carregamento adiantado, carregamento lento e carregamento explícito. Há compensações com cada técnica, por isso é importante entender como elas funcionam.

Carregamento adiantado

Com o carregamento adiantado, o EF carrega entidades relacionadas como parte da consulta inicial do banco de dados. Para executar o carregamento adiantado, use o método de extensão System.Data.Entity.Include .

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

Isso instrui o EF a incluir os dados do Autor na consulta. Se você fizer essa alteração e executar o aplicativo, agora os dados JSON serão semelhantes a este:

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

O log de rastreamento mostra que o EF executou uma junção nas tabelas Livro e Autor.

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]

Carregamento lento

Com o carregamento lento, o EF carrega automaticamente uma entidade relacionada quando a propriedade de navegação dessa entidade é desreferenciada. Para habilitar o carregamento lento, torne a propriedade de navegação virtual. Por exemplo, na classe Book:

public class Book
{
    // (Other properties)

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

Agora, considere o seguinte código:

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

Quando o carregamento lento está habilitado, acessar a Author propriedade em books[0] faz com que o EF consulte o banco de dados para o autor.

O carregamento lento requer várias viagens de banco de dados, pois o EF envia uma consulta sempre que recupera uma entidade relacionada. Em geral, você deseja que o carregamento lento seja desabilitado para objetos serializados. O serializador precisa ler todas as propriedades no modelo, o que dispara o carregamento das entidades relacionadas. Por exemplo, aqui estão as consultas SQL quando o EF serializa a lista de livros com carregamento lento habilitado. Você pode ver que o EF faz três consultas separadas para os três autores.

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

Ainda há ocasiões em que talvez você queira usar o carregamento lento. O carregamento adiantado pode fazer com que o EF gere uma junção muito complexa. Ou talvez você precise de entidades relacionadas para um pequeno subconjunto dos dados, e o carregamento lento seria mais eficiente.

Uma maneira de evitar problemas de serialização é serializar DTOs (objetos de transferência de dados) em vez de objetos de entidade. Mostrarei essa abordagem mais adiante no artigo.

Carregamento explícito

O carregamento explícito é semelhante ao carregamento lento, exceto que você obtém explicitamente os dados relacionados no código; isso não acontece automaticamente quando você acessa uma propriedade de navegação. O carregamento explícito oferece mais controle sobre quando carregar dados relacionados, mas requer código extra. Para obter mais informações sobre o carregamento explícito, consulte Carregando entidades relacionadas.

Quando defini os modelos Book e Author, defini uma propriedade de navegação na Book classe para a relação Book-Author, mas não defini uma propriedade de navegação na outra direção.

O que acontece se você adicionar a propriedade de navegação correspondente à Author classe ?

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

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

Infelizmente, isso cria um problema quando você serializa os modelos. Se você carregar os dados relacionados, ele criará um grafo de objeto circular.

Diagrama que mostra a classe Book carregando a classe Author e vice-versa, criando um grafo de objeto circular.

Quando o formatador JSON ou XML tentar serializar o grafo, ele gerará uma exceção. Os dois formatadores geram mensagens de exceção diferentes. Aqui está um exemplo para o formatador 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": "..."
     }
}

Aqui está o formatador 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>

Uma solução é usar DTOs, que descrevo na próxima seção. Como alternativa, você pode configurar os formatadores JSON e XML para lidar com ciclos de grafo. Para obter mais informações, consulte Manipulando referências de objeto circular.

Para este tutorial, você não precisa da Author.Book propriedade de navegação, portanto, pode deixá-la de fora.