情報応用演習Ⅰ(2024)

【T7a】データモデルの変更とマイグレーションの設計(3/7)

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

7a-3. データモデルの変更とマイグレーションの必要性

チュートリアル【T5a】でも述べたが,アプリケーションが扱うデータモデルはアプリケーションの成長 (≒アップデート)とともに変化する.たとえば学生の名簿を管理しているアプリで,学則の変更などに起因して,新たに 「スカラシップ対象かどうか」というフラグを持たせる必要が生じることがあるかもしれない. EF Core では, これは既存のモデルクラスにプロパティを追加することで実現する.もしチュートリアル【T5a】Studentクラス(_左)でこのことを行うならば,_右に示すように bool 型のプロパティを追加することになるだろう.

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
public class Student
{
    [Display(Name = "ID")]
    public int Id { get; set; }                   // ID

    [Display(Name = "姓")]
    public string LastName { 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; }      // 登録日時


}
 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
public class Student
{
    [Display(Name = "ID")]
    public int Id { get; set; }                   // ID

    [Display(Name = "姓")]
    public string LastName { 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; }      // 登録日時

    public bool ApplyScholarship { get; set; }    // スカラシップ対象かどうか
}

注意しなければならないのは, ASP.NET Core ではモデルクラスの定義は常にデータベース側のテーブル定義と関連づいていること である. モデルクラスを変更しただけではデータベース側のテーブルは変更されない.こういった モデルクラスの定義の変更をデータベースの テーブル定義に反映させること がマイグレーションの役割の一つである. チュートリアル【T5a】で説明したように,dotnet ef migrations add コマンドで生成した マイグレーションのためのコード( Migrations フォルダの .cs ファイル)はこのことを行っている.

マイグレーションのもう一つの役割は, データベース上に既にあるデータを新しいデータ定義に合わせること である. 例えば,_Studentクラスには学生の名前に関しては姓(FirstName)と名(LastName)しかないが, 留学生が増えたので ミドルネーム を管理する必要が出てきたと仮定しよう.先ほどのStudentクラスをこの要求に合わせるには _のようなプロパティを追加すればよいだろう( 警告: まだこの内容は書き込まないこと ).

「ミドルネーム」を追加する

ミドルネームは全員が持っているわけではないのでMiddleNameプロパティは string のNull許容型( string? 型)となっている.

そして,この変更を行う前は ミドルネームはLastNameに空白区切りで書き込む,という運用をしていた と仮定する. つまり現状のデータベース上はミドルネームは「姓」の列に書き込まれている,ということである(_).

ミドルネームの運用

この場合,データベース上でMiddleNameという列を追加するだけでなく,LastNameの列に書き込まれているミドルネームを削除して MiddleNameの列に移し替える,という必要が出てくる.マイグレーションではこのような 既存データの変換作業 も必要である. 注意しなければならないのは,このような既存データの変換作業のためのコードは プログラマが手作業で書かなければならない ということである. これは,プロパティ(≒列)の追加や削除に関しては,dotnet ef migrations add コマンドが新旧のモデルクラスの定義を比較して自動生成してくれるが, このような既存データの変換作業までは自動生成することができないためである.

実際にこのような既存データの変換作業のためのコードを記述してみよう.

まず, T7a プロジェクトを起動して_に示す学生情報を新規作成しよう(コピペ推奨,ただしペースト時に前後に半角スペースが入らないように注意).

追加する学生情報
#性別電話番号メールアドレス誕生日
医療太郎(適当でよい)(適当でよい)(適当でよい)(適当でよい)
アウグスト ナイトハルトカール同上同上同上同上
ライデノビッチ ライコフイワン同上同上同上同上
ファルブルケ ウィンゲーツ ヘルシングインテグラル同上同上同上同上

#の列はIDではなく項番を表しているだけである.

学生の一覧画面に_のように表示されたら, pgAdmin で Students テーブルの内容も確認しておこう. pgAdmin を起動して,「 Databases 」→「 t7a_db 」を右クリックして「 Query Tool 」をクリックし(_), Students テーブルの全内容を表示するSQL文を実行して_で追加した情報が追加されていることを確認しよう(_). 「 Databases 」以下に「 t7a_db 」が表示されていない場合は,「 Servers 」→「 PostgreSQL 16 」→「 Databases 」を右クリックして「 Refresh 」を実行する(_). なおId列の値,つまり各レコードの主キーの値についてはデータの登録状況や経緯などによって変化するため必ずしも_の通りにはならないことを付記しておく.

追加した学生情報の確認

次に実行を停止して,_に示すようにStudentsクラスにMiddleNameプロパティを追加する.

「ミドルネーム」を追加する
 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
public class Student
{
    [Display(Name = "ID")]
    public int Id { get; set; }                   // ID

    [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; }      // 登録日時
}

これで 今現在のデータベース上のテーブル定義とモデルクラスの定義が食い違うことになった .この状態で実行するとエラーになることを 確かめておこう.この状態で実行して_のようにIndexアクションなど,データベースにアクセスするいずれかのアクションにアクセスすると,_に示すような例外が発生する.

モデルクラスの更新に伴うエラー

データベースサーバー側でエラーが発生したことを示すPostgresException例外が発生している. 例外メッセージは「42703: column s.MiddleName does not exist」のようになっていることが分かるだろう.筆者が訳すまでもないと思われるが, このメッセージは「列 s.MiddleName は存在しない.」と言っている.このときにデータベースサーバで直前に実行されたSQL文も確認してみよう. データベースサーバでの直前のエラー内容を確認するには,コマンドラインターミナル 1_に示す コマンドを実行する.

データベースサーバでの直前のエラーを確認するためのコマンド
PS> Get-Eventlog -LogName Application -Source PostgreSQL -EntryType Error | select -ExpandProperty Message -First 1

すると_の2行目ようなSQL文が実行されていることが分かるはずである.

直前のSQL文の取得コマンドの実行結果
2022-01-25 15:52:20.872 JST [47140] ERROR:  column s.MiddleName does not exist at character 69
2022-01-25 15:52:20.872 JST [47140] STATEMENT:  SELECT s."Id", s."Birthday", s."FirstName", s."LastName", s."Mail", s."MiddleName", s."PhoneNumber", s."Registered", s."Sex"
        FROM "Students" AS s
        ORDER BY s."Id"

強調した部分に着目しよう.このSELECT文ではMiddleNameという名前の列を要求している.では 現状のテーブル定義を確認してみよう . pgAdminで「 Servers 」→「 PostgreSQL 16 」→「 Databases 」→「 t7a_db 」→「 Schemas 」→「 public 」→「 Tables 」→「 Students 」を右クリックして「 Properties 」をクリックしする(_). すると Students テーブルについての詳細情報を表示する子ウィンドウが開くので「 Columns 」タブをクリックする. この画面が今現在このテーブルに定義されている列の一覧である(_).

現状のテーブル定義の確認

_に示すように, 現状ではStudentsテーブルにはMiddleNameなどという列は含まれていない ことが分かるだろう. ここまでの説明の通り モデルクラスの定義を単に変更するだけではデータベースのテーブル定義は一切変化しない ということを覚えておこう.

重要なことなのでもう一度書いて強調しておこう.モデルクラスの定義を単に変更するだけではデータベースのテーブル定義は一切変化しない.モデルクラスの定義を変更した際は以降に示すマイグレーション処理の作成と実行を行う必要がある.

ではモデルクラスの変更をデータベースに反映させる操作を行おう.プログラムを実行したままになっている場合は一旦停止して, コマンドラインターミナル 1_に示すコマンドを実行する. 注意としてこのコマンドを実行する前に カレントディレクトリが,操作対象のプロジェクトのプロジェクトフォルダになっている ことを確認しておこう.

このコマンドを実行する前に一度以上プロジェクトをビルドしておく必要がある.また,ソリューションに含まれるプロジェクトに1つでもコンパイルエラーがあるとこのコマンドは失敗することがある.このためこのコマンドを実行する際は,事前にソリューション全体を一度ビルドして一つもコンパイルエラーがないことを確認しておくこと.

マイグレーション処理の生成
PS> dotnet ef migrations add AddMiddleName

これによって Migrations フォルダにコマンドを実行した日時_AddMiddleName.csというファイルが生成される. このファイルは_に示すような内容になっているはずである.なおコメントは筆者によるものである. もし,_ のようになっていない場合は,ここまでの操作を間違えている可能性がある. 手順を見直し適切に修正すること決して_のコードを手打ちするなどしてつじつまを合わせてはならない

自動生成されたマイグレーションコード

以前の説明の通りUp()メソッドは既存のデータベース定義を新しいデータベース定義に合わせるための処理を記述する場所, Down()メソッドはその変更を打ち消す処理を記述する場所である. APIの詳細は省略するが,おおむね①の部分(11~15行目)ではAddColumn()というメソッドによって, StudentsテーブルにMiddleNameという列を追加しており,逆に②の部分(19~23行目)では DropColumn()というメソッドによってMiddleName列を削除している,ということが分かるだろう.

さきほど「 ミドルネームはLastNameに空白区切りで書き込む,という運用をしていた 」という状況設定を書いたが, この自動生成されたマイグレーションコードではMiddleName列の追加と削除を行うのみで,既存のレコードのLastName列に 書きこまれているミドルネームを取り出してMiddleName列に移し替えたり,その逆を行ったりといったコードは生成されていない. これはある意味当然であるが EntityFramework Core にはもちろんそのような運用されていたということを知るすべはない ので, donet ef migrations add」コマンドは最低限の変換処理のみを含むマイグレーションコードしか生成することしかできない のである.

したがって,「既存のレコードのLastName列に書き込まれているミドルネームを取り出してMiddleName列に移し替える」といった 新旧のデータのすり合わせ処理を実装するのはプログラマーの責任 ということである.

ではこのすり合わせ処理を実装してみよう.このAddMiddleNameクラスのUp/Downメソッドに_に示す 内容を追記する.

マイグレーションコードへの追記内容
 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
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace T7a.Migrations
{
    public partial class AddMiddleName : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<string>(  //
                name: "MiddleName",              //
                table: "Students",               // ① MiddleName列の追加
                type: "text",                    //
                nullable: true);                 //

            // ③ LastName列からミドルネームを分離する
            migrationBuilder.Sql(
                "UPDATE \"Students\" " +
                "  SET \"MiddleName\" = COALESCE((regexp_match(\"LastName\", '^\\s*(.+)\\s+(\\S+)\\s*$'))[1], NULL), " +
                "      \"LastName\"   = COALESCE((regexp_match(\"LastName\", '^\\s*(.+)\\s+(\\S+)\\s*$'))[2], \"LastName\")");
    
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            // ④ MiddleName列とLastName列を結合する
            migrationBuilder.Sql(
                "UPDATE \"Students\" " +
                "  SET \"LastName\" = COALESCE(\"MiddleName\" || ' ' || \"LastName\", \"LastName\")");
            
            migrationBuilder.DropColumn(         //
                name: "MiddleName",              // ② MiddleName列の削除
                table: "Students");              //
        }
    }
}

migrationBuilderSql()メソッドは,その名の通りデータベース上でSQL文を実行するコマンドである. Up()メソッドでは,AddColumn()メソッド(=列の追加)の直後に③のSQL文を実行して,既存のLastName列を空白区切りで分解して 新たに追加したMiddleName列にセットしている(同時にLastName列に含まれていたミドルネーム部分を削除している). またDown()メソッドでは,DropColumn()メソッド(=列の削除)の直前に②のSQL文を実行して,既存のMiddleName列の内容を 空白区切りでLastName列に結合している.

_の追記ができたら 忘れずにソースコードを保存してから ,コマンドラインターミナル 1_に示すコマンドを実行しよう.

マイグレーション処理の実行
PS> dotnet ef database update

_を実行したら, pgAdmin で Students テーブルの全内容を確認してみよう. MiddleName列が追加され,LastName列からミドルネームが削除されてMiddleName列にその内容が移行したことが分かるだろう(_). これは先ほどのUp()メソッドの作用によるものである.

マイグレーション処理の実行後

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

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

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