Gestión de relaciones de entidades

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 circulares 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 carga EF los datos relacionados.

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

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 JSON como el siguiente:

[
  {
    "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 de Autor 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 Libros y no hace referencia a la tabla Author.

Como referencia, este es el método de la BooksController clase 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 inconvenientes con cada técnica, por lo que es importante comprender cómo funcionan.

Carga diligente

Con la carga ansiosa, EF carga entidades relacionadas como parte de la consulta inicial a la base de datos. 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 le indica a EF que incluya la información del Autor 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 en 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 correspondiente 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 Author propiedad en books[0] hace que EF consulte la base de datos del autor.

La carga diferida requiere varios viajes de base de datos, ya que Entity Framework envía una consulta cada vez que recupera una entidad relacionada. Por lo general, usted querrá 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, aquí están las consultas SQL cuando EF serializa la lista de libros con la carga diferida activada. 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

Todavía hay momentos en los que puedas querer usar la carga diferida. La carga ansiosa puede hacer que EF genere una unión muy compleja. O bien, podrías necesitar entidades relacionadas para un pequeño subconjunto de los datos, y la carga diferida sería más eficaz.

Una manera de evitar problemas de serialización es serializar objetos de transferencia de datos (DTO) en lugar de objetos de 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 se obtienen 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 entre el libro y el autor, pero no definí una propiedad de navegación en la dirección opuesta.

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

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, crea un gráfico de objetos circulares.

Diagrama que muestra la clase Book cargando la clase Author y viceversa, creando un gráfico de objetos circulares.

Cuando el formateador JSON o XML intenta serializar el gráfico, se producirá una excepción. Los dos formateadores lanzan diferentes mensajes de excepción. 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, vea Control de referencias de objetos circulares.

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