Обработка отношений сущностей

Скачать завершенный проект

В этом разделе описываются некоторые сведения о том, как 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, несмотря на то, что книга содержит допустимый идентификатор AuthorId. Это связано с тем, что 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 о том, что в запрос нужно включить данные author. Если вы внесете это изменение и запустите приложение, теперь данные 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 создаст очень сложное соединение. Или вам могут потребоваться связанные сущности для небольшого подмножества данных, и отложенная загрузка будет более эффективной.

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

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

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

При определении моделей Книги и Автора я определил свойство навигации для 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 не требуется, поэтому его можно оставить.