【C#】コードファーストで DB に 初期データ入れる

ASP.NET Core と Entity Framework Core を学習しました。
エンティティから DB を生成する方法と初期データの投入についてまとめました。
**簡略化のため、必要な `using 名前空間;` や `namespace {...}` はコードから省略しています。**

## エンティティとデータベースコンテキストの作成

```c#:Models/Book.cs
public class Book
{
    public int ID { get; set; }
    [Required]
    [StringLength(100)]
    public string Title { get; set; }
    [StringLength(100)]
    public string Author { get; set; }
    [Range(1900, 2100)]
    public int PublishYear { get; set; }
}
```

- 大雑把なイメージ
    - `class`：DB のレコード
    - `property`：DB のカラム
- プロパティの上の `[...]`
    - モデルの検証や DB の制約を C# のアノテーションとして記述できる。
    - 詳しくは：[ASP.NET Core MVC でのモデル検証の概要 - Microsoft Docs](https://docs.microsoft.com/ja-jp/aspnet/core/mvc/models/validation)
- `ID`
    - Entity Framework が自動的に主キーと認識する。
    - 連番で採番される。
    - 外部キーなど自分で採番したいときは以下のアノテーションをつけます。
        - `[DatabaseGenerated(DatabaseGeneratedOption.None)]`：自動連番をOFF
        - `[Key]`：キーとして認識

```c#:Data/BooksContext.cs
public class BooksContext : DbContext
{
    public BooksContext(DbContextOptions<BooksContext> options)
        : base(options)
    { }

    public DbSet<Book> Books { get; set; }
}
```

- Entity Framework ではこのクラスを経由して LINQ to Entities で DB にアクセスする。
- `DbContext`
    - コンテキストクラスは `Microsoft.EntityFrameworkCore.DbContext` から派生する。
- `DbSet<TEntity>`
    - エンティティのコレクションを表す。テーブルにあたる。
    - コレクションだからか名前は複数形にするのが一般的なよう。

## `Startup.cs` の `ConfigureServices` を設定

```c#:Startup.cs
public void ConfigureServices(IServiceCollection services) => services
    .AddDbContext<BooksContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString(nameof(BooksContext))))
    .AddMvc();
```

- fluent interfaces に興味があったのでメソッドチェーン記述。
    - サービス登録するメソッドは `this` を返す。（違うのもあるのでしょうか？）
- `services.AddDbContext<TContext>(...)`
    - ASP.NET Core の DI コンテナーに型引数： `BooksContext` を登録する。
    - これで `BooksContext` を使うクラスのコンストラクタに `BooksContext` のインスタンスが渡される。
- `options.UseSqlServer`
    - DB には SQL Server を使用する。
    - `Configuration.GetConnectionString`
        - `appsettings.json` から引数名の接続文字列を取得。

## `appsettings.json` に接続文字列を設定する

```json:appsettings.json
{
  ...略
  "ConnectionStrings": {
    "BooksContext": "Server=(localdb)\\mssqllocaldb;Database=BooksContext-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

- `Server=(localdb)\\mssqllocaldb;`
    - SQL Server LocalDB を使用する。
    - 名前の通りローカル開発環境で動作する簡易 DB。
- `Database=<schema_name>`
    - DB （スキーマ）名。今回はコンテキストクラスと同じ `BooksContext-1`
- `Trusted_Connection=True;`
    - 信頼された接続。ユーザー名属性とパスワード属性を接続文字列から省ける。
- `MultipleActiveResultSets=true;`
    - １度の DB 接続で複数のクエリを投げることを許可する。

## マイグレーションファイルと DB の生成

```shell:パッケージマネージャーコンソール
    # マイグレーションファイルを作成
PM> Add-Migration Initial
    # DB更新または生成
PM> Update-Database
```

- `Add-Migration [引数]`
    - Migrations フォルダとマイグレーションファイルが作られる。
        - `20180407053355_Initial.cs`
            - `20180407053355_Initial.Designer.cs` 
        - `BooksContextModelSnapshot.cs`
    - ![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/829708ce-110d-f151-3336-69e4f2afd425.png)
- `Update-Database`
    - マイグレーションファイルの `Up` メソッドの内容で DB を更新（作成）する。
    - ![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/dc2b9bca-67df-15c5-8777-08c2d3719227.png)

## DB 初期データ投入用のクラス作成とその呼び出し

```c#:Data/DbInitializer.cs
public class DbInitializer
{
    public static async Task SeedingAsync(BooksContext context)
    {
        await context.Database.EnsureCreatedAsync();
        if (await context.Books.AnyAsync())
            return;
        await context.Books.AddRangeAsync(
            new Book { Title = "アンドロイドは電気羊の夢を見るか? ", Author = "フィリップ・K・ディック", PublishYear = 1968 },
            new Book { Title = "1984年", Author = "ジョージ・オーウェル", PublishYear = 1949 },
            new Book { Title = "幼年期の終り", Author = "アーサー・C・クラーク", PublishYear = 1953 },
            new Book { Title = "アルジャーノンに花束を", Author = "ダニエル・キイス", PublishYear = 1959 },
            new Book { Title = "月は無慈悲な夜の女王", Author = "ロバート・A・ハイライン", PublishYear = 1966 },
            new Book { Title = "われはロボット", Author = "アイザック・アシモフ", PublishYear = 1950 },
            new Book { Title = "虐殺器官", Author = "伊藤計劃", PublishYear = 2007 });
        await context.SaveChangesAsync();
    }
}
```

- `Database.EnsureCreatedAsync`
    - データベースにテーブルがない場合は作成する。
    - ※ただし、マイグレーションファイルは使用されない
- `Books.AnyAsync`
    - すでにデータが存在する場合には `return` し処理しない。
- `Books.AddRangeAsync`
    - 複数同時にデータを追加できる。何度も `Add` するよりこっちのほうが簡潔。
    - まだクエリが作られているだけ。
- `SaveChangesAsync`
    - 遅延実行だからここで SQL が投げられて commit される。

```c#:Program.cs
public static async Task Main(string[] args)
{
    var host = BuildWebHost(args);
    using (var scope = host.Services.CreateScope())
    {
        var provider = scope.ServiceProvider;
        try
        {
            var context = provider.GetRequiredService<BooksContext>();
            await DbInitializer.SeedingAsync(context);
        }
        catch (Exception ex)
        {
            var logger = provider.GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "データベース初期化中にエラーが発生しました。");
        }
    }
    await host.RunAsync();
}
```

- `SeedingAsync` を非同期にしたため、`Main` も非同期にする。
    - 参考：[メイン関数（エントリーポイント）を非同期にする](https://qiita.com/sanonosa/items/b88758daa2931ee74dbf)
    - C# 7.1以降を使用しない場合、`SeedingAsync` を同期メソッドにするか `GetAwaiter().GetResult()` する必要がある。
- `GetRequiredService<TServiceType>`
    - エントリーポイントではコンストラクターによる依存性の注入ができないのでこれで `BooksContext` のインスタンスを取得する。

## 初期データを入れる

- Program.cs が走ればいいので、F5 するだけで登録される。
- ※ `DbInitializer.SeedingAsync` で既にデータがある場合はデータをインサートしないようにしたので、データがある場合にはデータを全部消すか、先に Object explorer からスキーマを削除して `Update-Database` する。
- ![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/4ee8d084-dc65-2540-a159-625480be4622.png)

## 新しいフィールドの追加や制約の変更

- エンティティクラスを変更や変更して `Add-Migration <migration name>` > `Update-Database` するだけでOK
    - 基本的にフィールドの追加や削除だけでなく既存フィールドの制約等もこれで変更できる。 
- 必要に応じてサンプルデータも変更する。

```c#
public class Book
{
    public int ID { get; set; }
    [Required]
    [StringLength(150)]
    public string Title { get; set; }
    [StringLength(150)]
    public string Author { get; set; }
    [Range(1900, 2100)]
    public int PublishYear { get; set; }
    [Required]
    [StringLength(100)]
    public string OriginalTitle { get; set; }
}
```

- `OriginalTitle` を追加

```c#
public class DbInitializer
{
    public static async Task SeedingAsync(BooksContext context)
    {
        await context.Database.EnsureCreatedAsync();
        if (await context.Books.AnyAsync())
            return;
        await context.Books.AddRangeAsync(
            new Book { Title = "アンドロイドは電気羊の夢を見るか? ", Author = "フィリップ・K・ディック", PublishYear = 1968, OriginalTitle = "Do Androids Dream of Electric Sheep?" },
            new Book { Title = "1984年", Author = "ジョージ・オーウェル", PublishYear = 1949, OriginalTitle = "Nineteen Eighty-Four" },
            new Book { Title = "幼年期の終り", Author = "アーサー・C・クラーク", PublishYear = 1953, OriginalTitle= "Childhood's End" },
            new Book { Title = "アルジャーノンに花束を", Author = "ダニエル・キイス", PublishYear = 1959, OriginalTitle= "Flowers for Algernon" },
            new Book { Title = "月は無慈悲な夜の女王", Author = "ロバート・A・ハイライン", PublishYear = 1966, OriginalTitle= "The Moon Is a Harsh Mistress" },
            new Book { Title = "われはロボット", Author = "アイザック・アシモフ", PublishYear = 1950, OriginalTitle= "I, Robot " },
            new Book { Title = "虐殺器官", Author = "伊藤計劃", PublishYear = 2007,OriginalTitle= "虐殺器官" });
        await context.SaveChangesAsync();
    }
}
```

- モデルの変更に合わせてデータも変更。

```shell:パッケージマネージャーコンソール
PM> Add-Migration OriginalTitle
PM> Update-Database
```

- `Add-Migration`
    - エンティティ、コンテキストと DB のスキーマを比べて移行に必要なマイグレーションコードを新しく生成する。

DB のデータを削除してから実行すると登録されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/8e0f4525-b241-30b8-2172-34504e906ae8.png)

読んでいただきありがとうございました。
LocalDB での開発は、他人を気にせずデータ入れ替えられるのが良いと思いました。