3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめてのアドベントカレンダーAdvent Calendar 2024

Day 9

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

Posted at

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

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

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

1. モデル
仕入れテーブル

    /// <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もこれと同じ内容でモデルを定義します(割っつ愛)

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

    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 });
        }
    }

3. バックアップ処理
こうなります。

        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を使うとパフォーマンスよいかも。

        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();
        }

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

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

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

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

2. DbContext
Purchaseテーブル、PurchaseBackupテーブルのDbSetの型はどちらもDbSet<Purchase>となります。ただし、明示的に共有型エンティティタイプをセットする必要があります。
また、OnModelCreatingPurchaseを共有型エンティティタイプとして登録します。

    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が走るの嫌だという方は

    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; }
    }

でよいかと。

3. バックアップ処理

    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でとってきています。しかし型が取れずExceptionが発生します。
メソッドの仕様を読むと、このメソッド、共有型エンティティタイプ(shared type entity type)は未対応のようです。

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

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

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

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?