先の記事「PostgreSQL で EF6 DB First」の続きで、PostgreSQL に Entity Framework 6 を利用してコードファーストでデータベースを作成し、ASP.MET MVC5 アプリで利用する話を書きます。
ASP.NET Core + EF Core ではなく ASP.NET MVC5 + Entity Framework 6 の話ですので注意してください。
一つ注意すべきことは、エンティティクラスに TableAttribute 属性を付与してスキーマ名を設定しないとデフォルトの dbo となってしまうことです。詳しくは下のステップ (2) を見てください。
環境は以下の通りで、すべてこの記事を書いた時点での最新版です。
-
PostgreSQL 14.4
-
Visual Studio Community 2022 17.2.6
-
Npgsql PostgreSQL Integration 4.1.12
-
Entityframework6.Npgsql 6.4.3
-
Npgsql 6.0.5
-
.NET Framework 4.8
-
ASP.NET MVC 5.2.7 (VS2022 のテンプレートで作成)
手順は以下の通りです。
(1) プロジェクトの作成
Visual Studio 2022 のテンプレートで .NET Framework 4.8 の ASP.NET MVC 5.2.7 ソリューション/プロジェクトを認証なしで作成します。
(2) エンティティクラスとコンテキストクラスの追加
Microsoft のドキュメント「新しいデータベースの Code First」にあるエンティティクラスとコンテキストクラスを使ってみます。
Data フォルダを追加しその中に BloggingContext クラスを、既存の Models フォルダに Blog, Post クラスを追加します。
Blog, Post クラスには Table 属性を付与して Schema プロパティを設定することを忘れないようにしてください。PostGreSQL のデフォルトは public です。設定しないと SQL Server のデフォルト dbo になります。
BloggingContext クラスには接続文字列を指定するコンストラクタを追加します。Blog, Post, BloggingContext クラスのコードは順に以下の通りです。
Blog クラス
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace Mvc5PostgreSQL2.Models
{
[Table("Blog", Schema = "public")]
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public virtual List<Post> Posts { get; set; }
}
}
Post クラス
using System.ComponentModel.DataAnnotations.Schema;
namespace Mvc5PostgreSQL2.Models
{
[Table("Post", Schema = "public")]
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
}
BloggingContext クラス
using Mvc5PostgreSQL2.Models;
using System.Data.Entity;
namespace Mvc5PostgreSQL2.Data
{
public class BloggingContext : DbContext
{
public BloggingContext() : base("name=BloggingContext")
{
}
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
}
(3) 接続文字列の追加
上の BloggingContext クラスのコンストラクタで "name=BloggingContext" と指定した BloggingContext という名前の接続文字列を web.config に追加します。
<connectionStrings>
<add name="BloggingContext"
connectionString="Server=localhost;Port=5432;Database=Blogging;Username=postgres;Password=***"
providerName="Npgsql" />
</connectionStrings>
(4) Enable-Migrations
Visual Studio のパッケージマネージャーコンソールから Enable-Migrations を実行します。実行すると Migrations フォルダが作られ、その中に Configuration.cs ファイルが生成されているはずです。
(5) Add-Migration
パッケージマネージャーコンソールから Add-Migration BlogInitial を実行します。BlogInitial という名前は任意です。成功すると Migrations フォルダに xxxxx_BlogInitial.cs というクラスファイルが生成されるはずです (xxxxx は作成日時)。
その内容は自分の環境では以下のようになりました。
このファイルを使って PostgreSQL にデータベース / テーブルが生成されます。上のコードのテーブル名、スキーマ名はステップ (2) で Blog, Post クラスに付与した Table 属性の通りとなっています。
(6) Update-Database
パッケージマネージャーコンソールから Update-Database を実行します。成功すると PostgreSQL に Blog, Post テーブルが生成されます。
(7) Controller / View の作成
ソリューションエクスプローラーで Controller フォルダを右クリックし、[追加 (D)]⇒[新規スキャフォールディングアイテム (F)...]で表示される「新規スキャフォールディングアイテムの追加」画面で[Entity Framework を使用した、ビューがある MVC5 コントローラー]を選び、以下の画像のように入力して[追加]をクリックすれば CRUD 操作のための Controller / View が一式生成されます。
この記事の一番上の画像が、アプリを実行して Create 画面でデータを 2 件追加したものです。
【オマケの話】
上のステップ (2) で「Blog, Post クラスには Table 属性を付与して Schema プロパティを設定することを忘れないようにしてください」と書きましたが、実は、そこにハマって約 1 日悩みました。
SQL Server の場合スキーマ名はデフォルトで dbo で、EF6 もデフォルトで dbo を設定するので、Microsoft のチュートリアルなどでスキーマを設定する例は自分は見たことがないです。なので、スキーマを指定するということは全く頭の中になかったです。(汗)
また同じ失敗をしないように、忘れるとどういうことになるかを書いておきます。
Table 属性を付与しないで Add-Migration を実行すると Migrations フォルダに作成される DB 作成のベースとなるクラスファイルは以下のようになります。
上のコードの中でテーブル名が dbo.Blogs, dbo.Posts となっているところに注目してください。それを見て少し気にはなったのですが、とりあえず Update-Database を実行しました。
エラーなく完了したので PostgreSQL に Blog, Posts テーブルが生成された・・・はずなのですが、コマンドラインツール SQL Shell (pqsl) で探しても見つかりません。(汗)
pgAdmin 4 で探してみると、スキーマが public ではなくて dbo として作成されていました。
上に書いたように、クラスファイルのコードの中でテーブル名 Blogs, Posts の先頭に付与されている dbo がスキーマ名と判断されたようです。
Add-Migration で生成されたファイルのテーブル名から手動で dbo. を削除してから Update-Database コマンドをかけるとスキーマは public になります。しかしそれでは Controller から DB にアクセスする際 42P01: relation "dbo.xxxxx" does not exist というエラーで失敗します。
EF6 は DB にアクセスするのに EDM が必要で、コードファーストの場合はアプリケーションの実行時にコードから生成されるそうです。想像ですが、コードから生成する際、コードにスキーマ名が指定されてないと DB に投げる SQL 文にはデフォルトの dbo が付与されるようです。
PostgreSQL 側でスキーマは public となっているのに、EF6 がスキーマ dbo を付与して SQL 文を投げるので 42P01: relation "dbo.xxxxx" does not exist というエラーで失敗したということのようです。
ちなみに、Entity Data Model ウィザードに[データベースから Code First]という既存のデータベースからコードファーストで使えるコンテキストモデルとエンティティモデルを作成できるオプションがありますが、これから生成されるエンティティクラスには [Table("public.Blog")] というように Table 属性が付与され、スキーマが指定されます。
上に書いた手順で PostgreSQL に作成した Blog テーブル、Post テーブルから Entity Data Model ウィザードの[データベースから Code First]オプションでエンティティクラスを生成すると以下のようになります。
Blog クラス
namespace ConsoleApp4
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
[Table("public.Blog")]
public partial class Blog
{
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Microsoft.Usage",
"CA2214:DoNotCallOverridableMethodsInConstructors")]
public Blog()
{
Post = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Name { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Microsoft.Usage",
"CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Post> Post { get; set; }
}
}
Post クラス
namespace ConsoleApp4
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
[Table("public.Post")]
public partial class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
}