Обработка связей сущностей

Скачивание завершенного проекта

В этом разделе описываются некоторые сведения о том, как EF загружает связанные сущности и как обрабатывать свойства круговой навигации в классах модели. (Этот раздел содержит общие знания и не требуется для завершения работы с руководством. Если вы предпочитаете, перейдите к части 5.)

Жадная загрузка и отложенная загрузка

При использовании EF с реляционной базой данных важно понимать, как EF загружает связанные данные.

Кроме того, полезно просмотреть запросы SQL, создаваемые EF. Чтобы выполнить трассировку SQL, добавьте в конструктор следующую строку кода BookServiceContext :

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

Если вы отправляете запрос GET в /api/books, он возвращает JSON, как показано ниже:

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

Вы можете увидеть, что свойство Author имеет значение NULL, хотя в книге указан допустимый идентификатор автора. Это связано с тем, что EF не загружает связанные сущности Author. Журнал трассировки запроса SQL подтверждает следующее:

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 берется из таблицы "Книги" и не ссылается на таблицу Author.

Для справки ниже приведен метод в BooksController классе, который возвращает список книг.

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

Давайте посмотрим, как мы можем вернуть автора в составе данных JSON. Существует три способа загрузки связанных данных в Entity Framework: жадная загрузка, отложенная загрузка и явная загрузка. Есть компромиссы с каждым методом, поэтому важно понять, как они работают.

Страстная загрузка

При активной загрузке EF загружает связанные сущности в рамках первоначального запроса базы данных. Чтобы выполнить жадную загрузку, используйте метод расширения System.Data.Entity.Include.

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

Это указывает EF включить данные автора в запрос. Если вы внесите это изменение и запустите приложение, теперь данные JSON выглядят следующим образом:

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

В журнале трассировки показано, что EF выполнил объединение таблиц "Книги" и "Авторы".

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]

Отложенная загрузка

При отложенной загрузке EF автоматически загружает связанную сущность при разыменовании свойства навигации для этой сущности. Чтобы включить отложенную загрузку, сделайте свойство навигации виртуальным. Например, в классе Book:

public class Book
{
    // (Other properties)

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

Теперь рассмотрим следующий код:

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

Если отложенная загрузка включена, доступ к Author свойству books[0] приводит к тому, что EF запрашивает базу данных об авторе.

Для отложенной загрузки требуется несколько доступов к базе данных, поскольку EF отправляет запрос каждый раз при получении связанной сущности. Обычно рекомендуется отключать отложенную загрузку для объектов, которые сериализуются. Сериализатор должен считывать все свойства модели, которые активируют загрузку связанных сущностей. Например, ниже приведены SQL-запросы, когда EF сериализирует список книг при включенной ленивой загрузке. Вы можете увидеть, что EF выполняет три отдельных запроса для трех авторов.

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

Все еще бывают случаи, когда может потребоваться использовать отложенную загрузку. Страстная загрузка может привести к созданию очень сложного соединения EF. Или вам могут потребоваться связанные сущности для небольшого подмножества данных, а отложенная загрузка будет более эффективной.

Одним из способов избежать проблем сериализации является сериализация объектов передачи данных (DTOs) вместо объектов сущностей. Я проявлю этот подход позже в статье.

Явная загрузка

Явная загрузка аналогична отложенной загрузке, но при этом связанные данные приходится явно получать в коде — это не происходит автоматически при обращении к свойству навигации. Явная загрузка дает больше контроля над тем, когда загружать связанные данные, но требует дополнительного кода. Дополнительные сведения о явной загрузке см. в разделе "Загрузка связанных сущностей".

Когда я определил модели Book и Author, я задал свойство навигации для класса Book в контексте связи Book-Author, но не задал свойство навигации в обратном направлении.

Что произойдет при добавлении соответствующего свойства навигации Author в класс?

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

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

К сожалению, при сериализации моделей возникает проблема. При загрузке связанных данных создается циклический граф объектов.

Схема, показывающая класс Book, загружающий класс Author, и наоборот, создает круговой граф объектов.

Когда модуль форматирования JSON или XML пытается сериализовать граф, он вызовет исключение. Два модуля форматирования вызывают различные сообщения об исключении. Ниже приведен пример форматирования 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": "..."
     }
}

Ниже приведен метод форматирования 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>

Одним из решений является использование DTO, которые я описываю в следующем разделе. Кроме того, можно настроить методы форматирования JSON и XML для обработки циклов графа. Дополнительные сведения см. в разделе "Обработка ссылок на циклические объекты".

В этом руководстве вам не нужно включать Author.Book свойство навигации, поэтому вы можете его пропустить.