情報応用演習Ⅰ(2024)

【T7b】複数のモデルクラスの連携(5/9)

プロジェクトタイプ(注意: 本文参照)
プロジェクト名T7b
ソリューション名PIT7
注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備作業を参照せよ.

7b-5. 新たなモデルクラスの作成

それでは実際に「学科」を表すモデルクラスを作成してみよう. この「学科」の情報には_に示す項目を含めることにしよう. 主キーと学科名だけを含むシンプルなモデルクラスである.

「学科」の情報
項目名必須/任意データ型備考
ID必須整数学科のID.
名前必須文字列学科名.

この「学科」に対応するモデルクラスを定義しよう.クラス名は「学科」を単純に 英訳してDepartmentとする.まずプロジェクト内の Models フォルダを右クリックし,「追加」→「クラス」をクリックする. 作成するクラス名を訊かれるのでDepartment(.csは省略可能)と入力して「追加」ボタンをクリックする. すると空のクラス定義が作られるので_の定義を書き込もう.

「学科」クラス
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
using System.ComponentModel.DataAnnotations; // 追記

// 「学科」クラス
namespace T7b.Models
{
    public class Department
    {   
        [Display(Name = "ID")]
        public int Id { get; set; }
        
        [Display(Name = "学科名")]
        public string Name { get; set; } = "";
    }
}

このクラスがデータベース側に反映されるようにするには,データコンテキストクラスにも手を加える必要がある. Data/T7bContext.cs に_に示す内容を追記しよう.

T7bContextクラスへの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using Microsoft.EntityFrameworkCore;
using T7b.Models;

namespace T7b.Data
{
    public class T7bContext : DbContext
    {
        public T7bContext(DbContextOptions<T7bContext> contextOptions)
            : base(contextOptions)
        { }

        public DbSet<Student> Students => Set<Student>();

        public DbSet<Department> Departments => Set<Department>(); // 追記
    }
}

さてここまでの追記によって作られたのは,「学生」と関係性を持たない独立した「学科」というモデルクラスである. 「学科」と「学生」とのあいだに一対多の関係を作るには,「多」の側のモデルクラス─つまりこの場合はStudentsクラスに 二つのプロパティを追加する必要がある. Models/Student.cs に_に示す内容を追記しよう.

Studentクラスへの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 「学生」クラス
public class Student
{
    [Display(Name = "ID")]
    public int Id { get; set; }

    [Display(Name = "姓")]
    public string LastName { get; set; } = "";

    [Display(Name = "ミドルネーム")]
    public string? MiddleName { get; set; }

    [Display(Name = "名")]
    public string FirstName { get; set; } = "";

    [Display(Name = "性別")]
    public SexType Sex { get; set; }

    [Phone]
    [Display(Name = "電話番号")]
    public string PhoneNumber { get; set; } = "";

    [EmailAddress]
    [Display(Name = "メールアドレス")]
    public string? Mail { get; set; }

    [DataType(DataType.Date)]
    [Display(Name = "誕生日")]
    public DateTime? Birthday { get; set; }

    [DataType(DataType.DateTime)]
    [Display(Name = "登録日時")]
    public DateTime Registered { get; set; }

    // ① 外部キー(Departmentを参照)
    [Display(Name = "所属学科")]
    public int DepartmentId { get; set; }

    // ② ↑のためのナビゲーションプロパティ
    public Department? Department { get; set; } 
}

①の int 型のプロパティDepartmentIdがデータベース上で外部キーとなるプロパティである. このプロパティはこの例のように「1」側←の主キープロパティ名 という名前である必要がある.

②は ナビゲーションプロパティ といい,C#のプログラム上でその「学生」が在籍している「学科」のデータ, すなわち Departmentクラスのオブジェクト にアクセスするためのプロパティである. ナビゲーションプロパティは ①で参照しているモデルクラス(Departmentクラス)のNull許容型にしておく必要がある .ちなみに,「多」の側から「1」の 側のモデルクラスを参照するナビゲーションプロパティを参照ナビゲーションプロパティ という. Departmentプロパティにアクセスすると,DepartmentIdプロパティの値と等しい Idプロパティの値を持つDepartmentクラスのオブジェクトを参照することができる. 注意する点は,外部キーとして使用されるDepartmentIdプロパティは実際にデータベースにテーブルの列として反映されるが, ナビゲーションプロパティそれ自体は データベース上で反映されるものではない ことである. ナビゲーションプロパティはあくまでもプログラミングをする上での利便性のためのものである が,非常に重要なものである. EF Core における外部キーのようなリレーションシップの詳細に関しては Relationships - EF Core - Microsoft Docs を読むとよいだろう.

ここまで追記ができたら,コマンドラインターミナル1_に 示すコマンドを実行して,マイグレーションコードの生成とデータベース側への適用を行ってみよう(_). この際にカレントディレクトリが,操作対象のプロジェクトのプロジェクトフォルダになっていることを確認しておこう. また,このコマンドを実行する前に一度以上プロジェクトをビルドしておく必要があることも注意しておこう.

先に書いておくと,この状況では dotnet ef database update コマンドはエラーとなって失敗するもし,dotnet ef database updateコマンドがエラーにならない場合は,ここまでの 手順で何か間違いを犯している可能性がある.そのまま進まずにそれまでの作業内容を見直し場合によってはこの章の最初からのやり直しを検討すること.

マイグレーション処理の生成と適用
PS> dotnet ef migrations add AddDepartment
PS> dotnet ef database update
実行結果

なぜ失敗してしまうのだろうか?エラーメッセージを見てみると以下のように書かれているはずである.

23503: insert or update on table "Students" violates foreign key constraint "FK_Students_Departments_DepartmentId"

頭の数値は単にエラーコードだが,メッセージの部分を直訳すればおおむね以下のような内容である.

23503: "Students"テーブルに対する挿入または更新が,外部キー制約"FK_Students_Departments_DepartmentId"に違反しました.

_Studentクラスの変更によって,データベース側のテーブルにどのような変更が行われるのか,マイグレーションコードをもとに考えてみよう. dotnet ef migrations add コマンドによって, Migrations フォルダにコマンドを実行した日時_AddDepartment.csが生成されているはずなので開いて内容を確認してみよう.このコードは_のようになっているはずである.なお,_には説明のためにコメントを追加してある. もし,_ のようになっていない場合は,ここまでの操作を間違えている可能性がある. 手順を見直し適切に修正すること決して_のコードを手打ちするなどしてつじつまを合わせてはならない

生成されたマイグレーションコードの内容

コメントで示した通り,Up()メソッドでは主に以下の3つの作業をこの順番で行っている.

  • StudentsテーブルにDepartmentId列を追加する.
  • Departmentsテーブルを作成する.
  • StudentsテーブルのDepartmentId列に外部キー制約を追加する.

エラーの原因一つはDepartmentIdプロパティがNull 許容な,つまり 値の設定が必須な型 のプロパティであることである. この場合,対応するデータベース上の列にはNOT NULL制約が課せられることは先ほど説明した.したがって,この列は空にする(NULLをセットする)ことができない. そしてもう一つの原因は, ②で作成したばかりのDepartmentsテーブルには当然のことながら一件もレコードが登録されていない ことである. StudentsテーブルのDepartmentIdプロパティは,外部キーとなるプロパティであり,Departmentsテーブル内のいずれかの レコードの主キーと同じ値を持つ必要がある.しかし, Departmentsテーブルが現時点ではそもそも空なのでこの列にセットできる有効な値が全く存在しない のである.

Studentsテーブルには,もともと5件程度のレコードを登録しているはずであるが,この外部キー制約を伴うDepartmentId列を 追加したことによって 既存のすべてのレコードが制約違反になってしまう のである. これを避ける方法はいくつか存在する.1つは,Up()メソッド内で③の前にDepartmentsテーブルにSql()メソッドなどで INSERT文を実行して適当なレコードを追加し,そのレコードのIDをUPDATE文で既存のStudentsテーブルのレコードにセットする方法である. これは分かりやすい反面,モデルクラスの値を手作業で(≒生のSQL文を用いて直接的に)データベースに登録することにほかならず,構築が複雑となるようなモデルクラスが あった場合に作業が極めて煩雑になってしまうという欠点がある.もう一つは,そもそもDepartmentIdに NOT NULL 制約が課されないように するという方法である.これはStudentsクラスのDepartmentIdプロパティの型を int 型から int? 型に変更するだけで実現できる. この方法であれば制約違反は発生しないが,同時に 学生が所属する学科がNULLである という状況を許すようになってしまう. 先ほどの分析の通り,学生は必ずいずれかの学科に所属しているはずでありそれを未設定にできてしまうのは問題である.

もう一つの方法は,マイグレーション処理を「1.Departmentクラスの追加」と「2.StudentsクラスへのDepartmentIdプロパティの追加」の 2つに分割する ことである.実はデータコンテキストクラスに手を加えることによって, 特定のモデルクラスに決め打ちのレコードをあらかじめ追加しておくことが可能 なのである. 「1.Departmentクラスの追加」のためのマイグレーション時にあらかじめ「学科」のレコードを登録しておき, 「2.StudentsクラスへのDepartmentIdプロパティの追加」時に登録したレコードの情報をセットすれば問題なくマイグレーションを行うことができる. 次節では実際にこのアプローチでマイグレーション処理を作成してみよう.

そのまえにこの失敗するマイグレーション処理は削除しておこう.幸い dotnet ef database update コマンドは先ほどエラーとなったので, データベース側にはいかなる変更も行われてはいない.そのため_に示すように dotnet ef migrations remove コマンドを実行しておくだけでよい.

マイグレーション処理の削除
PS> dotnet ef migrations remove

また,_Studentクラスに追加した外部キープロパティとナビゲーションプロパティも _のようにいったんコメントアウトしておこう.

Studentクラスへの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 「学生」クラス
public class Student
{
    [Display(Name = "ID")]
    public int Id { get; set; }

    [Display(Name = "姓")]
    public string LastName { get; set; } = "";

    [Display(Name = "ミドルネーム")]
    public string? MiddleName { get; set; }

    [Display(Name = "名")]
    public string FirstName { get; set; } = "";

    [Display(Name = "性別")]
    public SexType Sex { get; set; }

    [Phone]
    [Display(Name = "電話番号")]
    public string PhoneNumber { get; set; } = "";

    [EmailAddress]
    [Display(Name = "メールアドレス")]
    public string? Mail { get; set; }

    [DataType(DataType.Date)]
    [Display(Name = "誕生日")]
    public DateTime? Birthday { get; set; }

    [DataType(DataType.DateTime)]
    [Display(Name = "登録日時")]
    public DateTime Registered { get; set; }

    //// ① 外部キー(Departmentを参照)
    //[Display(Name = "所属学科")]
    //public int DepartmentId { get; set; }

    //// ② ↑のためのナビゲーションプロパティ
    //public Department? Department { get; set; }
}

本節で覚えておいてほしいことは,「 すでにレコードが存在しているテーブルに,必須な(=Null非許容な)外部キープロパティ(列)をいきなり追加してはならない 」ということである. その必要がある場合は,前述の通りマイグレーションを2つに分けるのが無難な方法である.


  1. タブのタイトルは「開発用PowerShell」もしくは「Developer PowerShell」となっている. ↩︎

Last updated on 2024-06-10
Published on 2024-06-10

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.