[Entity Framework Core] 継承されたモデルクラスを EF Core 3.0 で扱う

[Entity Framework Core] 継承されたモデルクラスを EF Core 3.0 で扱う

こんにちは。最近、私有のディスプレイが寿命を迎えつつある k-so16 です。新しいディスプレイの購入を検討しないといけませんね。

Entity Framework Core 3.0 (以下 EF Core 3.0 と表記) で 派生クラスのモデル を利用してデータベースにアクセスしようとすると、以下のようなエラーが出ました。

A key cannot be configured on ‘Attendance’ because it is a derived type. The key must be configured on the root type ‘LectureBase’.

すべてのモデルクラスが別のクラスの親クラスにならないようにクラスを定義すればエラーは出力されないのですが、 複数のモデルで共通するプロパティの定義は親クラスにまとめておきたい ところです。

本記事では、 EF Core 3.0 で 派生クラスのモデル を使用する方法を紹介します。

想定するモデル

本記事では、講義の出席状況データの取得を想定してデータ構造やモデルを定義します。 複数のモデルで共通するプロパティ を持つ場合は、 親クラスのプロパティとして定義 して共通化します。

取得するデータの構造

講義に関する基本的な情報のデータ構造を以下のように定義します。

カラム名 型 説明
lecture_id nvarchar 講義ID
lecture_name nvarchar 講義名
period int 開講時限
academic_year int 開講年度

出欠情報と詳細な出欠情報のデータ構造をそれぞれ以下のように定義します。

  • 出欠情報の取得

    カラム名 型 説明
    lecture_id nvarchar 講義ID
    lecture_name nvarchar 講義名
    period int 開講時限
    academic_year int 開講年度
    student_id nvarchar 学籍番号
    student_name nvarchar 氏名
    attendance int 出欠
    lecture_date datetime 開講日
  • 詳細な出欠情報の取得

    カラム名 型 説明
    lecture_id nvarchar 講義ID
    lecture_name nvarchar 講義名
    period int 開講時限
    academic_year int 開講年度
    student_id nvarchar 学籍番号
    student_name nvarchar 氏名
    attendance int 出欠
    lecture_date datetime 開講日
    department_id nvarchar 学科ID
    department_name nvarchar 学科名

取得対象のデータのモデル定義

上記で定義したデータ構造に基づいてモデルを定義します。講義の基本的な情報を取得するためのクラスを LectureBase, 出欠情報を取得するためのクラスを Attendance, 詳細な出欠情報を取得するためのクラスを StudentAttendance とします。

LectureBase を基底クラスとして、 Attendance は LectureBase を派生することで講義情報に学生と出欠の情報を加えます。詳細な出欠情報を取得するために StudentAttendance は Attendance を派生して追加するプロパティを定義します。

efcore3 model extension 1

モデルの継承関係を表すクラス図

基底クラスと継承クラスの定義例
/// <summary>
/// 講義情報
/// </summary>
public class LectureBase
{
    /// <summary>
    /// 講義ID
    /// </summary>
    /// <value></value>
    public string LectureId { get; set; }
    /// <summary>
    /// 講義名
    /// </summary>
    /// <value></value>
    public string LectureName { get; set; }
    /// <summary>
    /// 開講時限
    /// </summary>
    /// <value></value>
    public int Period { get; set; }
     /// <summary>
    /// 開講年度
    /// </summary>
    /// <value></value>
    public int AcademicYear { get; set; }
}

/// <summary>
/// 出欠情報
/// </summary>
public class Attendance : LectureBase
{
    /// <summary>
    /// 学籍番号
    /// </summary>
    /// <value></value>
    public string StudentId { get; set; }
    /// <summary>
    /// 氏名
    /// </summary>
    /// <value></value>
    public string StudentName { get; set; }
    /// <summary>
    /// 出欠
    /// </summary>
    /// <value></value>
    public int Attendance { get; set; }
    /// <summary>
    /// 開講日
    /// </summary>
    /// <value></value>
    public DateTime LectureDate { get; set; }
}

/// <summary>
/// 学生の出欠情報詳細
/// </summary>
public class StudentAttendance : Attendance
{
    /// <summary>
    /// 学科ID
    /// </summary>
    /// <value></value>
    public string DepartmentId { get; set; }
    /// <summary>
    /// 学科名
    /// </summary>
    /// <value></value>
    public string DepartmentName { get; set; }
}

DbContext の設定

DbContext の OnModelCreating() の定義は以下の通りです。

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    builder.Entity<LectureBase>()
        .HasKey(x => new { x.LectureId, x.Period, x.AcademicYear });
    builder.Entity<Attendance>()
        .HasKey(x => new { x.LectureId, x.Period, x.LectureDate, x.StudentId });
    builder.Entity<StudentAttendance>()
        .HasKey(x => new { x.LectureId, x.Period, x.LectureDate, x.StudentId });
}

上記のモデル定義と DbContext の OnModelCreating() の定義ではコンパイルは通るのですが、 OnModelCreating() が実行される際に冒頭に示したエラーが発生します。エラーメッセージには、 キーは派生元のルートクラスのプロパティが利用されなければならない と書かれていますが、 Attendance や StudentAttendance のキーを定義するには、 LectureBase のプロパティでは不十分です。

解決方法

継承されているクラスを 抽象クラスとして定義 することで解決します。 継承がネストしている場合は、継承されているクラスすべてを抽象クラスとして定義 します。この解決方法は kenzauros さんに教えていただきました。

モデルの修正

本記事の例では、 LectureBase と Attendance が継承されているので、それらのクラスを抽象クラスとして定義します。ただし、 Attendance はエンティティとして取得したいので、抽象クラスを AttendanceBase として定義します。 AttendanceBase の定義内容は修正前の Attendance のものと同じです。

public abstract class LectureBase
{
    /* 省略 */
}

public abstract class AttendanceBase : LectureBase
{
    /* 省略 */
}

StudentAttendance の継承先を AttendanceBase に修正します。 StudentAttendance はモデルのインスタンスを生成するので抽象クラスにはしません。

public class StudentAttendance : AttendanceBase
{
    /* 省略 */
}

抽象クラス AttendanceBase は インスタンス化できない ので、Attendance を AttendanceBase の 具象クラス に変更します。プロパティは AttendanceBase と同じものを利用するので、 Attendance 内で プロパティを宣言する必要はありません。

public class Attendance : AttendanceBase
{
}

これらの変更に基づいて修正したクラス図は以下の通りです。

efcore3 model extension 2

抽象クラスを導入したクラス図

DbContext の修正

モデルの定義を修正したら、 DbContext の OnModelCreating() の定義を変更します。

LectureBase は モデルのインスタンスを生成しない ので、 builder.Entity<LectureBase>() を削除 します。

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    builder.Entity<Attendance>()
        .HasKey(x => new { x.LectureId, x.Period, x.LectureDate, x.StudentId });
    builder.Entity<StudentAttendance>()
        .HasKey(x => new { x.LectureId, x.Period, x.LectureDate, x.StudentId });
}

これで OnModelCreating() が実行された際に生じていたエラーが解消して、データベースからデータを取得できるようになります。

総括

本記事のまとめは以下の通りです。

  • 継承するモデルは抽象クラスとして定義
  • OnModelCreating() ではインスタンス化可能なモデルをエンティティとして設定

以上、 k-so16 でした。 EF Core を使いこなすにはまだ時間がかかりそうです。

k-so16