.NET Core で DBマイグレーションを試したら色々ハマった話

こんにちは。ecbeing金澤です。
最近暑くなってきて、フラペチーノの季節だなぁと思います。

f:id:ecb_kkanazawa:20190606210741j:plain
プリンアラモードフラペチーノめちゃくちゃ美味しいですよね
このシトラス果肉追加+バレンシアシロップ追加は神の味でした。
シトラス果肉をプリンの上に追加するか底に追加するか選べるので、ぜひ底に追加してください(ひとくち目からフルーティーに!)

はじめに

今のプロジェクトは初期開発が終わり、2次フェーズへと移行しつつあります。
プロジェクトの立ち上げ時はだいぶバタバタだったので、DB定義は 取りあえずエクセルで管理して、マクロでCREATE文を出力していました。


初期開発はそれでも良かったのですが、改修でカラムの追加や拡張が必要になると、ALTER文は手で作ることになり…
更にブランチ開発環境やらステージング環境やら適用先も増えてしまったため、どの環境にどこまでALTER文を発行したのか管理できなくなってしまいました。


そこで、DBはマイグレーションで管理しようと思い、.NET Core の DBマイグレーションを試してみたのですが、色々ハマった挙句採用までたどり着けなかったので、そのいきさつを書こうと思います…

やったこととハマったこと

.NET Coreのバージョン確認

今回は 2.2 で進めます。
ちなみに DB は MySQL です。

c:\work>dotnet --version
2.2.104

まずはプロジェクトの作成

dotnet new web コマンドで、適当にプロジェクトを作ります。
 -o には好きなプロジェクト名を指定します。

c:\work>dotnet new web -o Ecbeing.Sample.Migration
テンプレート "ASP.NET Core Empty" が正常に作成されました。

作成後のアクションを処理しています...
'dotnet restore' を Ecbeing.Sample.Migration\Ecbeing.Sample.Migration.csproj で実行しています...
  c:\work\Ecbeing.Sample.Migration\Ecbeing.Sample.Migration.csproj のパッケージを復元しています...
  MSBuild ファイル c:\work\Ecbeing.Sample.Migration\obj\Ecbeing.Sample.Migration.csproj.nuget.g.props を生成しています。
  MSBuild ファイル c:\work\Ecbeing.Sample.Migration\obj\Ecbeing.Sample.Migration.csproj.nuget.g.targets を生成しています。
  c:\work\Ecbeing.Sample.Migration\Ecbeing.Sample.Migration.csproj の復元が 4.71 sec で完了しました。

正常に復元されました。
ハマりポイント1:プロジェクト名を Migration にすると後々詰む

早速ですが、プロジェクト名を Migration にしてしまうと、後々自動生成されるマイグレーションファイルで名前空間とクラス名が被ってしまい、マイグレーションが実行できなくなります…

c:\work\Ecbeing.Sample.Migration>dotnet ef -v database update
(中略)
Migrations\20190606092023_CreateKanazawa.cs(6,43): error CS0118: 'Migration' は 名前空間 ですが、種類 のように使用されています。 [c:\work\Ecbeing.Sample.Migration\Ecbeing.Sample.Migration.csproj]
Migrations\20190606092023_CreateKanazawa.cs(8,33): error CS0115: 'CreateKanazawa.Up(MigrationBuilder)': オーバーライドする適切なメソッドが見つかりませんでした。 [c:\work\Ecbeing.Sample.Migration\Ecbeing.Sample.Migration.csproj]
Migrations\20190606092023_CreateKanazawa.cs(24,33): error CS0115: 'CreateKanazawa.Down(MigrationBuilder)': オーバーライ ドする適切なメソッドが見つかりませんでした。 [c:\work\Ecbeing.Sample.Migration\Ecbeing.Sample.Migration.csproj]
Migrations\20190606092023_CreateKanazawa.Designer.cs(15,33): error CS0115: 'CreateKanazawa.BuildTargetModel(ModelBuilder)': オーバーライドする適切なメソッドが見つかりませんでした。 [c:\work\Ecbeing.Sample.Migration\Ecbeing.Sample.Migration.csproj]

ビルドに失敗しました。

f:id:ecb_kkanazawa:20190606182352p:plain
Migrationクラスが名前空間だと認識されている…
Migration クラスをフルパスで指定すれば一応回避はできるのですが、自動生成されたファイルを毎回修正するのもナンセンスなので、違うプロジェクト名にした方が良いです。


…というわけで、プロジェクト名を
Ecbeing.Sample.DbMigration に変えてやり直します。

プログラムファイルの新規作成・改修

Sample スキーマに Kanazawa というテーブルを作ってみます。
ファイル構成はこんな感じにします。

ファイル 新規/改修 説明
/ - ルートフォルダ
├ Program.cs - 触らない
├ Startup.cs 改修 ConfigureServices() にDB接続処理を追加
├ Entities/ 新規 エンティティを格納するフォルダ
 └ Kanazawa.cs 新規 Kanazawaテーブルのエンティティ
└ DbContexts/ 新規 DbContextクラスを格納するフォルダ
 └ SampleDbContext.cs 新規 SampleスキーマのDbContext
Entities/Kanazawa.cs

テーブルのカラムをメンバー変数として定義していきます。
単一主キーや not null 制約、文字列長はアノテーションで指定できます。
複合主キーや default 値は自分でアノテーションを作らないとできません…

using System;
using System.ComponentModel.DataAnnotations;

namespace Ecbeing.Sample.DbMigration.Entities
{
    public class Kanazawa
    {
        [Key]
        public Guid Id { get; set; }
        public int NumberValue { get; set; }
        [Required]
        [StringLength(100)]
        public string TextValue { get; set; }
    }
}
DbContexts/SampleDbContext.cs

作成するテーブルをメンバー変数として定義していきます。

using Microsoft.EntityFrameworkCore;
using Ecbeing.Sample.DbMigration.Entities;

namespace Ecbeing.Sample.DbMigration.DbContexts
{
    public class SampleDbContext : DbContext
    {
        public SampleDbContext(DbContextOptions<SampleDbContext> options)
            : base(options) { }

        public DbSet<Kanazawa> Kanazawa { get; set; }
    }
}
Startup.cs

ConfigureServices() に、DB の接続処理を追加します。
(行頭に + が付いている行が追加分です)
定義を appsettings.json に持っていけると更に良い感じです。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.EntityFrameworkCore;
+ using Ecbeing.Sample.DbMigration.DbContexts;

namespace Ecbeing.Sample.DbMigration
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
+             services.AddDbContext<SampleDbContext>(options =>
+             {
+                 options.UseMySQL(@"userid=[ユーザーID];pwd=[パスワード];database=[スキーマ名];server=[サーバ名];");
+             });
        }
(後略)
ハマりポイント2:標準だとSQLServerにしか接続できない

MySQLに繋ぎたかったんですが、メソッドが出てきませんでした…

f:id:ecb_kkanazawa:20190606191915p:plain
MySQLが無い…
SQLServer以外のDBに繋ぐ場合は自分でパッケージを入れる必要があります。
提供されているパッケージの一覧は↓で確認できます。
https://docs.microsoft.com/ja-jp/ef/core/providers/


今回は MySql.Data.EntityFrameworkCore を入れてみます。
Oracleさんがメンテしてくれているようなので安心!


パッケージのインストールには dotnet add package コマンドを使います。
先ほど作ったプロジェクトのフォルダに cd して実行します。

c:\work>cd Ecbeing.Sample.DbMigration

c:\work\Ecbeing.Sample.DbMigration>dotnet add package MySql.Data.EntityFrameworkCore
  Writing C:\Users\kkanazawa\AppData\Local\Temp\tmp8192.tmp
info : パッケージ 'MySql.Data.EntityFrameworkCore' の PackageReference をプロジェクト 'c:\work\Ecbeing.Sample.DbMigration\Ecbeing.Sample.DbMigration.csproj' に追加しています。
log  : c:\work\Ecbeing.Sample.DbMigration\Ecbeing.Sample.DbMigration.csproj のパッケージを復元しています...
info :   GET https://api.nuget.org/v3-flatcontainer/mysql.data.entityframeworkcore/index.json
info :   OK https://api.nuget.org/v3-flatcontainer/mysql.data.entityframeworkcore/index.json 604 ミリ秒
info : パッケージ 'MySql.Data.EntityFrameworkCore' は、プロジェクト 'c:\work\Ecbeing.Sample.DbMigration\Ecbeing.Sample.DbMigration.csproj' のすべての指定されたフレームワークとの互換性があります。
info : ファイル 'c:\work\Ecbeing.Sample.DbMigration\Ecbeing.Sample.DbMigration.csproj' に追加されたパッケージ 'MySql.Data.EntityFrameworkCore' バージョン '8.0.16' の PackageReference。
info : 復元をコミットしています...
log  : MSBuild ファイル c:\work\Ecbeing.Sample.DbMigration\obj\Ecbeing.Sample.DbMigration.csproj.nuget.g.props を生成し ています。
info : ロック ファイルをディスクに書き込んでいます。パス: c:\work\Ecbeing.Sample.DbMigration\obj\project.assets.json
log  : c:\work\Ecbeing.Sample.DbMigration\Ecbeing.Sample.DbMigration.csproj の復元が 2.66 sec で完了しました。


これで、UseMySQL() が使えるようになります。

f:id:ecb_kkanazawa:20190606192640p:plain
MySQLが出るようになりました!

マイグレーションファイル作成

dotnet ef migrations addコマンドでマイグレーションファイルを作成します。

c:\work\Ecbeing.Sample.DbMigration>dotnet ef migrations add CreateKanazawa
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 2.2.2-servicing-10034 initialized 'SampleDbContext' using provider 'MySql.Data.EntityFrameworkCore' with options: None
Done. To undo this action, use 'ef migrations remove'


コマンドが正常終了すると、Migrations フォルダの中にマイグレーションファイルが自動生成されます。

f:id:ecb_kkanazawa:20190606194254p:plain
ファイル名に日付+時分秒のプレフィックスが付くのは良いんだけど、UTCなんですね…

マイグレーション実行

dotnet ef database updateコマンドでマイグレーションを実行します!

c:\work\Ecbeing.Sample.DbMigration>dotnet ef database update
(中略)
MySql.Data.MySqlClient.MySqlException (0x80004005): Table 'sample.__EFMigrationsHistory' doesn't exist
   at MySql.Data.MySqlClient.MySqlStream.ReadPacket()
   at MySql.Data.MySqlClient.NativeDriver.GetResult(Int32& affectedRow, Int64& insertedId)
   at MySql.Data.MySqlClient.Driver.NextResult(Int32 statementId, Boolean force)
   at MySql.Data.MySqlClient.MySqlDataReader.NextResult()
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
   at System.Data.Common.DbCommand.ExecuteReader()
   at Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommand.Execute(IRelationalConnection connection, DbCommandMethod executeMethod, IReadOnlyDictionary`2 parameterValues)
Failed executing DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `MigrationId`, `ProductVersion`
FROM `__EFMigrationsHistory`
ORDER BY `MigrationId`;

(中略)
Table 'sample.__EFMigrationsHistory' doesn't exist

お…おお…?

ハマりポイント3:マイグレーションテーブルを自動作成してくれない

どうやら、MySql.Data.EntityFrameworkCore では __EFMigrationsHistory テーブルを自動生成してくれないようです。
(ちなみに Pomelo.EntityFrameworkCore.MySql では作ってくれます)
Oracleさぁん…(´;ω;`)


解決策は簡単、自分で作ればOKです。

CREATE TABLE `__EFMigrationsHistory` ( `MigrationId` nvarchar(150) NOT NULL, `ProductVersion` nvarchar(32) NOT NULL, PRIMARY KEY (`MigrationId`) );


再度 database update を叩いたら正常終了しました。

c:\work\Ecbeing.Sample.DbMigration>dotnet ef database update
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 2.2.2-servicing-10034 initialized 'SampleDbContext' using provider 'MySql.Data.EntityFrameworkCore' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1 FROM information_schema.tables
      WHERE table_name = '
      __EFMigrationsHistory' AND table_schema = DATABASE()
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1 FROM information_schema.tables
      WHERE table_name = '
      __EFMigrationsHistory' AND table_schema = DATABASE()
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT `MigrationId`, `ProductVersion`
      FROM `__EFMigrationsHistory`
      ORDER BY `MigrationId`;
info: Microsoft.EntityFrameworkCore.Migrations[20402]
      Applying migration '20190606110411_CreateKanazawa'.
Applying migration '20190606110411_CreateKanazawa'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (43ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE `Kanazawa` (
          `Id` varbinary(16) NOT NULL,
          `NumberValue` int NOT NULL,
          `TextValue` varchar(100) NOT NULL,
          PRIMARY KEY (`Id`)
      );
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (12ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
      VALUES ('20190606110411_CreateKanazawa', '2.2.2-servicing-10034');
Done.

`Id` varbinary(16) NOT NULL,

あれ…

ハマりポイント4:Guid型を変換してくれない

解決策としては、char(36) でカラムが作成されるようにアノテーションを入れます。

using System;
using System.ComponentModel.DataAnnotations;
+ using System.ComponentModel.DataAnnotations.Schema;

namespace Ecbeing.Sample.DbMigration.Entities
{
    public class Kanazawa
    {
        [Key]
+       [Column(TypeName ="char(36)")]
        public Guid Id { get; set; }
        public int NumberValue { get; set; }
        [Required]
        [StringLength(100)]
        public string TextValue { get; set; }
    }
}


これで何とかテーブルができました。
f:id:ecb_kkanazawa:20190606205700p:plain

既存のテーブルをマイグレーションに移行するには…?

Kizonテーブルがあったとして、これにカラムを追加したくなったとします。
f:id:ecb_kkanazawa:20190606212606p:plain


もちろんそのままエンティティクラスを書いてしまうとCREATE文が生成されてしまうので、
NotMapped アノテーションで上手くできないかなぁと思いました。

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Ecbeing.Sample.DbMigration.Entities
{
    public class Kizon
    {
        [NotMapped]
        [Key]
        [Column(TypeName ="char(36)")]
        public Guid Id { get; set; }
        [NotMapped]
        public DateTime DateTimeValue { get; set; }

        public String TextValue { get; set; }
    }
}
ハマりポイント5:主キーが無いとマイグレーションできない
c:\work\Ecbeing.Sample.DbMigration>dotnet ef migrations add AlterKizon
System.InvalidOperationException: The entity type 'Kizon' requires a primary key to be defined.

あぁ…そうなんですね…
まあでも、結局CREATE文が作られてダメな気がします。


ちなみに公式ドキュメントでは、マイグレーションファイルを作った後に手で直しなさいとなっています。
それ、今から全テーブルにやるのかぁ…
移行 - EF Core | Microsoft Docs

あれ?

ASP.NET Core - 既存のデータベース - EF Core の概要 | Microsoft Docs
既存のデータベースっていうページがあるぞ!
もしかしてこれでリバースエンジニアリングしたらうまく行くんじゃない?




次回、.NET Core で DBマイグレーションを試したら色々ハマった話(解決編) へ続く(?)





~ecbeingでは、一緒に色々悩んでくれるエンジニアを募集しています~
www.softcreate-holdings.co.jp