EntityFramework CoreとNpgsqlでPostgreSQLアクセス
そろそろ.NET Core系のプロダクトを使ってアプリを作ってみようと思い立ち調べものをはじめました。
まずはEntityFramework Coreの使い方です。アプリのデータベースにはPostgreSQLを想定しているので、EntityFramework CoreとNpgsqlを使用してPostgreSQLにアクセスするサンプルを作ってみます。
フレームワークのバージョンは.NET Core 2.0にしました。開発環境はVisual Studio for Mac v7.5.2です。
ソリューションの準備
ソリューションを準備します。メニューの[ファイル]→[新しいソリューション]をクリックして表示された画面で .NET Coreアプリのテンプレートを選択します。
対象フレームワークを選択します。
新しいソリューションが作成されます。
NuGetパッケージの追加
作成されたソリューションのプロジェクトを右クリック→[追加]→[NuGetパッケージを追加]をクリックしてパッケージ管理画面を表示。次の3つのパッケージを検索してすべて追加します:
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Proxies
- Npgsql.EntityFramework.PostgreSQL
パッケージを選択して[パッケージを追加]をクリック。
互換性のチェックにしばしの待たされたあと、ライセンス条項への同意を求められるので[同意する]をクリックします。
パッケージのインストールが完了します。これで当座の必要には足ります。
モデルクラスの作成
データベースのリレーション(テーブル)に対応するモデルクラスを作成していきます。
今回はあくまでもEF Coreの使用方法を学ぶことが目的なので、「ブログアプリを作っているつもり」という適当な仮定のもとで、以下のようなモデルクラスを用意します:
モデルクラス | リレーション | 説明 |
---|---|---|
User | tt_users | アプリのユーザーでありブログの投稿者を表す |
Role | tm_roles | ユーザーのロールを表す。ユーザー:ロール=N:N。 |
UserRole | tt_users_roles | ユーザーとロールの結びつきを示す。 |
Post | tt_posts | ブログの投稿を表す。ユーザー:投稿=1:N。 |
Userクラスは次のようになります:
[Table("tt_users")] public class User { [Key] [Column("id")] public long Id { get; set; } [Required] [Column("name")] public string Name { get; set; } [Required] [Column("password")] public string Password { get; set; } public virtual ICollection<UserRole> UserRoles { get; set; } public virtual ICollection<Post> Posts { get; set; } }
属性によりテーブル名やカラム名、プライマリキーを指定しています。User.UserRolesやUser.Postsのようなナビゲーション・プロパティ(内部的にはもちろんリレーション同士の結合がなされる)にはvirtual
修飾子を指定します。
Roleクラスも同様にしてコーディングします:
[Table("tm_roles")] public class Role { [Key] [Column("id")] public long Id { get; set; } [Required] [Column("name")] public string Name { get; set; } public virtual ICollection<UserRole> UserRoles { get; set; } }
UserRoleクラスでは外部キー制約を指定します:
[Table("tt_users_roles")] public class UserRole { [ForeignKey("User")] [Column("user_id")] public long UserId { get; set; } [ForeignKey("Role")] [Column("role_id")] public long RoleId { get; set; } public virtual User User { get; set; } public virtual Role Role { get; set; } }
UserRole.UserIdとUserRole.RoleIdの2つで複合キーを作成したいのですが、これには属性では役不足で後述のC#コードによる設定をして上げる必要があります。
Postクラスにはこれまで見てきたクラスとくらべてあまり目新しいものはありません。せいぜいのところRequired属性のオプション値を変更しているくらいです:
[Table("tt_posts")] public class Post { [Key] [Column("id")] public long Id { get; set; } [Required] [Column("title")] public string Title { get; set; } [Required(AllowEmptyStrings = true)] [Column("body")] public string Body { get; set; } [ForeignKey("Author")] [Column("author_id")] public long AuthorId { get; set; } [Column("published_at")] public DateTime PublishedAt { get; set; } public virtual User Author { get; set; } }
これでモデルの作成は終わりです。実際のアプリではもっと多くのモデルが必要なはずですが、今回の目的はアプリ開発ではないのでスキップします。
DbContextクラスの派生クラスの作成
モデルクラスができたらDbContextクラスの派生クラスを定義します。
DbSet<TModel>を戻り値とするプロパティを定義したあと、DbContext.OnConfiguring(...) と DbContext.OnModelCreating(...) の2つのメソッドをオーバーライドします:
public class MyDbContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<Role> Roles { get; set; } public DbSet<UserRole> UserRoles { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder ob) { // Npgsql.EntityFrameworkCore.PostgreSQLパッケージから提供されているAPI。 // 接続文字列をしていすることで、PostgreSQLに接続できる。 ob.UseNpgsql("Host=localhost;Username=postgres;Password=;Database=efcore_npgsql_study"); // Microsoft.EntityFrameworkCore.Proxiesパッケージから提供されているAPI。 // 遅延ロードプロキシーの機能を有効化する。 // この機能をONにしない場合、ナビゲーションプロパティを都度明示的にロードする必要がある。 ob.UseLazyLoadingProxies(); } protected override void OnModelCreating(ModelBuilder mb) { // tt_users_rolesテーブルのPK指定。 // 複合キーは属性では指定できないのでコードで指定する。 mb.Entity<UserRole>().HasKey(x => new { x.UserId, x.RoleId }); // tt_users, tm_roles, tt_postsのPKの値を自動生成するように指定。 mb.Entity<User>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo(); mb.Entity<Role>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo(); mb.Entity<Post>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo(); } }
OnConfiguring(...)メソッドの中では、PostgreSQLに接続するためのDB接続文字列を指定し、遅延ロードプロキシーの機能を有効化を行っています。
コメントにも書いている通り、遅延ロードプロキシーの機能の有効化は重要です。Microsoftのリファレンスによれば、EF Core 2.1にはナビゲーション・プロパティの値の設定方法に関して3つのオプション「先行ローディング」「明示的ローディング」「遅延ローディング」があります。プロキシーを有効化しない場合は「先行ローディング」もしくは「明示的ローディング」を使用することになり、ナビゲーション・プロパティに値を設定するのに一手間が増えます(そして手間をかけ忘れるとNullReferenceExceptionやArgumentNullExceptionに悩まされることになります)。
個人的には「それなしではありえない」という印象のある「遅延ローディング」ですが、EF Coreに導入されたのは直近のv2.1からです。
OnModelCreating(...)メソッドの中では、複合キーの設定やプライマリキー自動生成の設定を行っています。
DbContextを使用する
今回データベース側にテーブルを作成するのはEF Coreにお任せしてしまうので、これでおおよその準備は整ったことになります。あとは実際にPostgreSQLに対するCRUDを試みるサンプルコードを作成するだけです:
using (var db = new MyDbContext()) { // もしDBがまだ存在しなかった場合は作成する。 db.Database.EnsureCreated(); // マスタ登録がまだだった場合は登録する。 if (db.Roles.Count() == 0) { db.Roles.Add(new Role { Name = "administrator" }); db.Roles.Add(new Role { Name = "editor" }); // 変更を反映する(DMLを実行する)。 db.SaveChanges(); } // ユーザーが存在しない場合はダミーを登録する。 if (db.Users.Count() == 0) { db.Users.Add(new User { Name = "foo", Password = "foo***" }); db.Users.Add(new User { Name = "bar", Password = "bar***" }); db.Users.Add(new User { Name = "baz", Password = "baz***" }); // 変更を反映する(DMLを実行する)。 db.SaveChanges(); db.UserRoles.Add(new UserRole { UserId = db.Users.First(x => x.Name == "foo").Id, RoleId = db.Roles.First(x => x.Name == "administrator").Id }); db.UserRoles.Add(new UserRole { UserId = db.Users.First(x => x.Name == "foo").Id, RoleId = db.Roles.First(x => x.Name == "editor").Id }); db.UserRoles.Add(new UserRole { UserId = db.Users.First(x => x.Name == "bar").Id, RoleId = db.Roles.First(x => x.Name == "editor").Id }); // 変更を反映する(DMLを実行する)。 db.SaveChanges(); } }
DbContext.Database.EnsureCreated()メソッドを呼び出すことでPostgreSQL上に新規のデータベースを作成し、モデルクラスの属性やC#コードのかたちで定義したテーブルや付属のDBオブジェクトが自動生成されます。ただし、メソッドが呼び出された時点でデータベースがすでに存在する場合は何も起きません。
あとはひたすらDbContextを通じてリレーションにアクセスしてC#コードを通じてCRUDを実行する(内部的にはDMLが実行される)だけです。
ひとが一人いなくなるということ
昨年(2017年)秋から親が入院してドタバタしていました。その後年初(思えばちょうど5ヶ月前のこと)には当人が永眠してさらにドタバタ。遺産をめぐりシングル世帯同士の間で合意の上の限嗣相続みたいな手続きを行うためにまたドタバタ。ひとが一人日常生活を離脱していき、さらには特殊な状態の生活をすら終えてしまうということは、物理的にも精神的にもたいへんなイベントなのだということを痛感しました。
物理的というのはもちろん世帯のメンバーの欠員によりいろいろな作業が残されたメンバーに降り掛かってきたり、葬祭に関わるあれやこれや、国や自治体への届け出、金融機関とのやり取りが必要になったりということです。精神的というのはこれはひとによって感じ方はちがうのでしょうけれど・・・こころのなかにぽっかり穴が開くというのはなるほどこういうことかという状態のことです。
彼の亡くなる以前のことも以後のことも、ああすればよかったこうすればよかったが尽きません。ひとがその生を終えようとしつつあるときにどのようにして彼・彼女を送るべきか。今回のことと前後して読んでいた社会学的論考のなかで、その著者はいわば「死別の作法」というべきことごと(看病の仕方、告知の仕方、臨終のまさにそのときの迎え方、葬送の仕方など)が宗教的なものにせよ非宗教的なものにせよ常に型にはめられたものへと回帰していってしまうことを執拗に指摘しています。これは「自由」に関するナイーブな態度と言えるでしょう。
死別に関することに限らず、ひとの行う差異化とは完全新規で独特のやり方を発明することではなく、所与のあるカテゴリと別のあるカテゴリとを対照させることで行われるものではないでしょうか。その差異化の過程で所与のカテゴリが解体されて複数に分けられたり、統合されたり、忘れ去られたりする。あるいは誤差が生じてふいに新たなカテゴリが創始される──話はそれますが、統廃合と誤差、その結果生まれるものを独自性・創造性の発露として評価するよう要求して、個人や個人の制作物に至上の価値を与えようとする運動が芸術ということになるのではないでしょうか──したがって真の問題というのはある選択が「自由」か否かということではなく、その選択がどのような条件のもとで行われるかということでしょう。あるひとがある作法を選ぶ。別のあるひとは別のある作法を選ぶ。その選択のちがいや、選択の機会のちがいが何に由来するかを論じることでしょう。「自由」そのものをうんぬんするのは政治的ないし法律的な議論です。「ひとが何を以て自由とするか」ということこそ社会学的な議論でしょう。
自分自身の体験として親との死別の経過を思い起こすときその選択肢の問題はまだ十分に客観化されていないようです。もしもあのときああしていたら・・・しかしああする以外にどうしたらよかったろうか・・・という思考はまだその選択の由来を云々するステップには進んでいません。
死と死別の社会学―社会理論からの接近 (青弓社ライブラリー)
- 作者: 澤井敦
- 出版社/メーカー: 青弓社
- 発売日: 2005/11/01
- メディア: 単行本
- 購入: 2人 クリック: 21回
- この商品を含むブログ (9件) を見る
Java言語ではFunc<T>とFunc<T,U>は区別できない
この1年くらいの間Javaを離れてC#ばかりをコーディングしていました。戻ってきて愕然としたのは例えばFunc<T>
とFunc<T,U>
という型を仮定した時、Java言語においてはそれらが区別のつかない型、型名の競合を引き起こす型として捉えられてしまうということです。
これは、次の2点を考えればなんのことはない、当然の事態ではあります:
- Java言語のジェネリクスのベースにイレージャのメカニズムが存在すること、つまりコンパイル過程でリフレクションのための型のメタ情報はともかくとして型そのものからは型パラメータが消去されてしまうこと。
- C#言語におけるような型パラメータに即した型名の自動修飾──
Func<T>
はFunc`1
となり、Func<T,U>
はFunc`2
となる──の仕組みはJava言語には存在しないこと。
1点目のイレージャ型のジェネリクスを採用したことの背景にある事情を考えれば、2点目もこのようにならざるをえないわけです。既存APIを維持しつつ拡張しようとすると、イレージャを採用する一方で型名の自動修飾のメカニズム(もしくはルール)は不採用とする必要があるのです。
Java SE 8の関数型インターフェースについて勉強する人はFunction
やBiFunction
やSupplier
などなどのいくつものインターフェース名を記憶させられることになります。種々の型名で区別されるインターフェースが存在することは、それらのインターフェースの目的・役割を示す点でも有意義なものではありますが、実情としては型名の競合を防ぐためにはこうするしかなかったということです。
言い方を変えると「ソースコード上、型パラメータで示される情報は同時に型名でも示さないといけない」という二重修飾の拘束がはたらいているのです。
「だからなんだ、C#だって裏側では型名の自動修飾で競合を回避したりしているではないか」と意見もありそうですが、それにしてもそうした内情をソースコードのレベルで露わにしてしまい、標準APIの「概念的重み」(≒学習コスト)も増大させてしまうのはやはり望ましいことではないでしょう。イレージャ型ジェネリクスには、型パラメータの実パラメータが異なる型を引数とするオーバーロード・メソッドが定義できないという、これも地味ですが何かしらのAPI設計をしていると結構痛い(結果として引数の型名やメソッド名にバリエーションをつくる必要が生じる)問題もあります。
いずれも「言っても仕方のないこと」ではあるのですが、C#からの帰還後にはどうにも気になってしょうがないことでもあるのです。。