この記事は
SQL Server (Docker版)をデータベースファーストで操作してみた記事の続き
前回は、既存のデータベースからモデル(DbContext
を継承したクラス)を生成する、いわゆるスキャフォールディングを試した
(スキャフォールディングについて書かれた、マイクロソフトのよくわかる記事)
今回はモデルからデータベースを生成する方法、いわゆるコードファーストな方法を試す
EF Core で何ができるか
公式ドキュメントから一部引用させていただく
EF は、次のモデル開発アプローチをサポートしています。
既存のデータベースからモデルを生成する。
データベースに合わせてモデルのコードを手動で書く。
モデルが作成されたら、EF の移行 を使用して、モデルからデータベースを作成します。 移行により、モデルの変更に応じてデータベースを進化させることができます。
引用元:https://docs.microsoft.com/ja-jp/ef/core/#the-model
EF Core 個人的ロードマップ
- 「既存のデータベースから~」は前回やった
- 今回はモデルのコードを手動で書いて、そこから移行(Migration)を使ってデータベースをつくるところまでをでやりたい
- 次回以降があれば、データベースを進化させるを試したい
- 移行によりDBがどう変化していくか など
ソリューションにライブラリプロジェクトを追加する
さっそく作業を始める
まず最初に、EF Core ドキュメントを参考にして、ライブラリプロジェクトIntro
をつくる
今回は、主にこのプロジェクトでの作業となる
スタートアッププロジェクトは、前回記事からの続きでWPF_EFCore
をつかう
(※ 前回記事ではプロジェクト名まで言及してないかも・・・)
# プロジェクト作成
dotnet new classlib --framework "netcoreapp3.1" -o Intro
# ライブラリプロジェクトをスタートアッププロジェクトの参照に追加
dotnet sln add .\Intro\Intro.csproj
dotnet add .\WPF_EFCore\WPF_EFCore.csproj reference .\Intro\Intro.csproj
# ライブラリプロジェクトに必要なパッケージをインストール
dotnet add .\Intro\Intro.csproj package Microsoft.EntityFrameworkCore
dotnet add .\Intro\Intro.csproj package Microsoft.EntityFrameworkCore.SqlServer
プロジェクトにインストールしたパッケージを確認する
WPF_EFCore
およびIntro
のどちらも .NET Core 3.1 であり、EF Core 5系 が入っている
dotnet list package
# プロジェクト 'WPF_EFCore' に次のパッケージ参照が含まれています
# [netcoreapp3.1]:
# 最上位レベル パッケージ 要求済み 解決済み
# > Microsoft.EntityFrameworkCore.Sqlite 5.0.9 5.0.9
# > Microsoft.EntityFrameworkCore.SqlServer 5.0.9 5.0.9
# > Microsoft.EntityFrameworkCore.Tools 5.0.9 5.0.9
# > Prism.Unity 8.0.0.1909 8.0.0.1909
#
# プロジェクト 'Intro' に次のパッケージ参照が含まれています
# [netcoreapp3.1]:
# 最上位レベル パッケージ 要求済み 解決済み
# > Microsoft.EntityFrameworkCore 5.0.9 5.0.9
# > Microsoft.EntityFrameworkCore.SqlServer 5.0.9 5.0.9
なぜ .NET5 ではないのか?
Prism Template Pack から作った空のプロジェクトのデフォルトが .NET Core 3.1 であり
そこから .NET 5 に変更するのを忘れたため
Intro
プロジェクトにモデルを追加する
公式ドキュメントのモデルを参考にして、Intro
プロジェクトにクラスを3つ追加する
接続文字列を自分の環境にあわせる
BloggingContext
のOnConfiguring
メソッドを一部変更する
SQL Server DockerコンテナのBlogging
データベースに接続する
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
+ "Data Source=\"localhost, 11433\";Initial Catalog=Blogging;User ID=sa;Password=SqlPass1234");
- @"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True");
}
接続文字列がベタ書きであることはいけない気がするも、ベストプラクティスが分からない&テストのため、こう書いている
SQLサーバーをたてる
コードファーストで書いたモデルをDBに移行するために
移行先となる Docker コンテナの SQL サーバーを起動する
# コンテナを起動する
docker start sql1
sql1
については、関連する過去の記事を参照
EF Core ツールのコマンドを使って移行する
コードファーストでSQLサーバーに新しいDBをつくっていく
具体的には、Visual Studio のパッケージマネージャーコンソールから、EF Core ツールのコマンドを打っていく作業となる
移行というのが、モデルからDBを作成、または変更していくことを表している
前準備
リファレンスを準備して、共通パラメータを確認する
EF Core コマンドツールのドキュメント
コマンドは10個くらいあるらしい
今回は移行に関するものだけを使う
共通パラメータ
共通パラメータである-Project
を省略すると
パッケージマネージャーコンソール の 既定のプロジェクト がターゲットプロジェクトとして使用されます
とのこと。
毎回プルダウンから既定のプロジェクトを選びなおしてもいいし
あるいは毎回-Project
を指定してもいい
また、-V
or -Verbose
パラメータをつけると、実行中の詳細情報を見ることができる
コマンドからエラーが返ってきたときには、エラーの原因を探るために-V
をつけて再度コマンドを打つ
〈本題〉モデルを移行する
3つのコマンドを順番に打って、モデルをDBに移行する
-
Get-DbContext
・・・使用可能なDbContext
を取得する -
Add-Migration
・・・移行を追加する -
Update-Database
・・・移行をデータベースに反映する
2つ目の「移行を追加する」のイメージが難しいが
モデルの変更履歴をセーブポイントとしてローカルに残していくイメージに近いと思う
セーブポイントを実際にDBへ反映させるのが、3番目のアップデートになる
Get-DbContext
使用可能なDbContext
を取得する
Get-DbContext -Project Intro -StartupProject WPF_EFCore
# Build started...
# Build succeeded.
# MyDockerDBContext
# BloggingContext
スタートアッププロジェクトのMyDockerDBContext
と、新規追加したプロジェクトのBloggingContext
の2つのコンテキストが表示された
**今回使うのは、ライブラリプロジェクトに追加したBloggingContext
**となる
以後、-Context
パラメータをつかってBloggingContext
を毎回指定する
(指定しなかった場合の挙動について、次節で言及する)
〈参考〉Get-DbContext
のパラメータを未指定としたときの動作
パラメータを省略すると、スタートアッププロジェクトにあるMyDockerDBContext
だけが表示される
スタートアッププロジェクトから参照されるライブラリプロジェクトのBloggingContext
は表示されない
Get-DbContext
# Build started...
# Build succeeded.
# MyDockerDBContext
Add-Migration
移行を追加する
- 最初の移行は、DBの新規作成に相当する
- その次からの移行は、1つ前の移行からの差分が記録される
Add-Migration -Name Init -Context BloggingContext -Project Intro -StartupProject WPF_EFCore
# Build started...
# Build succeeded.
# To undo this action, use Remove-Migration.
```
`Intro`プロジェクトに新しく`Migrations`フォルダが生成される
以後、`Add-Migration`をするたびに、移行履歴が記録されていく
移行の挙動については、また別の記事で試す予定
### `DbContext`が複数ある場合は`-Context`を省略してはいけない
`Get-DbContext`で確認したときに`DbContext`が2個あったので
`-Context`パラメータで使うコンテキストを指定しないと、エラーが発生する
```Powershell:パッケージマネージャーコンソール
# -Context 引数を省略した
Add-Migration -Name Init -Project Intro -StartupProject WPF_EFCore
```
> More than one DbContext was found. Specify which one to use.
> Use the '-Context' parameter for PowerShell commands and the '--context' parameter for dotnet commands.
〈意訳〉 2つ以上のDbContextが見つかったよ。どっちを使うか決めてね。Powershellをつかっている人は`-Context`パラメータを使うといいよ。
## `Update-Database`
移行をデータベースに反映する
何も指定しなければ、最後の移行が反映される
```Powershell:パッケージマネージャーコンソール
Update-Database -Project Intro -Context BloggingContext
# Build started...
# Build succeeded.
# Applying migration '202108********_Init'.
```
`Add-Migration`を繰り返すと移行先が複数になる
移行先を選択したい場合は、`-Migration`で指定する
```Powershell:パッケージマネージャーコンソール
Update-Database -Project Intro -Context BloggingContext -Migration Init -StartupProject WPF_EFCore
# Build started...
# Build succeeded.
# No migrations were applied. The database is already up to date.
# Done.
```
すでに移行が完了しているので、「DBは`up to date`されている」と表示された
# データベースを確認する
ここまでの変更がDBに反映されていることを確認する
## SQL Server (Dokcerコンテナ)に入る
移行作業の前に`sql1`(Docker版SQLサーバー)をスタートさせているので、その中に入る
```Powershell:Powershell
# コンテナに入る
docker exec -it sql1 "bash"
```
## SQL Serverにログインする
`localhost`にユーザID`sa`でログインする
パスワードを聞かれるので入力する
```bash:bash
# DBサーバーにログインする。
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -W
```
`-W`オプションをつけると、画面への出力が整形されてイイ感じになる([参考にさせていただいた記事](https://qiita.com/Topiman/items/cd213f172af4733240a5#transact-sql%E3%82%AF%E3%82%A8%E3%83%AA%E3%81%AE%E5%AE%9F%E8%A1%8C))
## DB一覧を取得する
既存のDBに加えて、`Blogging`データベースが新たに追加されている
```diff_sql:sqlcmd
1> SELECT name FROM sys.databases;
2> GO
name
----
master
tempdb
model
msdb
+ Blogging
```
## 作業するデータベースを`Blogging`にする
作業するデータベースを`USE`で指定する
使うのはもちろん`Blogging`データベース
```sql:sqpcmd
1> USE Blogging;
2> GO
Changed database context to 'Blogging'.
```
今いるデータベースの名前を取得すると、`Blogging`と表示される
```sql:sqpcmd
1> SELECT DB_NAME();
2> GO
-
Blogging
```
ちなみに自分の環境では、デフォルトは`master`だった
## テーブル一覧を取得する
`TYPE = 'U'`のテーブル一覧を取得する
`TYPE`を指定しないと、たくさんのテーブルが表示される
```sql:sqpcmd
1> SELECT * FROM sys.objects WHERE TYPE = 'U';
2> GO
name object_id principal_id schema_id parent_object_id type type_desc create_date modify_date is_ms_shipped is_published is_schema_published
---- --------- ------------ --------- ---------------- ---- --------- ----------- ----------- ------------- ------------ -------------------
__EFMigrationsHistory 885578193 NULL 1 0 U USER_TABLE ...
Blogs 917578307 NULL 1 0 U USER_TABLE ...
Posts 949578421 NULL 1 0 U USER_TABLE ...
```
3つのテーブルが表示された
`Blogs`テーブルと`Posts`テーブルは
移行した際にコンテキストに含まれていた`DbSet<Blog> Blogs`と`DbSet<Post> Posts`である
`__EFMigrationsHistory`は、その名からして、移行履歴を保持しているテーブルらしい
### `__EFMigrationsHistory`の中を覗いてみる
```sql
1> SELECT * FROM __EFMigrationsHistory;
2> GO
MigrationId ProductVersion
----------- --------------
202108********_Init 5.0.9
(1 rows affected)
```
`MigrationId`と`ProductVersion`の2つのフィールドが存在する
`MigrationId`は`Add-Migration`したときに生成された`.cs`ファイルの名前と一致しており
一方の`ProductVersion`は EF Core のバージョンと一致している
## テーブルのフィールドを確認する
```sql
1> SELECT * FROM Blogs;
2> GO
BlogId Url Rating
------ --- ------
```
`Blogs`テーブルには、モデルで指定した3つのフィールドが存在する
`List<Post>`の`Posts`フィールドは存在しない
```sql
1> SELECT * FROM Posts;
2> GO
PostId Title Content BlogId
------ ----- ------- ------
```
`Posts`テーブルには、モデルで指定した4つのフィールドが存在する
`Blog`型の`Blog`フィールドは存在しない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/53862ca8-d68c-9d6c-1066-7931fd04d1e5.png)
いずれれも`string`や`int`などのプリミティブ型のみがテーブルのフィールドに反映されて
`List`や`Blog`などの集約関係や実態などはテーブルには反映されていないことがわかる
<details><summary>マイグレーションしたときのコード</summary><div>
```cs:Initクラスから一部抜粋
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Blogs",
columns: table => new
{
BlogId = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Url = table.Column<string>(type: "nvarchar(max)", nullable: true),
Rating = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Blogs", x => x.BlogId);
});
migrationBuilder.CreateTable(
name: "Posts",
columns: table => new
{
PostId = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
Content = table.Column<string>(type: "nvarchar(max)", nullable: true),
BlogId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Posts", x => x.PostId);
table.ForeignKey(
name: "FK_Posts_Blogs_BlogId",
column: x => x.BlogId,
principalTable: "Blogs",
principalColumn: "BlogId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Posts_BlogId",
table: "Posts",
column: "BlogId");
}
```
</div></details>
# おわりに
今回は、モデルからDBを作成するとこまでを試した
次は、実際にデータを入れて、さらにテーブルの構造を変えてみたりして
EF Core & SQL Serverの挙動を理解したい
# SQL Serverの操作に関して、参考にさせていただいた記事
https://qiita.com/zaburo/items/6edf7c05c5d4f5e039eb
https://qiita.com/qsuke92/items/51a85a58ac91782ee528
https://qiita.com/YamaDash82/items/b459ec502d8390708f45
# 関連する過去の記事
https://qiita.com/t13801206/items/4c2e85fc2732c4de5533
https://zenn.dev/t13801206/articles/2106-wpf-ef-study