Udostępnij za pośrednictwem


Obsługa relacji jednostek

Pobieranie ukończonego projektu

W tej sekcji opisano, jak program EF ładuje powiązane jednostki oraz jak obsługiwać cykliczne właściwości nawigacyjne w klasach modelu. (Ta sekcja zawiera wiedzę w tle i nie jest wymagana do ukończenia samouczka. Jeśli wolisz, przejdź do części 5.).

Zaczekaj na ładowanie a ładowanie z opóźnieniem

W przypadku korzystania z programu EF z relacyjną bazą danych ważne jest, aby zrozumieć, jak program EF ładuje powiązane dane.

Warto również zobaczyć zapytania SQL generowane przez program EF. Aby prześledzić kod SQL, dodaj następujący wiersz kodu do konstruktora BookServiceContext :

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

Jeśli wyślesz żądanie GET do /api/books, zwraca on kod JSON podobny do następującego:

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

Widać, że właściwość Author ma wartość null, mimo że książka zawiera prawidłowy identyfikator AuthorId. Dzieje się tak, ponieważ program EF nie ładuje powiązanych jednostek Author. Dziennik śledzenia zapytania SQL potwierdza to:

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]

Instrukcja SELECT pobiera z tabeli Books i nie odwołuje się do tabeli Author.

Dla odniesienia, oto metoda w klasie BooksController, która zwraca listę książek.

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

Zobaczmy, jak możemy zwrócić autora jako część danych JSON. Istnieją trzy sposoby ładowania powiązanych danych w programie Entity Framework: ładowanie natychmiastowe, ładowanie opóźnione i ładowanie jawne. Istnieją kompromisy z każdą techniką, więc ważne jest, aby zrozumieć, jak działają.

Ładowanie łapczywe

W przypadku ładowania chciwego EF ładuje powiązane jednostki jako część początkowego zapytania do bazy danych. Aby wykonać ładowanie natychmiastowe, użyj metody rozszerzenia System.Data.Entity.Include.

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

Spowoduje to, że program EF uwzględni dane autora w zapytaniu. Jeśli wprowadzisz tę zmianę i uruchomisz aplikację, teraz dane JSON wyglądają następująco:

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

Dziennik śledzenia pokazuje, że EF wykonał operację łączenia między tabelami Books i Authors.

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]

Późne ładowanie

Przy leniwym ładowaniu program EF automatycznie ładuje powiązaną jednostkę, gdy właściwość nawigacji dla tej jednostki zostanie odwołana. Aby włączyć ładowanie leniwe, ustaw właściwość nawigacji jako wirtualną. Na przykład w klasie Book:

public class Book
{
    // (Other properties)

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

Teraz rozważ następujący kod:

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

Po włączeniu ładowania z opóźnieniem uzyskiwanie właściwości books[0] przy Author powoduje, że program EF wysyła zapytanie do bazy danych, aby pobrać autora.

Ładowanie z opóźnieniem wymaga wielu odwołań do bazy danych, ponieważ EF wysyła zapytanie za każdym razem, gdy pobiera powiązaną jednostkę. Ogólnie rzecz biorąc, chcesz wyłączyć ładowanie z opóźnieniem dla obiektów, które serializujesz. Serializator musi odczytać wszystkie właściwości modelu, co wyzwala ładowanie powiązanych jednostek. Na przykład poniżej przedstawiono zapytania SQL, gdy EF serializuje listę książek, a włączone jest leniwe ładowanie. Widać, że program EF tworzy trzy oddzielne zapytania dla trzech autorów.

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

Są sytuacje, w których nadal warto używać leniwego ładowania. Duże obciążenie może spowodować wygenerowanie bardzo złożonego sprzężenia przez program EF. Możesz też potrzebować powiązanych encji dla małego podzestawu danych, a leniwe ładowanie byłoby bardziej wydajne.

Jednym ze sposobów uniknięcia problemów z serializacji jest serializacja obiektów transferu danych (DTO) zamiast obiektów jednostek. Pokażę to podejście w dalszej części artykułu.

Jawne ładowanie

Jawne ładowanie jest podobne do ładowania leniwego, z tą różnicą, że jawnie pobierasz powiązane dane w kodzie; nie występuje automatycznie, gdy uzyskujesz dostęp do właściwości nawigacji. Jawne ładowanie zapewnia większą kontrolę nad tym, kiedy ładować powiązane dane, ale wymaga dodatkowego kodu. Aby uzyskać więcej informacji na temat jawnego ładowania, zobacz Ładowanie powiązanych jednostek.

Po zdefiniowaniu modeli Książki i Autora zdefiniowałem właściwość nawigacji w Book klasie dla relacji Book-Author, ale nie zdefiniowałem właściwości nawigacji w innym kierunku.

Co się stanie w przypadku dodania odpowiedniej właściwości nawigacji do Author klasy?

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

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

Niestety, powoduje to problem podczas serializacji modeli. W przypadku załadowania powiązanych danych zostanie utworzony graf obiektu cyklicznego.

Diagram przedstawiający klasę Book ładującą klasę Author i odwrotnie, tworząc graf obiektu cyklicznego.

Gdy program formatujący JSON lub XML próbuje serializować graf, zgłosi wyjątek. Oba formatery zgłaszają różne komunikaty o wyjątkach. Oto przykład dla formatowania 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": "..."
     }
}

Oto formater 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>

Jednym z rozwiązań jest użycie jednostek DTO, które opisano w następnej sekcji. Alternatywnie można skonfigurować formatery JSON i XML do obsługi cykli grafów. Aby uzyskać więcej informacji, zobacz Obsługa odwołań do obiektów okrągłych.

Na potrzeby tego samouczka nie potrzebujesz właściwości nawigacji Author.Book, więc możesz ją pominąć.