EF Core 5.0のTablePerTypeマッピング

# はじめに
EF Core 5.0では階層化されたエンティティのマッピング方法として、従来のTablePerHierarchy（階層単位のマッピング）とは別にTablePerType（テーブル単位のマッピング）が追加されました。この記事ではEF Core 5.0でTablePerTypeの階層化マッピングを作成する方法と注意点について説明します。

接続データベースにMySQL、データベースアダプターに[Pomelo.EntityFrameworkCore.MySql](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql)を利用しています。

# 継承関係にあるエンティティーの定義

次のような継承関係を持つエンティティーがあったとします。
![@startuml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/35538039-7245-52dd-bf76-c970dedb467f.png)
コードにするとこんな感じですね。

``` cs
    public abstract class Test
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    public class TestA : Test
    {
        public string PropA { get; set; }
    }
    public class TestB : Test
    {
        public string PropB { get; set; }
    }
```
EF Core 5.0ではこのようなエンティティーをデータベースに反映する場合、2つの手段をとることができます。

- 階層単位のマッピング（以前から利用可能）
- テーブル単位のマッピング（EF Core 5から利用可能）

# 階層単位のマッピング

まずは従来から利用可能な階層単位のマッピングでデータベースに表定義を反映していきましょう。
DataContextを下記のように定義します。ここで`Test`をDbContextに定義しないと、`TestA`と`TestB`は全く関係のないテーブルになってしまうので注意してください。

``` AppDbContext.cs
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> dbContextOptions) 
            : base(dbContextOptions)
        {
        }

        public DbSet<Test> Test { get; set; }
        public DbSet<TestA> TestA { get; set; }
        public DbSet<TestB> TestB { get; set; }
    }
```
データベースマイグレーションを実施すると下記のようなSQLが発行されます。

``` sql
      CREATE TABLE `Test` (
          `Id` int NOT NULL AUTO_INCREMENT,
          `Name` longtext CHARACTER SET utf8mb4 NULL,
          `Discriminator` longtext CHARACTER SET utf8mb4 NOT NULL,
          `PropA` longtext CHARACTER SET utf8mb4 NULL,
          `PropB` longtext CHARACTER SET utf8mb4 NULL,
          CONSTRAINT `PK_Test` PRIMARY KEY (`Id`)
      ) CHARACTER SET utf8mb4;
```
次のコードでデータベースにサンプルデータを追加していきます。データの登録時は親クラスである`Test`に対して登録しても、子クラスである`TestA`や`TestB`に登録しても結果は変わりません。

``` cs
    await _dbContext.TestA.AddAsync(new TestA { Name = "TestA_1", PropA = "12345"});
    await _dbContext.TestB.AddAsync(new TestB { Name = "TestB_1", PropB = "67890"});

    await _dbContext.Test.AddAsync(new TestA { Name = "TestA_2", PropA = "ABC"});
    await _dbContext.Test.AddAsync(new TestB { Name = "TestB_2", PropB = "DEF"});

    await _dbContext.SaveChangesAsync();
```
`Test`テーブルには次のようにデータが格納されています。`TestA`の`PropA`と`TestB`の`PropB`が`Test`という1つのテーブルに定義され、関係ないプロパティーには`null`が格納されているのが分かります。また、エンティティーを識別する`Discriminator`というカラムが自動的に追加され、そのレコードが`TestA`のものなのか、`TestB`のものなのかが判別できるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/b215cad8-917a-325c-a4bf-286828bf6772.png)
TestAから1行読み出してみると、

``` cs
Console.WriteLine(await _dbContext.TestA.FirstAsync(r => r.PropA == "12345"));
```
こんなSQLが発行されました。このSQLだったら`Discriminator`にIndexを張りたい気がしますね（まぁPropAで検索している時点でダメダメなんですけれど）。

```
      SELECT `t`.`Id`, `t`.`Discriminator`, `t`.`Name`, `t`.`PropA`
      FROM `Test` AS `t`
      WHERE (`t`.`Discriminator` = 'TestA') AND (`t`.`PropA` = '12345')
      LIMIT 1
```
# テーブル単位のマッピング
続いてEF Core 5.0から利用になったテーブルの単位マッピングです。OnModelCreating時にToTableメソッドを使って配置するテーブル名を指定しています。

``` AppDbContext.cs
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> dbContextOptions) 
            : base(dbContextOptions)
        {
        }

        public DbSet<Test> Test { get; set; }
        public DbSet<TestA> TestA { get; set; }
        public DbSet<TestB> TestB { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestA>().ToTable(nameof(TestA));
            modelBuilder.Entity<TestB>().ToTable(nameof(TestB));
        }
    }
```
マイグレーション時には、下記のようなDDLが発行されます。`Testテーブル`と`TestAテーブル`、`TestBテーブル`の3つのテーブルが作成され、ON DELETE CASCADEオプション付きで外部制約が張られていることが確認できます。

``` sql
      CREATE TABLE `Test` (
          `Id` int NOT NULL AUTO_INCREMENT,
          `Name` longtext CHARACTER SET utf8mb4 NULL,
          CONSTRAINT `PK_Test` PRIMARY KEY (`Id`)
      ) CHARACTER SET utf8mb4;

      CREATE TABLE `TestA` (
          `Id` int NOT NULL AUTO_INCREMENT,
          `PropA` longtext CHARACTER SET utf8mb4 NULL,
          CONSTRAINT `PK_TestA` PRIMARY KEY (`Id`),
          CONSTRAINT `FK_TestA_Test_Id` FOREIGN KEY (`Id`) REFERENCES `Test` (`Id`) ON DELETE CASCADE
      ) CHARACTER SET utf8mb4;

      CREATE TABLE `TestB` (
          `Id` int NOT NULL AUTO_INCREMENT,
          `PropB` longtext CHARACTER SET utf8mb4 NULL,
          CONSTRAINT `PK_TestB` PRIMARY KEY (`Id`),
          CONSTRAINT `FK_TestB_Test_Id` FOREIGN KEY (`Id`) REFERENCES `Test` (`Id`) ON DELETE CASCADE
      ) CHARACTER SET utf8mb4;
```
ER図にするとクラス定義と同じ形にマッピングされていることが確認できます。個人的にはこちらの方がすっきりしてよさそうな気がしますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/decef40a-c7c5-ae77-74cd-e620e98879c1.png)
データを追加すると、親である`Testテーブル`と子供である`TestAテーブル`、`TestBテーブル`の両方にちゃんとデータ格納されていることが分かります。

``` cs
    await _dbContext.TestA.AddAsync(new TestA { Name = "TestA_1", PropA = "12345"});
    await _dbContext.TestB.AddAsync(new TestB { Name = "TestB_2", PropB = "67890"});

    await _dbContext.Test.AddAsync(new TestA { Name = "TestA_1", PropA = "ABC"});
    await _dbContext.Test.AddAsync(new TestB { Name = "TestB_1", PropB = "DEF"});

    await _dbContext.SaveChangesAsync();
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/7d5787f2-feaf-6769-60a9-1f1010f47e1b.png)
先ほどと同じように1行読み出してみると、下記のように`Testテーブル`と`TestAテーブル`が結合されて取得されているのが分かります。

```
 SELECT `t`.`Id`, `t`.`Name`, `t0`.`PropA`
      FROM `Test` AS `t`
      INNER JOIN `TestA` AS `t0` ON `t`.`Id` = `t0`.`Id`
      WHERE `t0`.`PropA` = '12345'
      LIMIT 1
```

# どっちが良いんだろう
出来上がるテーブルの構造だけ見ると`テーブル単位マッピング`のほうが素直に実装されているので何となくこちらの方がよさそうに見えます。ただし、EF Coreの[パフォーマンスマッピング - 継承のマッピング](https://docs.microsoft.com/ja-jp/ef/core/performance/modeling-for-performance#inheritance-mapping)というトピックでは、Joinは負荷の高い処理であるため従来の`階層単位のマッピング`のほうがパフォーマンスが良くなるという記載があります。

[例にあるクラス](https://github.com/dotnet/EntityFramework.Docs/blob/main/samples/core/Benchmarks/Inheritance.cs)を実際にローカルのコンテナで実行しているMySQLに対してパフォーマンスを計測してみました。
この例ではRootクラスと継承関係にある6つのエンティティー（`Child1`, `Child1A`, `Child1B`, `Child2`, `Child2A`, `Child2B`）に対してそれぞれ5000件のデータを挿入し、全件読み込む速度を計測したものです。
![@startuml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/ae4c31f9-3123-3dbd-9d12-673b59e200a5.png)


``` log:階層単位のマッピング
Inheritance.TPH: DefaultJob [RowsPerEntityType=5000]
Runtime = .NET 5.0.8 (5.0.821.31504), X64 RyuJIT; GC = Concurrent Workstation
Mean = 205.444 ms, StdErr = 1.596 ms (0.78%), N = 99, StdDev = 15.876 ms
Min = 181.314 ms, Q1 = 194.081 ms, Median = 199.921 ms, Q3 = 215.525 ms, Max = 242.547 ms
IQR = 21.444 ms, LowerFence = 161.914 ms, UpperFence = 247.691 ms
ConfidenceInterval = [200.031 ms; 210.858 ms] (CI 99.9%), Margin = 5.413 ms (2.63% of Mean)
Skewness = 0.92, Kurtosis = 2.67, MValue = 2.2
-------------------- Histogram --------------------
[176.810 ms ; 182.918 ms) | @
[182.918 ms ; 191.195 ms) | @@@@@@@@@@
[191.195 ms ; 200.203 ms) | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
[200.203 ms ; 209.577 ms) | @@@@@@@@@@@@@@@@@@@
[209.577 ms ; 219.541 ms) | @@@@@@@
[219.541 ms ; 228.550 ms) | @@@@@@@@@@
[228.550 ms ; 241.748 ms) | @@@@@@@@@@@
[241.748 ms ; 247.051 ms) | @
---------------------------------------------------
```

``` log:テーブル単位マッピング
Inheritance.TPT: DefaultJob [RowsPerEntityType=5000]
Runtime = .NET 5.0.8 (5.0.821.31504), X64 RyuJIT; GC = Concurrent Workstation
Mean = 532.521 ms, StdErr = 3.827 ms (0.72%), N = 98, StdDev = 37.888 ms
Min = 467.127 ms, Q1 = 503.789 ms, Median = 523.570 ms, Q3 = 553.880 ms, Max = 634.075 ms
IQR = 50.092 ms, LowerFence = 428.651 ms, UpperFence = 629.018 ms
ConfidenceInterval = [519.533 ms; 545.510 ms] (CI 99.9%), Margin = 12.989 ms (2.44% of Mean)
Skewness = 0.78, Kurtosis = 2.89, MValue = 2
-------------------- Histogram --------------------
[456.341 ms ; 480.340 ms) | @
[480.340 ms ; 503.044 ms) | @@@@@@@@@@@@@@@@@@@@@@
[503.044 ms ; 526.901 ms) | @@@@@@@@@@@@@@@@@@@@@@@@@@@@
[526.901 ms ; 548.473 ms) | @@@@@@@@@@@@@@@@@@
[548.473 ms ; 573.524 ms) | @@@@@@@@@@@@
[573.524 ms ; 595.096 ms) | @@@@@@@@@@
[595.096 ms ; 622.916 ms) | @@@@@@
[622.916 ms ; 644.861 ms) | @
---------------------------------------------------
```

確かに`階層単位のマッピング`の平均が205.444 msなのに対し、`テーブル単位マッピング`は532.521 msかかっています。考えてみれば、`階層単位のマッピング`は単一テーブルをそのままSelectするのに対し、`テーブル単位マッピング`はRootテーブルと子供テーブルを常にJoinする必要があるのでわからなくもないです。
どちらを利用するかは、メンテナンスコストとパフォーマンスコストのどちらを優先するかをしっかりと検討する必要がありそうですね。

# まとめ

- 継承関係にあるエンティティーの表現方法として、TablePerHierarchy（階層単位のマッピング）とTablePerType（テーブル単位のマッピング）がある
- どちらを利用するかは、それぞれの利用用途に応じて検討する必要がある