Tratamento de relações de entidade

por Mike Wasson

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 que o EF gera. Para rastrear o SQL, adicione a seguinte linha de código ao construtor de BookServiceContext:

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 como o seguinte:

[
  {
    "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 autor é nula, embora o livro contenha uma AuthorId válida. Isso ocorre porque o EF não está carregando as entidades do autor relacionadas. O log de rastreamento da consulta SQL confirma isso:

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 de autor.

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

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

Vejamos 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, portanto, é importante entender como elas funcionam.

Carregamento adiantado

Com o carregamento adiantado, o EF carrega entidades relacionadas como parte da consulta de banco de dados inicial. 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 informa ao EF para incluir os dados do autor na consulta. Se você fizer essa alteração e executar o aplicativo, agora os dados JSON têm a seguinte aparência:

[
  {
    "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 no livro e cria tabelas.

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 para essa 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, o acesso à propriedade Author 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, porque o EF envia uma consulta cada vez 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, que dispara o carregamento das entidades relacionadas. Por exemplo, aqui estão as consultas SQL quando o EF serializa a lista de livros com o 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á momentos em que você talvez 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 os DTOs (objetos de transferência de dados) em vez de objetos de entidade. Mostrarei essa abordagem posteriormente neste artigo.

Carregamento explícito

O carregamento explícito é semelhante ao carregamento lento, exceto pelo fato de que você obtém explicitamente os dados relacionados no código; Ele 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 classe Book para a relação Book-Author, mas não defini uma propriedade de navegação na outra direção.

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

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 ao serializar os modelos. Se você carregar os dados relacionados, ele criará um gráfico de objeto circular.

Quando o formatador JSON ou XML tenta serializar o grafo, ele gerará uma exceção. Os dois formatadores lançam 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": "..."
     }
}

Este é 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 descreverei na próxima seção. Como alternativa, você pode configurar os formatadores JSON e XML para lidar com os ciclos do grafo. Para obter mais informações, consulte lidando com referências a objetos circulares.

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