【C#】Entity Framework Core で移行したあと、sqlcmdを使ってSQL Serverを観察する

# この記事は

[SQL Server (Docker版)をデータベースファーストで操作してみた記事](https://qiita.com/t13801206/items/4c2e85fc2732c4de5533)の続き

前回は、**既存のデータベースからモデル（`DbContext`を継承したクラス）を生成する**、いわゆるスキャフォールディングを試した
（[スキャフォールディングについて書かれた、マイクロソフトのよくわかる記事](https://docs.microsoft.com/ja-jp/ef/core/managing-schemas/scaffolding)）

今回は**モデルからデータベースを生成する**方法、いわゆるコードファーストな方法を試す

## EF Core で何ができるか

公式ドキュメントから一部引用させていただく

> EF は、次のモデル開発アプローチをサポートしています。

> 既存のデータベースからモデルを生成する。
> データベースに合わせてモデルのコードを手動で書く。
> モデルが作成されたら、EF の移行 を使用して、モデルからデータベースを作成します。 移行により、モデルの変更に応じてデータベースを進化させることができます。

引用元：https://docs.microsoft.com/ja-jp/ef/core/#the-model

### EF Core 個人的ロードマップ

- 「既存のデータベースから～」は前回やった
- 今回は**モデルのコードを手動で書いて、そこから移行(Migration)を使ってデータベースをつくる**ところまでをでやりたい
- 次回以降があれば、データベースを進化させるを試したい
    - 移行によりDBがどう変化していくか　など

# ソリューションにライブラリプロジェクトを追加する

さっそく作業を始める

まず最初に、EF Core ドキュメントを参考にして、ライブラリプロジェクト`Intro`をつくる
今回は、主にこのプロジェクトでの作業となる

スタートアッププロジェクトは、前回記事からの続きで`WPF_EFCore`をつかう
（※ 前回記事ではプロジェクト名まで言及してないかも・・・）

```Powershell:開発者用パワーシェル
# プロジェクト作成
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系 が入っている

```Powershell:開発者用パワーシェル
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](https://marketplace.visualstudio.com/items?itemName=BrianLagunas.PrismTemplatePack) から作った空のプロジェクトのデフォルトが .NET Core 3.1 であり
そこから .NET 5 に変更するのを忘れたため


# `Intro`プロジェクトにモデルを追加する

[公式ドキュメントのモデル](https://docs.microsoft.com/ja-jp/ef/core/#the-model)を参考にして、`Intro`プロジェクトにクラスを3つ追加する

```plantuml
package EFCore
{
    class DbContext   
}

package Intro
{
    class BloggingContext extends DbContext
    {
        + DbSet<Blog> Blogs
        + public DbSet<Post> Posts

        # override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    }

    class Blog
    {
        + int BlogId
        + string Url
        + int Rating
        + List<Post> Posts
    }

    class Post
    {
        + int PostId
        + string Title
        + string Content

        + int BlogId
        + Blog Blog
    }

Blog o-- Post
BloggingContext -> Blog
BloggingContext -> Post
}
```

## 接続文字列を自分の環境にあわせる

`BloggingContext`の`OnConfiguring`メソッドを一部変更する

[SQL Server Dockerコンテナ](https://hub.docker.com/_/microsoft-mssql-server)の`Blogging`データベースに接続する

```diff_c_sharp:BloggingContext
 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");
 }
```

:::note warn
接続文字列がベタ書きであることはいけない気がするも、ベストプラクティスが分からない＆テストのため、こう書いている
:::

# SQLサーバーをたてる

コードファーストで書いたモデルをDBに移行するために
移行先となる Docker コンテナの SQL サーバーを起動する

```Powershell:Powershell
# コンテナを起動する
docker start sql1
```

`sql1`については、[関連する過去の記事](#関連する過去の記事)を参照

# EF Core ツールのコマンドを使って移行する

コードファーストでSQLサーバーに新しいDBをつくっていく

具体的には、Visual Studio のパッケージマネージャーコンソールから、EF Core ツールのコマンドを打っていく作業となる

移行というのが、モデルからDBを作成、または変更していくことを表している

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/a41e7804-3188-57fa-e75c-d7a367c70514.png)

## 前準備

リファレンスを準備して、共通パラメータを確認する

### EF Core コマンドツールのドキュメント

コマンドは10個くらいあるらしい
今回は移行に関するものだけを使う

https://docs.microsoft.com/ja-jp/ef/core/cli/powershell

### 共通パラメータ

[共通パラメータ](https://docs.microsoft.com/ja-jp/ef/core/cli/powershell#common-parameters)である`-Project `を省略すると

> パッケージマネージャーコンソール の 既定のプロジェクト がターゲットプロジェクトとして使用されます

とのこと。

毎回プルダウンから既定のプロジェクトを選びなおしてもいいし
あるいは毎回`-Project`を指定してもいい

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/4189a957-af95-8a52-6f47-a3935cfd56a9.png)

また、`-V` or `-Verbose` パラメータをつけると、実行中の詳細情報を見ることができる
コマンドからエラーが返ってきたときには、エラーの原因を探るために`-V`をつけて再度コマンドを打つ

## 〈本題〉モデルを移行する

3つのコマンドを順番に打って、モデルをDBに移行する

1. `Get-DbContext` ・・・使用可能な`DbContext`を取得する
1. `Add-Migration` ・・・移行を追加する
1. `Update-Database` ・・・移行をデータベースに反映する

2つ目の「移行を追加する」のイメージが難しいが
モデルの変更履歴をセーブポイントとしてローカルに残していくイメージに近いと思う

セーブポイントを実際にDBへ反映させるのが、3番目のアップデートになる

## `Get-DbContext`

使用可能な`DbContext`を取得する

```Powershell:パッケージマネージャーコンソール
Get-DbContext -Project Intro -StartupProject WPF_EFCore
# Build started...
# Build succeeded.

# MyDockerDBContext
# BloggingContext  
```

スタートアッププロジェクトの`MyDockerDBContext`と、新規追加したプロジェクトの`BloggingContext`の2つのコンテキストが表示された

**今回使うのは、ライブラリプロジェクトに追加した`BloggingContext`**となる
以後、`-Context`パラメータをつかって`BloggingContext`を毎回指定する
（指定しなかった場合の挙動について、次節で言及する）

### 〈参考〉`Get-DbContext`のパラメータを未指定としたときの動作

パラメータを省略すると、スタートアッププロジェクトにある`MyDockerDBContext`だけが表示される
スタートアッププロジェクトから参照されるライブラリプロジェクトの`BloggingContext`は表示されない

```Powershell:パッケージマネージャーコンソール
Get-DbContext
# Build started...
# Build succeeded.

# MyDockerDBContext
```

## `Add-Migration`

移行を追加する

- 最初の移行は、DBの新規作成に相当する
- その次からの移行は、1つ前の移行からの差分が記録される

``````Powershell:パッケージマネージャーコンソール
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