Gestion des relations d’entité

Télécharger le projet terminé

Cette section décrit en détail la façon dont EF charge les entités associées et comment gérer les propriétés de navigation circulaire dans vos classes de modèle. (Cette section fournit des connaissances en arrière-plan et n’est pas nécessaire pour suivre le didacticiel. Si vous préférez, passez à la partie 5.)

Chargement avide ou chargement paresseux

Lorsque vous utilisez EF avec une base de données relationnelle, il est important de comprendre comment EF charge les données associées.

Il est également utile de voir les requêtes SQL générées par EF. Pour suivre le sql, ajoutez la ligne de code suivante au BookServiceContext constructeur :

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

Si vous envoyez une requête GET à /api/books, elle retourne JSON comme suit :

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

Vous pouvez voir que la propriété Author a la valeur Null, même si le livre contient un AuthorId valide. Cela est dû au fait qu’EF ne charge pas les entités d’auteur associées. Le journal de trace de la requête SQL confirme ce qui suit :

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’instruction SELECT prend à partir de la table Books et ne fait pas référence à la table Author.

Pour référence, voici la méthode de la BooksController classe qui retourne la liste des livres.

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

Voyons comment retourner l’auteur dans le cadre des données JSON. Il existe trois façons de charger des données associées dans Entity Framework : chargement pressé, chargement paresseux et chargement explicite. Il existe des compromis avec chaque technique. Il est donc important de comprendre leur fonctionnement.

Chargement hâtif

Avec un chargement pressé, EF charge des entités associées dans le cadre de la requête de base de données initiale. Pour effectuer un chargement pressé, utilisez la méthode d’extension System.Data.Entity.Include .

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

Cela indique à EF d’inclure les données d’auteur dans la requête. Si vous apportez cette modification et exécutez l’application, les données JSON ressemblent à ceci :

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

Le journal de suivi indique qu’EF a effectué une jointure sur les tables Book et 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]

Chargement différé

Avec le chargement différé, EF charge automatiquement une entité associée lorsque la propriété de navigation de cette entité est déréférencée. Pour activer le chargement paresseux, rendez la propriété de navigation virtuelle. Par exemple, dans la classe Book :

public class Book
{
    // (Other properties)

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

Considérez maintenant le code suivant :

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

Lorsque le chargement paresseux est activé, l’accès à la Author propriété sur books[0] oblige EF à interroger la base de données pour l’auteur.

Le chargement différé nécessite plusieurs déplacements de base de données, car EF envoie une requête chaque fois qu’il récupère une entité associée. En règle générale, vous souhaitez désactiver le chargement paresseux pour les objets que vous sérialisez. Le sérialiseur doit lire toutes les propriétés du modèle, ce qui déclenche le chargement des entités associées. Par exemple, voici les requêtes SQL quand EF sérialise la liste des livres avec un chargement différé activé. Vous pouvez voir qu’EF effectue trois requêtes distinctes pour les trois auteurs.

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

Il existe encore des moments où vous pouvez utiliser le chargement paresseux. Le chargement pressé peut entraîner la génération d’une jointure très complexe par EF. Vous pouvez également avoir besoin d’entités associées pour un petit sous-ensemble des données, et le chargement paresseux serait plus efficace.

Une façon d’éviter les problèmes de sérialisation consiste à sérialiser les objets de transfert de données (DTO) au lieu d’objets d’entité. Je montrerai cette approche plus loin dans l’article.

Chargement explicite

Le chargement explicite est similaire au chargement paresseux, sauf que vous obtenez explicitement les données associées dans le code ; cela ne se produit pas automatiquement lorsque vous accédez à une propriété de navigation. Le chargement explicite vous permet de mieux contrôler quand charger les données associées, mais nécessite du code supplémentaire. Pour plus d’informations sur le chargement explicite, consultez Chargement d’entités associées.

Quand j’ai défini les modèles Book et Author, j’ai défini une propriété de navigation sur la Book classe pour la relation Book-Author, mais je n’ai pas défini de propriété de navigation dans l’autre sens.

Que se passe-t-il si vous ajoutez la propriété de navigation correspondante à la Author classe ?

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

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

Malheureusement, cela crée un problème lorsque vous sérialisez les modèles. Si vous chargez les données associées, cela crée un graphique d’objet circulaire.

Diagramme montrant la classe Book qui charge la classe Author et vice versa, créant un graphique d’objets circulaire.

Lorsque le formateur JSON ou XML tente de sérialiser le graphe, il lève une exception. Les deux formateurs lèvent des messages d’exception différents. Voici un exemple pour le formateur 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": "..."
     }
}

Voici le formateur 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>

Une solution consiste à utiliser des DTO, que je décrit dans la section suivante. Vous pouvez également configurer les formateurs JSON et XML pour gérer les cycles de graphe. Pour plus d’informations, consultez Gestion des références d’objets circulaires.

Pour ce tutoriel, vous n’avez pas besoin de la Author.Book propriété de navigation. Vous pouvez donc la laisser de côté.