.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]をクリックします。

今回はEFCorePostgresAccessというプロジェクト名にしました。
[OK]をクリックするとClass1.csだけのプロジェクトが作成されると思います。(このClass1.csは使いません。消してもOKです。)
NuGetインストール
続きまして、NuGetにて必要なパッケージをインストールしていきましょう。
EntityFrameworkCoreでPostgreSQLを操作するにはNpgsql.EntityFrameworkCore.PostgreSQLというパッケージを使用します。
[ソリューションエクスプローラー]のEFCorePostgresAccessプロジェクトの上で右クリック→[NuGetパッケージの管理]を選択するとNuGetタブが出現します。
「参照」タブにてNpgsql.EntityFrameworkCore.PostgreSQLを検索等で表示させて選択。そしてインストール。

「変更のプレビュー」ダイアログが表示されたら[OK]、「ライセンスへの同意」ダイアログは[同意する]をクリックしましょう。
僕のPCにある.NETCoreがversion2.1なのでNpgsql.EntityFrameworkCore.PostgreSQL version2.1.2をインストールしました。
.NETCore 2.2が出ているのでそちらを使いたい場合は2.2.0を入れればいいと思います。
Entity作成
Entityは、データベースの1テーブルをC#のオブジェクトに対応付けたクラスのことです。
ソリューションエクスプローラーのEFCorePostgresAccessプロジェクト上で右クリック→[追加]→[新しいフォルダー]で、Entityというフォルダを作成します。
Entityフォルダの上でさらに右クリック→[追加]→[クラス]で「新しい項目の追加」ウィンドウが表示されます。

今回はアイドルユニットとその在籍メンバーを登録することを想定して下記の二つのEntityを追加しましょう。ユニットとメンバーは1対nの関係です。
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; }
}
}
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クラスを作成します。
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("カラム論理名")カラムの論理名を指定
-
そしてMemberのmodelBuilderの最後に注目。
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メソッドの中で行います。その他の設定方法はこのページから読み取ってください。(日本語訳がひどくて英語のままのほうがまだマシなので。。。)
ご存知の方もいるかと思いますが、Entityクラスで属性(Attribute)を付与することで詳細設定することもできます。しかし複合キー指定など属性付与ではできないことがあるので今回はOnModelCreatingメソッドでの設定にしました。
ここまででEntityFrameworkCoreの準備は終わりです。次からは使用する側を作成します。
Webアプリケーションの作成
プロジェクトの準備
EntityやDbContextを利用するアプリケーションを作成していきます。
ソリューションエクスプローラーの[ソリューション'EFCorePostgresAccess']で右クリック→[追加]→[新しいプロジェクト]でまたまたプロジェクトを追加します。

ASP.NET CoreWebアプリケーションを選択してください。
アプリケーションのプロジェクト名はHelloApplicationという名前にしました。

GUIまで作るのはめんどくさいのでJsonを投げ合うだけのRESTful APIを作成します。
作成したEntityFrameworkを利用するためにHelloApplicationプロジェクトの[依存関係]で右クリック→[参照の追加]でEFCorePostgresAccessの参照を追加しましょう。チェックを入れて[OK]をクリックです。

接続先情報の指定
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"の値を取得してきます。
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テーブル

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

memberも同様にプロパティがカラムに対応しています。
実はMember.JoinedDateはnull許容型で宣言していました。ですので[必須]にYesが入っていません。

そしてunit_idカラムは外部キーとしてunitテーブルを参照していることがわかります。
CRUDしてみよう
下記の2つのControllersクラスを追加しましょう。
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();
}
}
}
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
まずはデータを登録します。

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が返ってきます。
{
"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にアクセスします。
{
"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()はもちろん必要)
現在の設定ではDELETEでUnitを削除すると、外部参照しているMemberも削除されます。
参照元を削除するCASCADEではなく被参照データの削除させないRESTRICTにする方法は調べておきます。(今後の課題ってやつ)
所感
めちゃくちゃ長い記事になってしまいました。が、この記事をあれやこれやと調べながら書いてるうちにかなりEntityFrameworkCoreの理解が深まった気がします。
誤字脱字や間違った内容を記述している場合は教えていただけると幸いです。
最近MongoDBが気になっているすてぃんでした。終わり(オチ)