こんにちは。ecbeing金澤です。
最近暑くなってきて、フラペチーノの季節だなぁと思います。このシトラス果肉追加+バレンシアシロップ追加は神の味でした。
シトラス果肉をプリンの上に追加するか底に追加するか選べるので、ぜひ底に追加してください(ひとくち目からフルーティーに!)
はじめに
今のプロジェクトは初期開発が終わり、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] ビルドに失敗しました。
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に繋ぎたかったんですが、メソッドが出てきませんでした…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() が使えるようになります。
マイグレーションファイル作成
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 フォルダの中にマイグレーションファイルが自動生成されます。
マイグレーション実行
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; } } }
これで何とかテーブルができました。
既存のテーブルをマイグレーションに移行するには…?
Kizonテーブルがあったとして、これにカラムを追加したくなったとします。
もちろんそのままエンティティクラスを書いてしまうと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