EF Coreで同じテーブル定義のテーブルデータを１つのエンティティモデルで扱う

EntityFrameworkは、各テーブルに対応するモデル`TEntity`型を定義し、`DbSet<TEntity>`でテーブルのデータにアクセスします。また、テーブル名＝モデル名とするのが一般的です。
ですので、同じ構造のテーブルが複数ある場合でも、
・それぞれのテーブルのモデルを用意する必要がある。
・同じカラム定義のテーブル間のデータの移行は、エンティティを移し替える必要がある
と、なんか不効率だなぁと思っていました。

最近、機会があったので、これを１つのエンティティモデルでアクセスすることにトライしてみました。

# 例
かんたんな例として、商品の仕入れを管理するテーブルと、半年経過した仕入れ情報をバックアップテーブルに移行する処理を考えます。これまで通りテーブルとモデルを1:1の関係で作成します。

１. モデル
仕入れテーブル
```CSharp
    /// <summary>
    /// 仕入れ情報
    /// </summary>
    public class Purchase
    {
        /// <summary>店舗ID</summary>
        [Key, Column(Order = 0)]
        public int StoreId { get; set; }

        /// <summary>商品ID</summary>
        [Key, Column(Order = 1)]
        public int ProductId { get; set; }

        /// <summary>仕入れ日</summary>
        [Key, Column(Order = 2)]
        public DateTime PurchaseDate { get; set; }

        /// <summary>数量</summary>
        public int Quantity { get; set; }
    }
```    
バックアップテーブル`PurchaseBack`もこれと同じ内容でモデルを定義します（割っつ愛）

２.DbContext
それぞれのテーブルの`DbSet`は、当然それぞれのモデルの型での定義となります。

```CSharp
    internal class MyContext : DbContext
    {
        // 仕入れ情報
        public DbSet<Purchase> Purchase { get; set; }

        // 仕入れ情報バックアップ
        public DbSet<PurchaseBackup> PurchaseBackup { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // 複合キー
            modelBuilder.Entity<Purchase>()
                .HasKey(si => new { si.StoreId, si.ProductId, si.PurchaseDate });
        }
    }
```
３. バックアップ処理
こうなります。
```CSharp
        private void Backup()
        {
            // 半年前のデータ抽出
            var queryDatas = _context.Purchase.Where(x => x.PurchaseDate < DateTime.Now.AddDays(-180));

            var backupList = new List<PurchaseBackup>();
            foreach (var data in queryDatas)
            {
                // 詰め替え
                backupList.Add(new PurchaseBackup()
                {
                    StoreId = data.StoreId,
                    ProductId = data.ProductId,
                    PurchaseDate = data.PurchaseDate,
                    Quantity = data.Quantity,
                });
            }

            // バックアップテーブルにAdd＆Save
            _context.PurchaseBackup.AddRange(backupList);
            _context.SaveChanges();
        }
```

詰め替え処理は次のように書けば少しは汎用的なります。
また、[EFCore.BulkExtensions](https://www.nuget.org/packages/EFCore.BulkExtensions)を使うとパフォーマンスよいかも。

```Csharp
        private void Backup2()
        {
            // 半年前のデータ抽出
            var queryDatas = _context.Purchase.Where(x => x.PurchaseDate < DateTime.Now.AddDays(-180));

            var props = queryDatas.First().GetType().GetProperties();
            var backupList = new List<PurchaseBackup>();
            foreach (var data in queryDatas)
            {
                var backupItem = new PurchaseBackup();
                foreach (var property in props)
                {
                    var memberValue = property.GetValue(data);
                    property.SetValue(backupItem, memberValue);
                }
                backupList.Add(backupItem);
            }

            _context.BulkInsert(backupList);
            _context.BulkSaveChanges();
        }
```

この例ならまだカラムが数個なので苦にならないですが、大量にある、バックアップ対象のものがたくさんあるとげんなりします。

# 共有型エンティティタイプを使う

https://learn.microsoft.com/ja-jp/ef/core/modeling/entity-types?tabs=data-annotations#shared-type-entity-types

ということで、EntityFrameworkの共有型エンティティタイプを使うことで、同じモデルを複数テーブルに使用することができます。

上前述の仕入れ情報のバックアップの、モデル定義、処理は次のようになります。
１.モデル
モデルは、`Purchase`のみ用意すればよく、`PurchaseBackup`は不要となります。

２. DbContext
Purchaseテーブル、PurchaseBackupテーブルのDbSetの型はどちらも`DbSet<Purchase>`となります。ただし、明示的に共有型エンティティタイプをセットする必要があります。
また、`OnModelCreating`で`Purchase`を共有型エンティティタイプとして登録します。
```CSharp
    internal class MyContextEx : DbContext
    {
        // 仕入れテーブル
        public DbSet<Purchase> Purchase => Set<Purchase>("Purchase");

        // 仕入れバックアップテーブル
        public DbSet<Purchase> PurchaseBackup => Set<Purchase>("Purchase");

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // 共有型エンティティとして
            modelBuilder.SharedTypeEntity<Purchase>("Purchase")
                .HasKey(si => new { si.StoreId, si.ProductId, si.PurchaseDate });
        }
    }
```
アクセサを書きたいし、アクセスするたびに、`Set`が走るの嫌だという方は

```CSharp
    private readonly DbSet<Purchase> _purchase;
    private readonly DbSet<Purchase> _purchaseBackup;

    public MyContextEx()
    {
        _purchase = Set<Purchase>("Purchase");
        _purchaseBackup = Set<Purchase>("Purchase");
    }

    public DbSet<Purchase> Purchase {get}=> _purchase;
    {
        get { return _purchase; }
    }

    public DbSet<Purchase> PurchaseBackup => _purchaseBackup;
    {
        get { return _purchaseBackup; }
    }
```
でよいかと。

３. バックアップ処理
```CSharp
    private void BackupEx()
    {
        var backupDatas = _context.Purchase.Where(x => x.PurchaseDate < DateTime.Now.AddDays(-180));
        _context.PurchaseBackup.AddRange(backupDatas);
        _context.SaveChanges();
    }
```
**あーすっきり！**

# おち
さて、共有型エンティティタイプで複数テーブルにアクセスするコードを書いて実行してみました。ちゃんと動きます。

いや、BulkExtentionの処理で飛びます。
追っていくと、BulkExtention内の処理で、テーブルの型情報を[IModel.FindEntityType](
https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.entityframeworkcore.metadata.imodel.findentitytype?view=efcore-9.0&devlangs=csharp&f1url=%3FappId%3DDev17IDEF1%26l%3DJA-JP%26k%3Dk(Microsoft.EntityFrameworkCore.Metadata.IModel.FindEntityType)%3Bk(DevLang-csharp)%26rd%3Dtrue)でとってきています。しかし型が取れずExceptionが発生します。
メソッドの仕様を読むと、このメソッド、共有型エンティティタイプ（shared type entity type）は未対応のようです。

ということで、共有型エンティティタイプとEFBulkExtentionは今のところ併用できなさそうです。

ん－、なんだかなぁ。結局バックアップはストアド プロシージャとかのほうがいいのかな。

ーーーー
*「きょうゆうがた」で変換するといっつも今日夕方になる...*