Zpracování vztahů mezi entitou

Stáhnout dokončený projekt

Tato část popisuje některé podrobnosti o tom, jak EF načítá související entity a jak zpracovávat vlastnosti kruhové navigace ve třídách modelu. (Tato část poskytuje základní znalosti a není nutná k dokončení kurzu. Pokud chcete, přejděte na část 5.)

Okamžité načítání versus Líné načítání

Při použití EF s relační databází je důležité pochopit, jak EF načítá související data.

Je také užitečné vidět dotazy SQL, které EF generuje. Pokud chcete trasovat SQL, přidejte do konstruktoru BookServiceContext následující řádek kódu:

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

Pokud odešlete požadavek GET do /api/books, vrátí json takto:

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

Vidíte, že vlastnost Author má hodnotu null, i když kniha obsahuje platné Id autora. Důvodem je, že EF nenačítá entity související s Autorem. Protokol trasování dotazu SQL potvrzuje následující:

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]

Příkaz SELECT přebírá z tabulky Books a neodkazuje na tabulku Autor.

Pro referenci zde je metoda ve BooksController třídě, která vrací seznam knih.

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

Pojďme se podívat, jak můžeme autora vrátit jako součást dat JSON. Existují tři způsoby, jak načíst související data v Entity Frameworku: dychtivé načítání, opožděné načítání a explicitní načítání. S každou technikou existují kompromisy, takže je důležité pochopit, jak fungují.

Nedočkavé načítání

S dychtivým načítáním EF načte související entity jako součást počátečního databázového dotazu. K provádění dychtivého načítání použijte metodu rozšíření System.Data.Entity.Include .

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

Ef to říká, aby do dotazu zahrnula data autora. Pokud provedete tuto změnu a spustíte aplikaci, data JSON teď vypadají takto:

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

Protokol trasování ukazuje, že EF provedl spojení s tabulkami Knihy a Autor.

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]

Líné načítání

Při lazy loading EF automaticky načte související entitu, když je vlastnost navigace pro danou entitu dereferencována. Pokud chcete povolit opožděné načítání, nastavte navigační vlastnost jako virtuální. Například ve třídě Kniha:

public class Book
{
    // (Other properties)

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

Teď zvažte následující kód:

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

Pokud je povolené opožděné načítání, přístup k Author vlastnosti books[0] způsobí, že EF dotazuje databázi pro autora.

Opožděné načítání vyžaduje více cest k databázi, protože EF odesílá dotaz pokaždé, když načte související entitu. Obecně platí, že pro objekty, které serializujete, chcete líné načítání zakázat. Serializátor musí číst všechny vlastnosti modelu, které aktivují načítání souvisejících entit. Tady jsou například dotazy SQL, když EF serializuje seznam knih s povoleným opožděným načítáním. Vidíte, že EF vytváří tři samostatné dotazy pro tři autory.

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

Stále existují chvíle, kdy můžete chtít používat opožděné načítání. Eager loading může způsobit, že EF vygeneruje velmi složité spojení. Nebo můžete potřebovat související entity pro malou podmnožinu dat a opožděné načítání by bylo efektivnější.

Jedním ze způsobů, jak se vyhnout problémům se serializací, je serializovat objekty přenosu dat (DTO) místo objektů entity. Tento přístup si ukážem později v článku.

Explicitní načítání

Explicitní načítání je podobné opožděnému načítání, avšak s tím rozdílem, že související data získáte kódem explicitně; k načítání nedochází automaticky, když přistupujete k navigační vlastnosti. Explicitní načítání vám dává větší kontrolu nad tím, kdy se mají načítat související data, ale vyžaduje další kód. Další informace o explicitním načítání naleznete v tématu Načítání souvisejících entit.

Když jsem definoval modely Knihy a Autor, definoval jsem navigační vlastnost třídy Book pro Book-Author vztah, ale nedefinoval jsem navigační vlastnost v opačném směru.

Co se stane, když do Author třídy přidáte odpovídající navigační vlastnost?

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

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

Tento problém se bohužel vytvoří při serializaci modelů. Pokud načtete související data, vytvoří se kruhový graf objektu.

Diagram znázorňující třídu Book, která načítá třídu Author a naopak, a vytvářející kruhový objektový graf

Když se formát JSON nebo XML pokusí serializovat graf, vyvolá výjimku. Dva formátovací moduly můžou vyvolat různé zprávy o výjimce. Tady je příklad formátovače 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": "..."
     }
}

Tady je formátovací modul 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>

Jedním z řešení je použití DTO, které popíšem v další části. Alternativně můžete nakonfigurovat formátovací moduly JSON a XML tak, aby zpracovávaly cykly grafu. Další informace naleznete v tématu Zpracování cyklických odkazů na objekty.

Pro účely tohoto kurzu nepotřebujete Author.Book navigační vlastnost, takže ji můžete vynechat.