Entity Framework Core - Owned typesのサンプル

EF Coreの「Owned types」を試してみました。日本語訳はどうなるんでしょう。所有型?かな?

What is new in EF Core 2.0 - EF Core | Microsoft Docs

「Owned types」はEF6の複合型に似た機能のようで、テーブルにある複数カラムをプロパティクラスにマッピングするものです。

ということで早速。次のテーブルがあるとします。

-- テーブル作成
drop table if exists dbo.Character;
create table dbo.Character(
    Id int,
    Name nvarchar(4) not null,   -- 名前
    Level int not null,   -- レベル
    Hp int not null,  -- HP
    Mp int not null,  -- MP
    constraint PK_Character primary key(Id)
);

次のモデルにマッピングしてみましょう。HpとMpのカラムは再利用したいという体でStatusクラスとしています。

// ステータス
public class Status {
    public int Hp { get; set; }
    public int Mp { get; set; }
}

// キャラクター
public class Character {
    public int Id { get; set; }
    public string Name { get; set; }
    public int Level { get; set; }
    // HPとMPを持つステータス
    public Status Status { get; set; }
}

マッピングするにはDBコンテキストのOnModelCreatingメソッド内でOwnsOneメソッドを使ってStatusプロパティを指定します。

OwnsOneメソッドの2つ目の引数では入れ子になったStatusクラスのマッピングも指定できます。マッピングを指定しないとStatusクラスの各プロパティは「Status_Hp」「Status_Mp」といったカラム名マッピングされるので、下記コードでは実際のカラム名に合うように調整しました。

// DBコンテキストの一部
public class AppDbContext : DbContext {
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        // Characterテーブルへのマッピング
        modelBuilder.Entity<Character>().ToTable(nameof(Character));

        // CharacterがStatusを所有する
        modelBuilder.Entity<Character>().OwnsOne(
            character => character.Status,
            statusBuilder => {
                // StatusをHpカラムとMpカラムへマッピング
                // (デフォルトだと「Status_Hp」「「Status_Mp」」といったカラムにマッピングされる)
                statusBuilder.Property(status => status.Hp).HasColumnName(nameof(Status.Hp));
                statusBuilder.Property(status => status.Mp).HasColumnName(nameof(Status.Mp));
            });
    }
}

これで準備が整ったので、うまくマッピングできているか確認してみましょう。

class Program {
    // 結果出力用
    private static void Dump(IEnumerable<Character> characters) {
        foreach (var character in characters) {
            Console.WriteLine($"{character.Name}");
            Console.WriteLine($"Lv {character.Level}");
            Console.WriteLine($"HP {character.Status.Hp}");
            Console.WriteLine($"MP {character.Status.Mp}");
            Console.WriteLine();
        }
    }

    static void Main(string[] args) {
        // データ投入
        using (var dbContext = new AppDbContext(tracking: true)) {
            dbContext.Set<Character>().AddRange(
                new Character {
                    Id = 1,
                    Name = "エイト",
                    Level = 10,
                    Status = new Status { Hp = 59, Mp = 32 },
                },
                new Character {
                    Id = 2,
                    Name = "ゼシカ",
                    Level = 10,
                    Status = new Status { Hp = 47, Mp = 28 },
                });

            dbContext.SaveChanges();
        }

        // 確認
        using (var dbContext = new AppDbContext()) {
            var characters = dbContext.Set<Character>().ToList();
            Dump(characters);
            /*
           エイト
           Lv 10
           HP 59
           MP 32
           ゼシカ
           Lv 10
           HP 47
           MP 28
           */
        }

        // データ更新
        using (var dbContext = new AppDbContext(tracking: true)) {
            // エイトを取得
            var character = dbContext.Set<Character>().Find(1);

            // エイトはレベルがあがった!
            character.Level += 1;
            character.Status.Hp += 12;
            character.Status.Mp += 2;

            dbContext.SaveChanges();
        }

        // 確認
        using (var dbContext = new AppDbContext()) {
            var characters = dbContext.Set<Character>().ToList();
            Dump(characters);
            /*
           エイト
           Lv 11
           HP 71
           MP 34
           ゼシカ
           Lv 10
           HP 47
           MP 28
           */
        }
    }
}

コードとクエリはこちら。

EF CoreのOwned typesのサンプル · GitHub