EntityFrameworkCoreでPostgreSQLを操作する

`.NETCore`でSNSを作りたいと宣言したので口だけにならないように頑張って勉強します。
今回は`EntityFrameworkCore`を用いて`PostgreSQL`に接続する方法を残しておきます。

## 開発環境

- Windows10(1803)
- Visual Studio 2017
- PostgreSQL 9.6(インストール済みでサービスがPort5432で起動していることを想定)
- .NETCore 2.1

## 到達目標

- C#を書いた通りにDBのテーブルを作成してくれる機能「マイグレーション」を実行できる
- C#上のデータベースに相当する`DbCntext`クラスとテーブルに相当する`Entity`クラスをひとまとめにしたプロジェクトを作成する

`DbContext`と`Entity`をまとめたプロジェクトにするのは別のアプリケーションを作成する際にコピペ流用がしやすいかなと思ったからです。

## 手順

### 事前準備

`PostgreSQL`のサービスを起動し、そこに`hello_db`というデータベースを作成します。テーブルはマイグレーションで作成したいので空っぽでよいです。

### プロジェクトの作成

Visual Studio 2017を起動します。

[ファイル]→[新規作成]→[プロジェクト]で「新しいプロジェクト」ウィンドウが開きます。
左側で[Web/.NET Core]を選択して中央で[クラスライブラリ(.NET Core)]を選択します。
プロジェクト名とソースの保存場所を指定して[OK]をクリックします。
![01_新しいプロジェクト画面.png](https://qiita-image-store.s3.amazonaws.com/0/299625/52c09d2d-564a-78e3-fc5b-92a9f8024c1c.png)

今回は`EFCorePostgresAccess`というプロジェクト名にしました。

[OK]をクリックすると`Class1.cs`だけのプロジェクトが作成されると思います。(この`Class1.cs`は使いません。消してもOKです。)

### NuGetインストール

続きまして、NuGetにて必要なパッケージをインストールしていきましょう。
`EntityFrameworkCore`で`PostgreSQL`を操作するには`Npgsql.EntityFrameworkCore.PostgreSQL`というパッケージを使用します。

[ソリューションエクスプローラー]の`EFCorePostgresAccess`プロジェクトの上で右クリック→[NuGetパッケージの管理]を選択するとNuGetタブが出現します。
「参照」タブにて`Npgsql.EntityFrameworkCore.PostgreSQL`を検索等で表示させて選択。そしてインストール。
![02_NuGetタブ.png](https://qiita-image-store.s3.amazonaws.com/0/299625/d0e7a8c9-be5d-3812-42ee-e5f28f653250.png)

「変更のプレビュー」ダイアログが表示されたら[OK]、「ライセンスへの同意」ダイアログは[同意する]をクリックしましょう。

僕のPCにある`.NETCore`が`version2.1`なので`Npgsql.EntityFrameworkCore.PostgreSQL version2.1.2`をインストールしました。
`.NETCore 2.2`が出ているのでそちらを使いたい場合は`2.2.0`を入れればいいと思います。

### Entity作成

Entityは、データベースの1テーブルをC#のオブジェクトに対応付けたクラスのことです。

ソリューションエクスプローラーの`EFCorePostgresAccess`プロジェクト上で右クリック→[追加]→[新しいフォルダー]で、`Entity`というフォルダを作成します。
`Entity`フォルダの上でさらに右クリック→[追加]→[クラス]で「新しい項目の追加」ウィンドウが表示されます。
![03_Entityクラスの追加.png](https://qiita-image-store.s3.amazonaws.com/0/299625/b50ef1ed-b8a5-970c-8fe5-d3ff45a98d60.png)

今回はアイドルユニットとその在籍メンバーを登録することを想定して下記の二つのEntityを追加しましょう。ユニットとメンバーは1対nの関係です。

```csharp:EFCorePostgresAccess/Entity/Unit.cs
using System;
using System.Collections.Generic;

namespace EFCorePostgresAccess.Entity
{
    /// <summary>
    /// ユニットEntity
    /// </summary>
    public class Unit
    {
        /// <summary>
        /// ユニットID
        /// </summary>
        public int UnitId { set; get; }

        /// <summary>
        /// ユニット名
        /// </summary>
        public string UnitName { get; set; }

        /// <summary>
        /// 結成日
        /// </summary>
        public DateTime FormedDate { get; set; }

        /// <summary>
        /// Memberナビゲーションプロパティ
        /// </summary>
        public List<Member> Members { get; set; }
    }
}

```

```csharp:Entity/Member.cs
using System;

namespace EFCorePostgresAccess.Entity
{
    /// <summary>
    /// メンバーEntity
    /// </summary>
    public class Member
    {
        /// <summary>
        /// メンバーID
        /// </summary>
        public int MemberId { set; get; }

        /// <summary>
        /// メンバー名
        /// </summary>
        public string MemberName { get; set; }

        /// <summary>
        /// ユニット加入日
        /// </summary>
        public DateTime? JoinedDate { get; set; }

        /// <summary>
        /// 誕生日
        /// </summary>
        public DateTime Birthday { get; set; }

        /// <summary>
        /// ユニットID
        /// </summary>
        public int UnitId { get; set; }

        /// <summary>
        /// Unitナビゲーションプロパティ
        /// </summary>
        public Unit Unit { get; set; }
    }
}

```

ナビゲーションプロパティとは、データベースのリレーションを`Entity`で表現したものです。
`Member`クラスから見ると`Unit`は一つなので、プロパティとして`Unit`を一つ持ちます。
`Unit`クラスから見ると`Member`は複数あるので、プロパティとして`Member`のコレクションを持ちます。(`List<T>`にしましたが`IEnumerable<T>`ならなんでもいいと思います。たぶん。試してないけど。)

### DbContext作成

テーブルを対応付けたクラスが`Entity`です。
そしてデータベースを対応付けたクラスが`DbContext`になります。(そんなイメージ)

`EFCorePostgresAccess`プロジェクトの直下に`HelloContext.cs`というファイルを作成しましょう。
`Microsoft.EntityFrameworkCore.DbContext`クラスを継承して独自の`DbContext`クラスを作成します。

```csharp:HelloContext.cs
using System;
using Microsoft.EntityFrameworkCore;
using EFCorePostgresAccess.Entity;

namespace EFCorePostgresAccess
{
    /// <summary>
    /// DbContext
    /// </summary>
    public class HelloContext : DbContext
    {
        public HelloContext(DbContextOptions options)
            :base(options)
        {
        }

        /// <summary>
        /// ユニット
        /// </summary>
        public DbSet<Unit> Units { get; set; }

        /// <summary>
        /// メンバー
        /// </summary>
        public DbSet<Member> Members { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Unit>(entity =>
            {
                entity.ToTable("unit")
                .ForNpgsqlHasComment("ユニット");

                entity.HasKey(e => e.UnitId);

                entity.Property(e => e.UnitId)
                .HasColumnName("unit_id")
                .ForNpgsqlHasComment("ユニットID");

                entity.Property(e => e.UnitName)
                .HasColumnName("unit_name")
                .ForNpgsqlHasComment("ユニット名");

                entity.Property(e => e.FormedDate)
                .HasColumnName("formed_date")
                .ForNpgsqlHasComment("結成日");
            });

            modelBuilder.Entity<Member>(entity =>
            {
                entity.ToTable("member")
                .ForNpgsqlHasComment("メンバー");

                entity.HasKey(e => e.MemberId);

                entity.Property(e => e.MemberId)
                .HasColumnName("member_id")
                .ForNpgsqlHasComment("ユニットID");

                entity.Property(e => e.MemberName)
                .HasColumnName("member_name")
                .ForNpgsqlHasComment("メンバー名");

                entity.Property(e => e.Birthday)
                .HasColumnName("birthday")
                .ForNpgsqlHasComment("誕生日");

                entity.Property(e => e.JoinedDate)
                .HasColumnName("joined_date")
                .ForNpgsqlHasComment("加入日");

                entity.Property(e => e.UnitId)
                .HasColumnName("unit_id")
                .ForNpgsqlHasComment("ユニットID");

                entity.HasOne(m => m.Unit)
                .WithMany(u => u.Members)
                .HasForeignKey(p => p.UnitId);
            });

            base.OnModelCreating(modelBuilder);
        }
    }
}
```

`DbSet<T>`プロパティを定義することで`T`型のテーブルをデータベースに`Create`していることになります。今回は`DbSet<Unit> Units`プロパティと`DbSet<Member> Members`プロパティを定義しました。

`OnModelCreating`overrideメソッドにてデータベースの詳細を設定していきます。

- `ToTable("テーブル名")` Entityに対応するテーブルの名称指定
  - `ForNpgsqlHasComment("テーブル論理名")` テーブルの論理名を指定
- `HasKey(e => e.プロパティ)` 主キーを指定
  - `HasKey(e => new { e.Key1, e.Key2, ... })` 複合キーの場合
- `Property(e => e.プロパティ)` この後に設定するプロパティを指定
  - `HasColumnName("カラム名")` テーブルのカラム名を指定
  - `ForNpgsqlHasComment("カラム論理名")` カラムの論理名を指定

そして`Member`の`modelBuilder`の最後に注目。

```csharp
entity.HasOne(m => m.Unit)
 .WithMany(u => u.Members)
 .HasForeignKey(p => p.UnitId);
```
`HasOne`メソッドで`Member.Unit`はナビゲーションプロパティだよという指定をします。
それにぶら下げて`WithMany`メソッドを呼ぶことで逆の`Unit.Members`もナビゲーションプロパティだよということを指定しています。
最後に`HasForeignKey`メソッドにて、どのプロパティが外部キーなのかを指定します。
ここまで指定することで、`Unit`と`Member`の1対nの関係を築くことができます。

他にもカラムの初期値や明示的な型指定(未指定の場合は`Entity`のプロパティに合わせて自動指定)など、細かいテーブルへの設定は`OnModelCreating`メソッドの中で行います。その他の設定方法は[このページ](https://docs.microsoft.com/ja-jp/ef/core/)から読み取ってください。(日本語訳がひどくて英語のままのほうがまだマシなので。。。)

ご存知の方もいるかと思いますが、`Entity`クラスで属性(Attribute)を付与することで詳細設定することもできます。しかし複合キー指定など属性付与ではできないことがあるので今回は`OnModelCreating`メソッドでの設定にしました。

ここまでで`EntityFrameworkCore`の準備は終わりです。次からは使用する側を作成します。

### Webアプリケーションの作成

#### プロジェクトの準備

`Entity`や`DbContext`を利用するアプリケーションを作成していきます。

ソリューションエクスプローラーの[ソリューション'EFCorePostgresAccess']で右クリック→[追加]→[新しいプロジェクト]でまたまたプロジェクトを追加します。
![04_新しいプロジェクト画面_WebApplication.png](https://qiita-image-store.s3.amazonaws.com/0/299625/57b37920-9801-ab3f-5359-db4b95c7fd49.png)

`ASP.NET Core`Webアプリケーションを選択してください。
アプリケーションのプロジェクト名は`HelloApplication`という名前にしました。
![05_ASP.NETCore新規追加.png](https://qiita-image-store.s3.amazonaws.com/0/299625/1a98b65e-d860-3f80-1559-56ba1dbef749.png)


GUIまで作るのはめんどくさいのでJsonを投げ合うだけのRESTful APIを作成します。

作成したEntityFrameworkを利用するために`HelloApplication`プロジェクトの[依存関係]で右クリック→[参照の追加]で`EFCorePostgresAccess`の参照を追加しましょう。チェックを入れて[OK]をクリックです。
![06_参照マネージャー画面.png](https://qiita-image-store.s3.amazonaws.com/0/299625/8e347d5c-fe38-6c62-e790-298142c0000c.png)

#### 接続先情報の指定

`HelloApplication`プロジェクト作成で`appsettings.json`が自動生成されています。それを下記のように編集しましょう。

```json:HelloApplication/appsettings.json
{
    "Logging": {
        "LogLevel": {
            "Default": "Warning"
        }
    },
    "ConnectionStrings": {
        "HelloContext": "Host=localhost;Port=5432;User Id=postgres;Password=パスワード;Database=hello_db"
    }
}
```
そして`StartUp.cs`の`ConfigureServices`メソッドに`AddDbContext`を追記しましょう。`Configuration.GetConnectionString`によって`appsettings.json`から`"ConnectionStrings"`内の`"HelloContext"`の値を取得してきます。

```csharp:HelloApplication/Startup.cs(抜粋)
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<HelloContext>(option =>
{
    option.UseNpgsql(Configuration.GetConnectionString("HelloContext"));
});
services.AddMvc().AddJsonOptions(options =>
{
    options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
}
```
これで`HelloContext`インスタンスに対する操作は`PostgreSQL`に対する操作になります。
`services.AddMvc().AddJsonOptions`では循環参照をしているインスタンスをJson化する際に循環を無視するおまじないです。

#### マイグレーションの実行

接続情報を指定できたところでマイグレーションを実行してみましょう。
メニューの[表示]→[その他のウィンドウ]→[パッケージマネージャーコンソール]を選択します。

[既定のプロジェクト]が`EFCorePostgresAccess`であることを確認して、コンソールにて以下のコマンドを実行します。

```
Add-Migration InitialCreate
```
すると`EFCorePostgresAccess`プロジェクトに`Migrations`というフォルダが作成され、`(timestamp)_InitialCreate.cs`というファイルが作成されます。

中を見てみると、SQLの`CREATE TABLE`に相当しそうなことが書かれています。この時点ではSQLが作成されただけでまだデータベースには反映されていません。

さらにコンソールで以下のコマンドを実行してみましょう。

```
Update-Database
```
すると、実データベースにSQLが発行されてテーブルが作成されます。
`DbContext`に保持した`Entity`に加えて`__EFMigrationsHistory`というテーブルも作成されます。これは`Add-Migration`の度に作成されるデータベース変更用のファイルがどこまで反映されているかの履歴管理をするテーブルです。

各テーブルを見ていきます。([A5:SQL](https://a5m2.mmatsubara.com/)という神ツールでデータベースを見ています)

##### `unit`テーブル
![07_新規Migration後unit.png](https://qiita-image-store.s3.amazonaws.com/0/299625/57f37ef3-4ff8-c66e-c091-3d4d8ff49ab7.png)
`Entity`に定義したプロパティがそのままカラムに反映されているのがわかります。データ型もプロパティの型と一致しています。

##### `member`テーブル
![08_新規Migration後member.png](https://qiita-image-store.s3.amazonaws.com/0/299625/7792bb61-ec0f-c078-d036-c6e70f069cf8.png)
`member`も同様にプロパティがカラムに対応しています。
実は`Member.JoinedDate`は`null`許容型で宣言していました。ですので[必須]に`Yes`が入っていません。

![09_新規Migration後member外部キー.png](https://qiita-image-store.s3.amazonaws.com/0/299625/f99c316a-a3da-10e7-848a-d63ac548f9e1.png)
そして`unit_id`カラムは外部キーとして`unit`テーブルを参照していることがわかります。

#### CRUDしてみよう

下記の2つの`Controllers`クラスを追加しましょう。

```csharp:HelloApplication/Controllers/UnitController.cs
using System.Collections.Generic;
using System.Linq;
using EFCorePostgresAccess;
using EFCorePostgresAccess.Entity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace HelloApplication.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UnitController : ControllerBase
    {
        HelloContext _context;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="context">DbContext</param>
        public UnitController(HelloContext context)
        {
            _context = context;
        }

        [HttpGet("{id}")]
        public ActionResult<Unit> Get(int id)
        {
            var result = _context.Units.Include(a => a.Members).SingleOrDefault(a => a.UnitId == id);

            return result;
        }

        [HttpPost]
        public void Post([FromBody] Unit entity)
        {
            _context.Units.Add(entity);

            _context.SaveChanges();
        }

        [HttpPut]
        public void Put([FromBody] Unit unit)
        {
            _context.Entry(unit).State = EntityState.Modified;

            _context.SaveChanges();
        }

        [HttpDelete("{id}")]
        public void Delete(int id)
        {
            var entity = _context.Units.SingleOrDefault(a => a.UnitId == id);

            _context.Remove(entity);

            _context.SaveChanges();
        }
    }
}
```

```csharp:HelloApplication/Controllers/MemberController.cs
using System.Collections.Generic;
using System.Linq;
using EFCorePostgresAccess;
using EFCorePostgresAccess.Entity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace HelloApplication.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class MemberController : ControllerBase
    {
        HelloContext _context;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="context">DbContext</param>
        public MemberController(HelloContext context)
        {
            _context = context;
        }

        [HttpGet("{id}")]
        public ActionResult<Member> Get(int id)
        {
            return _context.Members.SingleOrDefault(a => a.MemberId == id);
        }

        [HttpPost]
        public void Post([FromBody] Member entity)
        {
            _context.Members.Add(entity);

            _context.SaveChanges();
        }

        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string name)
        {
            var entity = _context.Members.SingleOrDefault(a => a.MemberId == id);

            entity.MemberName = name;

            _context.SaveChanges();
        }

        [HttpDelete("{id}")]
        public void Delete(int id)
        {
            var entity = _context.Members.SingleOrDefault(a => a.MemberId == id);

            _context.Remove(entity);

            _context.SaveChanges();
        }
    }
}

```

F5を押すかメニューバーの[デバッグ]→[デバッグの開始]をクリックしてIIS Expressを起動します。(`ValuesController.cs`を削除した人は404NotFoundが出ますが無視でOKです。煩わしいと思ったら[ここ](https://qiita.com/Nossa/items/e6bc1bc542a9fad94ebf)の一番下を参考)
自動で採番されるポート番号を覚えておきましょう。

HTTPの検証は`Postman`というツールで行います。HTTPのリクエストが送信できればなんでもよいのですが。

##### POST
まずはデータを登録します。
![10_PostmanInsertつばき.png](https://qiita-image-store.s3.amazonaws.com/0/299625/8855cbef-fca8-0dea-b691-dd3f3f34fd7f.png)
`Send`ボタンで実行すると`unit`テーブルにレコードが挿入されます。
同様にいくつかのユニットとメンバーのレコードを挿入しておきました。

###### unit
| unit_id | unit_name | formed_date |
|:-:|:-:|:-:|
| 1 | つばきファクトリー | 2015/04/29 |
| 2 | こぶしファクトリー | 2015/01/02 |
| 3 | モーニング娘。 | 1994/09/14 |

###### member
| member_id | member_name | joined_date | birthday | unit_id |
|:-:|:-:|:-:|:-:|:-:|
| 1 | 谷本安美 | 2015/04/29 | 1999/11/06 | 1 |
| 2 | 小片リサ | 2015/04/29 | 1998/11/05 | 1 |
| 3 | 小野田紗栞 | 2016/08/13 | 2001/12/17 | 1 |
| 4 | 広瀬彩海 | 2015/01/02 | 1998/08/04 | 2 |
| 5 | 井上玲音 | 2015/01/02 | 2001/07/17 | 2 |
| 6 | 佐藤優樹 | 2011/09/29 | 1999/05/07 | 3 |
| 7 | 小田さくら | 2012/09/14 | 1999/03/12 | 3 |
| 8 | 牧野真莉愛 | 2014/09/30 | 2001/02/02 | 3 |
| 9 | 加賀楓 | 2016/12/12 | 1999/11/30 | 3 |

##### GET

`POST`したデータを`GET`しましょう。
`https://localhost:44317/api/member/1`にアクセスすると下記のようなjsonが返ってきます。

```json:Member
{
    "memberId": 1,
    "memberName": "谷本安美",
    "joinedDate": "2015-04-29T00:00:00",
    "birthday": "1999-11-06T00:00:00",
    "unitId": 1,
    "unit": null
}
```
今度は`https://localhost:44317/api/unit/1`にアクセスします。

```json:Unit
{
    "unitId": 1,
    "unitName": "つばきファクトリー",
    "formedDate": "2015-04-29T00:00:00",
    "members": [
        {
            "memberId": 1,
            "memberName": "谷本安美",
            "joinedDate": "2015-04-29T00:00:00",
            "birthday": "1999-11-06T00:00:00",
            "unitId": 1
        },
        {
            "memberId": 2,
            "memberName": "小片リサ",
            "joinedDate": "2015-04-29T00:00:00",
            "birthday": "1998-11-05T00:00:00",
            "unitId": 1
        },
        {
            "memberId": 3,
            "memberName": "小野田紗栞",
            "joinedDate": "2016-08-13T00:00:00",
            "birthday": "2001-12-17T00:00:00",
            "unitId": 1
        }
    ]
}
```
`Member`のjsonでは`"unit"`が`null`になっています。一方`Unit`ではぶら下がる`"members"`を同時に取得しています。
これは`Include(a => a.ナビゲーションプロパティ)`メソッドの有無の違いです。データ取得の際に`Include`を挟むと指定したナビゲーションプロパティのインスタンスが生成された状態でデータが取得されます。

##### PUT

続いて、既存データを更新する`UPDATE`です。
`https://localhost:44317/api/unit`に対して次のjsonを`PUT`します。

```json
{
	"unitId" : 3,
	"unitName" : "モーニング娘。'18",
    "formedDate": "1994-09-14T00:00:00"
}
```
「モーニング娘。'18」に名称が変更することができます。

次は`https://localhost:44317/api/member/1`に対して`"あんみぃ"`という文字列をリクエストボディに詰めて投げると`member_id == 1`のレコードの`member_name`が`"あんみぃ"`に更新されます。

2つの更新方法には違いがあります。
前者では`Entity`を丸ごと渡すことですべてのプロパティ(カラム)を更新します。

```csharp
_context.Entry(unit).State = EntityState.Modified;
_context.SaveChanges();
```

後者では一度`Entity`を取得して、特定のプロパティに値を代入してから`SaveChages()`を呼ぶことで、一部のプロパティのみ更新します。

```csharp
var entity = _context.Members.SingleOrDefault(a => a.MemberId == id);
entity.MemberName = name;
_context.SaveChanges();
```

##### DELETE

`DELETE`はシンプルですね。`List<T>`などの他のコレクションクラスと同じように`Remove`メソッドを呼んでいるだけです。(`SaveChanges()`はもちろん必要)

現在の設定では`DELETE`で`Unit`を削除すると、外部参照している`Member`も削除されます。
参照元を削除する`CASCADE`ではなく被参照データの削除させない`RESTRICT`にする方法は調べておきます。(今後の課題ってやつ)

## 所感

めちゃくちゃ長い記事になってしまいました。が、この記事をあれやこれやと調べながら書いてるうちにかなり`EntityFrameworkCore`の理解が深まった気がします。
誤字脱字や間違った内容を記述している場合は教えていただけると幸いです。

最近`MongoDB`が気になっているすてぃんでした。終わり(オチ)


## 参考記事
- https://qiita.com/Nossa/items/e6bc1bc542a9fad94ebf