エンティティ関係の処理

完成したプロジェクトをダウンロードする

このセクションでは、EF で関連エンティティがどのように読み込まれるかと、モデル クラスで循環ナビゲーション プロパティを処理する方法の詳細について説明します。 (このセクションでは背景知識を提供し、このチュートリアルを実行するうえで必須ではありません。希望する場合は、パート 5 までスキップしてください)。

一括読み込みと遅延読み込みの比較

リレーショナル データベースで EF を使用する場合は、EF で関連データがどのようにして読み込まれるかを理解することが重要です。

また、EF によって生成される SQL クエリを確認するのも有用です。 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
  },
  ...

書籍に有効な AuthorId が含まれている場合でも、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 ステートメントは Books テーブルから取得し、Author テーブルを参照しません。

参考までに、書籍のリストを返す BooksController クラスのメソッドを次に示します。

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

JSON データの一部として Author をどのように返すことができるかを見てみましょう。 Entity Framework には、関連データを読み込む方法として、一括読み込み、遅延読み込み、明示的読み込みの 3 つがあります。 各手法にはトレードオフがあるため、それぞれのしくみを理解することが重要です。

一括読み込み

"一括読み込み" を使用すると、EF によって最初のデータベース クエリの一部として関連エンティティが読み込まれます。 一括読み込みを実行するには、System.Data.Entity.Include 拡張メソッドを使用します。

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

これにより、クエリに 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 によって Book テーブルと 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]

遅延読み込み

遅延読み込みを使用すると、関連エンティティのナビゲーション プロパティが逆参照されると、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]

遅延読み込みが有効になっている場合、books[0]Author プロパティにアクセスすると、EF によってその著者のデータベースに対してクエリが実行されます。

遅延読み込みでは、EF によって関連エンティティが取得されるたびにクエリが送信されるため、複数のデータベース トリップが必要です。 一般に、シリアル化するオブジェクトに対しては遅延読み込みを無効にする必要があります。 シリアライザーはモデルのすべてのプロパティを読み取る必要があり、それによって関連エンティティの読み込みがトリガーされます。 たとえば、EF で遅延読み込みを有効にして書籍の一覧をシリアル化する場合の SQL クエリを次に示します。 EF によって 3 人の著者に対して 3 つの別個のクエリが実行されていることがわかります。

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 で非常に複雑な結合が生成されることがあります。 また、小さなデータのサブセットの関連エンティティが必要な場合は、遅延読み込みのほうが効率的です。

シリアル化の問題を回避する 1 つの方法は、エンティティ オブジェクトではなくデータ転送オブジェクト (DTO) をシリアル化することです。 この方法については、この記事の後半で説明します。

明示的な読み込み

明示的読み込みは遅延読み込みと似ていますが、関連データをコードで明示的に取得する点が異なります。ナビゲーション プロパティにアクセスしても自動的には発生しません。 明示的読み込みを使用すると、関連データを読み込むタイミングをより細かく制御できますが、追加のコードが必要です。 明示的読み込みの詳細については、「関連エンティティの読み込み」を参照してください。

Book モデルと Author モデルを定義したときに、Book-Author リレーションシップの Book クラスにナビゲーション プロパティを定義しましたが、逆方向にはナビゲーション プロパティを定義しませんでした。

対応するナビゲーション プロパティを Author クラスに追加するとどうなるでしょうか。

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

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

残念ながら、こうするとモデルをシリアル化するときに問題が発生します。 関連データを読み込むと、循環オブジェクト グラフが作成されます。

Diagram that shows the Book class loading the Author class and vice versa, creating a circular object graph.

JSON または XML フォーマッタでグラフのシリアル化が試行されると、例外がスローされます。 2 つのフォーマッタは、異なる例外メッセージをスローします。 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>

1 つの解決策は DTO を使用することです。これについては次のセクションで説明します。 または、グラフのサイクルを処理するように JSON フォーマッタと XML フォーマッタを構成することもできます。 詳細については、「循環オブジェクト参照の処理」を参照してください。

このチュートリアルでは、Author.Book ナビゲーション プロパティは必要ないため、省いてかまいません。