.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
プロパティを定義しました。
OnModelCreating
overrideメソッドにてデータベースの詳細を設定していきます。
-
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 Core
Webアプリケーションを選択してください。
アプリケーションのプロジェクト名は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
が気になっているすてぃんでした。終わり(オチ)