Controlar las relaciones de entidad

Descargar el proyecto completado

En esta sección, se describen algunos detalles sobre cómo EF carga entidades relacionadas y cómo controlar las propiedades de navegación circular en las clases de modelo. (En esta sección, se proporciona información general y no es necesario completar el tutorial. Si lo prefiere, vaya a la parte 5).

Carga diligente frente a carga diferida

Al usar EF con una base de datos relacional, es importante comprender cómo EF carga los datos relacionados.

También es útil ver las consultas SQL que EF genera. Para realizar un seguimiento de SQL, agregue la siguiente línea de código al constructor BookServiceContext:

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

Si envía una solicitud GET a /api/books, devuelve datos JSON como los siguientes:

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

Puede ver que la propiedad Author es null, aunque el libro contenga un AuthorId válido. Esto se debe a que EF no carga las entidades Author relacionadas. El registro de seguimiento de la consulta SQL confirma lo siguiente:

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]

La instrucción SELECT toma de la tabla Books y no hace referencia a la tabla Author.

Como referencia, este es el método de la clase BooksController que devuelve la lista de libros.

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

Veamos cómo podemos devolver el autor como parte de los datos JSON. Hay tres maneras de cargar datos relacionados en Entity Framework: carga diligente, carga diferida y carga explícita. Hay desventajas con cada técnica, por lo que es importante comprender cómo funcionan.

Carga diligente

Con la carga diligente, EF carga entidades relacionadas como parte de la consulta de base de datos inicial. Para realizar una carga diligente, use el método de extensión System.Data.Entity.Include.

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

Esto indica a EF que incluya los datos de Author en la consulta. Si realiza este cambio y ejecuta la aplicación, ahora los datos JSON tienen el siguiente aspecto:

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

El registro de seguimiento muestra que EF realizó una combinación entre las tablas Book y 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]

Carga diferida

Con la carga diferida, EF carga automáticamente una entidad relacionada cuando se desreferencia la propiedad de navegación de esa entidad. Para habilitar la carga diferida, haga que la propiedad de navegación sea virtual. Por ejemplo, en la clase Book:

public class Book
{
    // (Other properties)

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

Ahora, tenga en cuenta el código siguiente:

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

Cuando se habilita la carga diferida, el acceso a la propiedad Author en books[0] hace que EF consulte la base de datos para el autor.

La carga diferida requiere varios viajes a la base de datos, ya que EF envía una consulta cada vez que recupera una entidad relacionada. Por lo general, se recomienda deshabilitar la carga diferida para los objetos que serialice. El serializador tiene que leer todas las propiedades del modelo, lo que desencadena la carga de las entidades relacionadas. Por ejemplo, estas son las consultas SQL cuando EF serializa la lista de libros con carga diferida habilitada. Puede ver que EF realiza tres consultas independientes para los tres 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

Sin embargo, es posible que haya veces que deba usar la carga diferida. La carga diligente puede hacer que EF genere una combinación muy compleja. O bien, es posible que necesite entidades relacionadas para un pequeño subconjunto de los datos y la carga diferida sería lo más eficaz.

Una manera de evitar problemas de serialización es serializar objetos de transferencia de datos (DTO) en lugar de objetos entidad. Mostraré este enfoque más adelante en el artículo.

Carga explícita

La carga explícita es similar a la carga diferida, salvo que obtiene explícitamente los datos relacionados en el código; no se produce automáticamente cuando se accede a una propiedad de navegación. La carga explícita proporciona más control sobre cuándo cargar datos relacionados, pero requiere código adicional. Para obtener más información sobre la carga explícita, consulte Carga de entidades relacionadas.

Cuando definí los modelos Book y Author, definí una propiedad de navegación en la clase Book para la relación Book-Author, pero no definí una propiedad de navegación en la otra dirección.

¿Qué ocurre si agrega la propiedad de navegación correspondiente a la clase Author?

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

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

Desafortunadamente, esto crea un problema al serializar los modelos. Si carga los datos relacionados, se crea un gráfico de objetos circulares.

Diagram that shows the Book class loading the Author class and vice versa, creating a circular object graph.

Cuando el formateador JSON o XML intenta serializar el gráfico, se producirá una excepción. Los dos formateadores inician mensajes de excepción diferentes. Este es un ejemplo del formateador 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 es el formateador 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 solución consiste en usar DTO, que describo en la sección siguiente. Como alternativa, puede configurar los formateadores JSON y XML para controlar los ciclos de grafos. Para obtener más información, consulte Controlar las referencias de objetos circulares.

En este tutorial, no necesita la propiedad de navegación Author.Book, por lo que puede dejarla fuera.