このセクションでは、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]
遅延読み込みが有効になっている場合、Authorのbooks[0] プロパティにアクセスすると、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; }
}
残念ながら、モデルをシリアル化するときに問題が発生します。 関連データを読み込む場合は、循環オブジェクト グラフが作成されます。
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 ナビゲーション プロパティは必要ないため、そのままにしておくことができます。