Entity Framework Coreで特定の具象型に依存せずオブジェクトを保存・復元する
最近、業務でEntity Framework Core (EF Core)を利用する機会があったので、得た知見をまとめたいと思います。
Entity Framework Core (EF Core) とは
EF Coreはオブジェクト関係マッパー (O/RM)の一種で、.NETのオブジェクトとRDBとの対応付けを行ってくれます。
通常、オブジェクトをデータとしてRDBに保存しようとすると、例えばSQL Serverであれば、保存のためのSQLのクエリを作成してRDBを操作する必要があります。
EF Coreを利用することで、RDBに対する操作を.NETのオブジェクトの操作により実行することができます。
例えば、Blogs
テーブルに新しいBlog
レコードを追加したい場合、EF Core では以下のように記述することで実現できます(以下はほぼMicrosoftのサンプルです)。
using (var db = new SampleDbContext())
{
var blog = new Blog { Url = "http://sample.com" };
db.Blogs.Add(blog);
db.SaveChanges();
}
// DBのコンテキスト。RDBを抽象する
public class SampleDbContext : DbContext
{
// Blogsテーブル
public DbSet<Blog> Blogs { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// DBに対する接続の設定
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public int Rating { get; set; }
}
SQLのクエリを作成したりする処理は一切現れていませんが、RDBのBlogsテーブルに新しいレコードを追加できます。
また、C#ではお馴染みのLINQをDbSet<Blog>
に対して実行もできるため、Blogsテーブルから特定のレコードを取得する処理なども直感的に書けます。
特定の具象型に依存せずオブジェクトを保存・復元する
本稿のテーマである特定の具象型に依存せずオブジェクトを保存・復元するための EF Core の使い方を紹介します。
本稿ではオブジェクトの格納先としてSQLiteを想定し、以下のバージョンを前提とします。
- C#: .NET 6
- Microsoft.EntityFrameworkCore.Sqlite: 7.0.13
まず、簡単な例として、以下のような継承関係を持つオブジェクトを考えてみます。
internal abstract class DocumentBase
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Content { get; set; } = string.Empty;
}
internal class Article : DocumentBase
{
public string Title { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
}
Content
が定義されたDocumentBase
クラスに対して、Article
クラスではTitle
とImageUrl
が新たに定義されています。
Article
オブジェクトを保存するためのDbContext
を作るにはどうすればよいでしょうか?
前述した例のように、DbContext
を継承したクラスにDbSet<Article> Articles
プロパティを持たせることでも実現できますが、DbContext
の設定は以下のように書くこともできます。
public class DocumentDbContext : DbContext
{
private readonly string _databasePath;
private readonly IEnumerable<Type> _instanceTypes;
public DocumentDbContext(string databasePath, IEnumerable<Type> instanceTypes)
{
_databasePath = databasePath;
_instanceTypes = instanceTypes;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite($"Data Source={_databasePath}");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 抽象型のDocumentBaseのテーブルを作成
modelBuilder.Entity<DocumentBase>().ToTable("Documents");
// DocumentBaseから派生する具象型の型を登録
foreach (var type in _instanceTypes)
{
modelBuilder.Entity(type);
}
}
}
OnModelCreating
メソッドにおいて、テーブルはあくまでDocumentBase
クラスに対応するものとして定義し、くわえてDocumentBase
から派生する具象型の型情報を登録します。
このように設定することで、"Documents"テーブルに対して具象型のArticle
オブジェクトの保存が可能になります。
CRUD操作をしてみた例を以下に示します。
var dbPath = "DBファイルのパス";
using var context = new DocumentDbContext(dbPath, new List<Type> { typeof(Article) });
context.Database.EnsureCreated();
// 追加
context.Set<DocumentBase>().Add(new Article
{
Content = "Content",
Title = "Title",
ImageUrl = "https://~~"
});
context.SaveChanges();
// 取得
var documents = context.Set<DocumentBase>();
var document = documents.FirstOrDefault()!;
// 更新
document.Content = "XXX";
context.SaveChanges();
// 削除
context.Remove(document);
context.SaveChanges();
context.Set<DocumentBase>
のように指定することで、テーブルに格納されたレコードの一覧を指定した型にキャストして取得できます。
また、ここではDocumentBase
クラスを指定して取得していますが、内部の型は登録時の型であるArticle
として認識されています。
実際に、"Documents"テーブルにArticle
オブジェクトを保存すると、テーブルとしては以下の状態になっています。
Discriminator列が追加され、"Article"という文字列が入っています。
Table-Per-Hierarchyパターンによって継承関係のあるオブジェクトがテーブルに保存されると、このような列が自動で追加されるようです。
上記のような仕組みを用いることで、例えばドキュメントを永続化する仕組みは共通のライブラリとして提供し、実際に保存したい情報の詳細はライブラリの利用側で定義する、といったことが実現できます。
まとめ
EF Core の利用例を紹介しました。
EF Core を利用することで、継承関係があるオブジェクトについてある程度柔軟に保存・復元ができることがわかりました。
ただ、Table-Per-Hierarchyパターンで様々な型をテーブルに保存した場合、保存した型に存在しないフィールドに対応するデータベース列が自動的にNull許容になる副作用もあるようです。
作成するアプリケーションに合わせて適切な設定をする必要がありそうですね。
参考
Discussion