😸

Entity Framework Coreで特定の具象型に依存せずオブジェクトを保存・復元する

2023/10/31に公開

最近、業務で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クラスではTitleImageUrlが新たに定義されています。
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許容になる副作用もあるようです。
作成するアプリケーションに合わせて適切な設定をする必要がありそうですね。

参考

https://learn.microsoft.com/ja-jp/ef/core/?source=recommendations
https://learn.microsoft.com/ja-jp/ef/core/modeling/inheritance
https://www.learnentityframeworkcore.com/inheritance/table-per-hierarchy

Discussion