Posted at

EntityFrameworkCoreでPostgreSQLを操作する

.NETCoreでSNSを作りたいと宣言したので口だけにならないように頑張って勉強します。

今回はEntityFrameworkCoreを用いてPostgreSQLに接続する方法を残しておきます。


開発環境


  • Windows10(1803)

  • Visual Studio 2017

  • PostgreSQL 9.6(インストール済みでサービスがPort5432で起動していることを想定)

  • .NETCore 2.1


到達目標


  • C#を書いた通りにDBのテーブルを作成してくれる機能「マイグレーション」を実行できる

  • C#上のデータベースに相当するDbCntextクラスとテーブルに相当するEntityクラスをひとまとめにしたプロジェクトを作成する

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


手順


事前準備

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


プロジェクトの作成

Visual Studio 2017を起動します。

[ファイル]→[新規作成]→[プロジェクト]で「新しいプロジェクト」ウィンドウが開きます。

左側で[Web/.NET Core]を選択して中央で[クラスライブラリ(.NET Core)]を選択します。

プロジェクト名とソースの保存場所を指定して[OK]をクリックします。

01_新しいプロジェクト画面.png

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

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


NuGetインストール

続きまして、NuGetにて必要なパッケージをインストールしていきましょう。

EntityFrameworkCorePostgreSQLを操作するにはNpgsql.EntityFrameworkCore.PostgreSQLというパッケージを使用します。

[ソリューションエクスプローラー]のEFCorePostgresAccessプロジェクトの上で右クリック→[NuGetパッケージの管理]を選択するとNuGetタブが出現します。

「参照」タブにてNpgsql.EntityFrameworkCore.PostgreSQLを検索等で表示させて選択。そしてインストール。

02_NuGetタブ.png

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

僕のPCにある.NETCoreversion2.1なのでNpgsql.EntityFrameworkCore.PostgreSQL version2.1.2をインストールしました。

.NETCore 2.2が出ているのでそちらを使いたい場合は2.2.0を入れればいいと思います。


Entity作成

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

ソリューションエクスプローラーのEFCorePostgresAccessプロジェクト上で右クリック→[追加]→[新しいフォルダー]で、Entityというフォルダを作成します。

Entityフォルダの上でさらに右クリック→[追加]→[クラス]で「新しい項目の追加」ウィンドウが表示されます。

03_Entityクラスの追加.png

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


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; }
}
}



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クラスを作成します。


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プロパティを定義しました。

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



  • ToTable("テーブル名") Entityに対応するテーブルの名称指定



    • ForNpgsqlHasComment("テーブル論理名") テーブルの論理名を指定




  • HasKey(e => e.プロパティ) 主キーを指定



    • HasKey(e => new { e.Key1, e.Key2, ... }) 複合キーの場合




  • Property(e => e.プロパティ) この後に設定するプロパティを指定



    • HasColumnName("カラム名") テーブルのカラム名を指定


    • ForNpgsqlHasComment("カラム論理名") カラムの論理名を指定



そしてMembermodelBuilderの最後に注目。

entity.HasOne(m => m.Unit)

.WithMany(u => u.Members)
.HasForeignKey(p => p.UnitId);

HasOneメソッドでMember.Unitはナビゲーションプロパティだよという指定をします。

それにぶら下げてWithManyメソッドを呼ぶことで逆のUnit.Membersもナビゲーションプロパティだよということを指定しています。

最後にHasForeignKeyメソッドにて、どのプロパティが外部キーなのかを指定します。

ここまで指定することで、UnitMemberの1対nの関係を築くことができます。

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

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

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


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


プロジェクトの準備

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

ソリューションエクスプローラーの[ソリューション'EFCorePostgresAccess']で右クリック→[追加]→[新しいプロジェクト]でまたまたプロジェクトを追加します。

04_新しいプロジェクト画面_WebApplication.png

ASP.NET CoreWebアプリケーションを選択してください。

アプリケーションのプロジェクト名はHelloApplicationという名前にしました。

05_ASP.NETCore新規追加.png

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

作成したEntityFrameworkを利用するためにHelloApplicationプロジェクトの[依存関係]で右クリック→[参照の追加]でEFCorePostgresAccessの参照を追加しましょう。チェックを入れて[OK]をクリックです。

06_参照マネージャー画面.png


接続先情報の指定

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


HelloApplication/appsettings.json

{

"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"HelloContext": "Host=localhost;Port=5432;User Id=postgres;Password=パスワード;Database=hello_db"
}
}

そしてStartUp.csConfigureServicesメソッドにAddDbContextを追記しましょう。Configuration.GetConnectionStringによってappsettings.jsonから"ConnectionStrings"内の"HelloContext"の値を取得してきます。


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という神ツールでデータベースを見ています)


unitテーブル

07_新規Migration後unit.png

Entityに定義したプロパティがそのままカラムに反映されているのがわかります。データ型もプロパティの型と一致しています。


memberテーブル

08_新規Migration後member.png

memberも同様にプロパティがカラムに対応しています。

実はMember.JoinedDatenull許容型で宣言していました。ですので[必須]にYesが入っていません。

09_新規Migration後member外部キー.png

そしてunit_idカラムは外部キーとしてunitテーブルを参照していることがわかります。


CRUDしてみよう

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


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();
}
}
}



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です。煩わしいと思ったらここの一番下を参考)

自動で採番されるポート番号を覚えておきましょう。

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


POST

まずはデータを登録します。

10_PostmanInsertつばき.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が返ってきます。


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にアクセスします。


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します。

{

"unitId" : 3,
"unitName" : "モーニング娘。'18",
"formedDate": "1994-09-14T00:00:00"
}

「モーニング娘。'18」に名称が変更することができます。

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

2つの更新方法には違いがあります。

前者ではEntityを丸ごと渡すことですべてのプロパティ(カラム)を更新します。

_context.Entry(unit).State = EntityState.Modified;

_context.SaveChanges();

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

var entity = _context.Members.SingleOrDefault(a => a.MemberId == id);

entity.MemberName = name;
_context.SaveChanges();


DELETE

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

現在の設定ではDELETEUnitを削除すると、外部参照しているMemberも削除されます。

参照元を削除するCASCADEではなく被参照データの削除させないRESTRICTにする方法は調べておきます。(今後の課題ってやつ)


所感

めちゃくちゃ長い記事になってしまいました。が、この記事をあれやこれやと調べながら書いてるうちにかなりEntityFrameworkCoreの理解が深まった気がします。

誤字脱字や間違った内容を記述している場合は教えていただけると幸いです。

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


参考記事