プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2018/01/17

Entity Framework Core におけるデータの取得

update2018/01/30 event_note2018/01/17 2:49

Entity Framework Core を使ってデータベースからデータを取得する方法についてです。

環境

  • Visual Studio 2017
  • .NET Core 2.0
  • Entity Framework Core 2.0

基本

全データの取得

using (var context = new BloggingContext())
{
    var blogs = context.Blogs.ToList();
}

単一のエンティティ(レコード)を取得

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);
}

フィルタリング

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Where(b => b.Url.Contains("dotnet"))
        .ToList();
}

関連するデータの取得

Entity Framework Core におけるリレーションシップについては以下を参照してください。

ここでは上記のときと同じ以下のモデルを例とします。

// Blog is the principal entity
public class Blog
{
    // Blog.BlogId is the principal key
    // (in this case it is a primary key rather than an alternate key)
    public int BlogId { get; set; }
    public string Url { get; set; }

    // Blog.Posts is a collection navigation property
    public List<Post> Posts { get; set; }
}

// Post is the dependent entity
public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    // Post.BlogId is the foreign key
    public int BlogId { get; set; }
    // Post.Blog is a reference navigation property
    // Post.Blog is the inverse navigation property of Blog.Posts (and vice versa)
    public Blog Blog { get; set; }
}

公式サイトによると、関連データを取得する際のO/Rマッピングのパターンは以下の3パターンがあるようです。

  • Eager loading
    関連するデータが初期クエリの一部としてデータベースからロードされる
  • Explicit loading
    関連するデータが後でデータベースから明示的にロードされる
  • Lazy loading Navigation property にアクセスしたときに関連するデータがデータベースから透過的にロードされる

直訳しただけですが、分かったような分からないような・・・。
以下、ほぼ訳しただけの内容です。

Eager loading

Include メソッドを使用して、クエリの結果に含める関連データを指定できます。
以下の例では、blogs には、関連する Posts プロパティのインスタンスを持った状態でデータが取得されます。

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .ToList();
}

もし Include がなかったら、blogsPosts プロパティは null になります。

複数の関連データを取得したい場合でも、以下のように1つのクエリで指定できます。

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .Include(blog => blog.Owner)
        .ToList();
}

複数のレベルのデータを取得

エンティティが複数の関係を持つ場合、ThenInclude メソッドを使用して複数のレベルの関連データを含めることができます。
次の例では、すべての blog 関連する post、および各 postAuthor を読み込みます。

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
        .ToList();
}

さらに ThenInclude を呼び出すことで、連鎖的に関連データを取得できます。

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
                .ThenInclude(author => author.Photo)
        .ToList();
}

これらを組み合わせて、1つのクエリでの複数のレベルおよび複数のルートから関連データを取得できます。

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
            .ThenInclude(author => author.Photo)
        .Include(blog => blog.Owner)
            .ThenInclude(owner => owner.Photo)
        .ToList();
}

含まれているエンティティの1つに複数の関連エンティティを含めることができます。

例えば、blogs に対するクエリのおいて、Posts を取得し、さらに Posts の中の AuthorTags を取得したい場合などです。
これを行うには、それぞれに対してルートからインクルードパスを指定する必要があります。

例えば、以下のように、Blog -> Posts -> Author Blog -> Posts -> Tags のようにします。

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Tags)
        .ToList();
}

無視される内容

クエリが開始されたときのエンティティのインスタンスを返さないようにクエリを変更すると、Include 演算子は無視されます。
次の例では、Include 演算子は Blog に基づいていますが、Select 演算子はクエリを変更して匿名型を返すために使用されています。
この場合、Include 演算子は何の効果もありません。

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .Select(blog => new
        {
            Id = blog.BlogId,
            Url = blog.Url
        })
        .ToList();
}

デフォルトでは、Include 演算子が無視されると、EF Core は警告をだします。
ロギング出力の表示の詳細については、Logging を参照してください。

Include 演算子が無視されたときに、例外を投げるか何もしないか、振る舞いを変更することができます。
これは、コンテキストのオプションを設定するときに行われます。
(通常は DbContext.OnConfiguring または ASP.NET Core を使用している場合は Startup.cs で行います。)

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0")
        .ConfigureWarnings(warnings => warnings.Throw(CoreEventId.IncludeIgnoredWarning));
}

Explicit loading

Navigation property は、DbContext.Entry(...) API を使用して明示的に読み込むことができます。

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    context.Entry(blog)
        .Collection(b => b.Posts)
        .Load();

    context.Entry(blog)
        .Reference(b => b.Owner)
        .Load();
}

関連するエンティティを返すクエリを個別に実行することによって、Navigation property を明示的にロードすることもできます。

Querying related entities

また、Navigation property の内容を表す LINQ クエリを取得することもできます。
これにより、関連エンティティをメモリにロードせずに Count 演算子を実行したりすることができます。

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    var postCount = context.Entry(blog)
        .Collection(b => b.Posts)
        .Query()
        .Count();
}

また、関連エンティティをフィルタリングしてメモリにロードすることもできます。

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    var goodPosts = context.Entry(blog)
        .Collection(b => b.Posts)
        .Query()
        .Where(p => p.Rating > 3)
        .ToList();
}

Lazy loading

EF Core ではまだサポートされていません。

データのリレーションシップとシリアライズ

EF Core は自動的に Navigation property を修正するので、オブジェクトグラフに循環構造を持つことになります。
例えば、blog とそれに関連する Posts を読み込むと、Posts コレクションに対する参照を持った blog オブジェクトという結果になります。
Posts コレクション内のそれぞれ要素は、blog への参照を持つことになります。

一部のシリアライゼーションフレームワークでは、このような循環は許可されません。 たとえば、Json.NET では循環構造が発見された場合、次の例外をスローします。

Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Blog' with type 'MyApplication.Models.Blog'.

ASP.NET Core を使用している場合は、オブジェクトグラフで見つかった循環を無視するようにJson.NETを設定できます。
これは Startup.csConfigureServices(...) メソッドで行われます。