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
がなかったら、blogs
の Posts
プロパティは null になります。
複数の関連データを取得したい場合でも、以下のように1つのクエリで指定できます。
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.Include(blog => blog.Owner)
.ToList();
}
複数のレベルのデータを取得
エンティティが複数の関係を持つ場合、ThenInclude
メソッドを使用して複数のレベルの関連データを含めることができます。
次の例では、すべての blog
関連する post
、および各 post
の Author
を読み込みます。
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
の中の Author
と Tags
を取得したい場合などです。
これを行うには、それぞれに対してルートからインクルードパスを指定する必要があります。
例えば、以下のように、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.cs
の ConfigureServices(...)
メソッドで行われます。