WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

PostgreSQL で EF6 Code First

by WebSurfer 2022年7月23日 13:10

先の記事「PostgreSQL で EF6 DB First」の続きで、PostgreSQL に Entity Framework 6 を利用してコードファーストでデータベースを作成し、ASP.MET MVC5 アプリで利用する話を書きます。

PostgresSQL で EF6 Code First

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 は作成日時)。

その内容は自分の環境では以下のようになりました。

BlogInitial.cs

このファイルを使って PostgreSQL にデータベース / テーブルが生成されます。上のコードのテーブル名、スキーマ名はステップ (2) で Blog, Post クラスに付与した Table 属性の通りとなっています。

(6) Update-Database

パッケージマネージャーコンソールから Update-Database を実行します。成功すると PostgreSQL に Blog, Post テーブルが生成されます。

 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 作成のベースとなるクラスファイルは以下のようになります。

BlogInitial.cs

上のコードの中でテーブル名が dbo.Blogs, dbo.Posts となっているところに注目してください。それを見て少し気にはなったのですが、とりあえず Update-Database を実行しました。

エラーなく完了したので PostgreSQL に Blog, Posts テーブルが生成された・・・はずなのですが、コマンドラインツール SQL Shell (pqsl) で探しても見つかりません。(汗)

pgAdmin 4 で探してみると、スキーマが public ではなくて dbo として作成されていました。

pgAdmin 4

上に書いたように、クラスファイルのコードの中でテーブル名 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 属性が付与され、スキーマが指定されます。

Entity Data Model ウィザード

上に書いた手順で 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; }
    }
}

Tags: , , , ,

MVC

PostgreSQL で EF6 DB First

by WebSurfer 2022年7月22日 14:26

PostgreSQL の既存のデータベースから Visual Studio 2022 の ADO.NET Entity Data Model ウィザードで Entity Data Model (EDM) を作成し、ASP.MET MVC5 アプリで利用する話を書きます。ウィザードが期待通り働かず紆余曲折ありました。そのあたりを以下に詳しく書きましたので興味があれば読んでください。

PostgresSQL で EF6 DB First

ASP.NET Core + EF Core ではなく、.NET Framework ベースの ASP.NET MVC5 で Entity Framework 6 を利用する話ですので注意してください。(ちなみに、EF Core では ADO.NET Entity Data Model ウィザードは使えません)

環境は以下の通りで、すべてこの記事を書いた時点での最新版です。

  • 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 のテンプレートで作成)

PosrgreSQL は先の記事「PostgreSQL をインストールしました」に書いた自分の Windows 10 PC にインストールしたものです。

Npgsql PostgreSQL Integration は Visual Studio の拡張機能で、その中に "Create an Entity Framework 6 model from an existing database" という機能があって、ADO.NET Entity Data Model ウィザードで EDM を作成するには必須のようです。バージョン 4.1.12 は Visual Studio 2022 用にリリースされたものだそうです。

Npgsql PostgreSQL Integration

Entityframework6.Npgsql 6.4.3 と Npgsql 6.0.5 は NuGet からインストールします。これをインストールしないとデータソースのメニューの中に PostgreSQL Database が表示されないようです。

NuGet パッケージ

試しにまず .NET Framework 4.8 のコンソールアプリで EDM を作成してみました。

Visual Studio 2022 でコンソールアプリプロジェクトを作成し Entityframework6.Npgsql 6.4.3 と Npgsql 6.0.5 をインストールします。

ソリューションエクスプローラーでプロジェクトを右クリックして[追加(D)]⇒[新しい項目(W)...]で表示される「新しい項目の追加」画面で[ADO.NET Entity Data Model]を選択してウィザードを起動します。

ADO.NET Entity Data Model

「モデルのコンテンツの選択」画面が表示されるので[データベースから EF Designer]を選択して[次へ(N) >]をクリックします。(注: [データベースから Code First]を選択するのは DB First としては正しくないです)

モデルのコンテンツの選択

「データ接続の選択」画面が表示されるので[新しい接続]ボタンをクリックします。

データ接続の選択

「接続の変更」画面が表示されるのでその[変更]ボタンをクリックすると、下のような「データソースの変更」画面が表示されるので PostgreSQL Database を選択して[OK]ボタンをクリックします。

データソースの変更

「接続のプロパティ」画面で接続する PostgreSQL のサーバー名、ポート、データベース名などを入力して[OK]ボタンをクリックします。

接続のプロパティ

「データベースオブジェクトと設定の選択」画面で使用するテーブル(この例では Movie テーブル)にチェックを入れ[完了]をクリックすれば EDM(.edmx などのファイル)が生成されます。

データベースオブジェクトと設定の選択

以上、コンソールアプリでは問題なく EDM を作成できることが確認できたので、Visual Studio 2022 のテンプレートで .NET Framework 4.8 の ASP.NET MVC 5.2.7 ソリューション/プロジェクトを認証なしで作成し、それに上記と同様な手順で EDM の追加をトライしてみました。

コンソールアプリの時と同様に楽勝・・・だと思っていたのですがダメでした。(涙)

Entity Data Model ウィザードの最初の画面で[データベースから EF Designer]を選択して[次へ(N) >]をクリックすると、なぜか「新しい項目の追加」メニューに戻ってしまいます。

いろいろ試したのですが何をどうしてもダメでした。原因不明で今のところ解決策を見つけることができていません。

やむを得ないので、同じソリューション内に別プロジェクトでクラスライブラリを追加し、上に書いたコンソールアプリの手順と同様にして EDM を作りました。その結果が以下の画像です。

クラスライブラリと EDM

ただし、この時もクラスライブラリに Entityframework6.Npgsql 6.4.3, Npgsql 6.0.5 をインストールしてから、続けて EDM を生成しようとすると上に書いたのと同様に「新しい項目の追加」メニューに戻ってしまうという問題が出ました。Visual Studio を一旦終了して再度立ち上げるとその問題は出なくなり EDM は作成できたのですが、どうしてなのかこれも原因不明です。

EDM が完成したらクラスライブラリを ASP.NET MVC5 プロジェクトで参照に追加し、クラスライブラリの App.config に生成された接続文字列を MVC アプリの web.congfig にコピーします。接続文字列のコピーは忘れないようにしてください。

ソリューションをリビルドしてから「新規スキャフォールディングアイテムの追加」操作で CRUD 用の Controller/View を自動的に一式生成できます。

新規スキャフォールディングアイテムの追加

その実行結果がこの記事の一番上の画像です。Index(一覧表示)だけでなく Create, Edit, Details, Delete も期待通りに動きます。

以上ですが、そのあともう一つ不可解な現象がありました。上の操作が完了した後なら ASP.NET MVC5 プロジェクトでも「Entity Data Model ウィザード」画面から先に進めるようになったということです。

Npgsql PostgreSQL Integration 4.1.12 は Visual Studio 2022 用に 6 月にリリースされたばかりということで、動作が不安定なのかもしれません。どうしても .NET Framework ベースでなければならないということでなければ、上に書いたような不可解な動きはない .NET 6.0 の ASP.NET Core MVC の方向に進むのがよさそうです。


さて、次はコードファーストです。これも予期してなかった問題がありました。続けてこの記事に書くと長くなりすぎるので別の記事に書くことにします。

Tags: , , , ,

MVC

EF6 で PK / Unique 制約違反例外をキャッチ

by WebSurfer 2021年3月8日 18:08

EF6 を利用して SQL Server のテーブルの CRUD 操作を行う ASP.NET MVC5 アプリで、新規レコードの Create の際に Primary / Unique キー制約違反例外をキャッチし、エラーメッセージを表示する方法を書きます。

Primary / Unique キー制約の検証

ADO.NET + SqlClient を使って SqlCommand.ExecuteNonQuery メソッドで INSERT 操作を行う場合は、例外が発生すると SqlException がスローされますので、try - catch 句でそれを補足して SqlException.Number プロパティを調べれば例外の内容が分かります。ちなみに、2627 が Primary キー制約違反、2601 が Unique キー制約違反になります。

しかしながら、EF6 の DbContext.SaveChanges メソッドで INSERT 操作を行う場合、スローされる例外は DbUpdateException, DbUpdateConcurrencyException などで、SqlException を直接補足することができません。

そこをどうするかですが、ググって調べた記事「EF6とSQL ServerでUniqueKey違反の例外をキャッチするにはどうすればよいですか?」によると、try - catch 句で DbUpdateException を補足し、その InnerException の InnerException から SqlException を取得できるとのことです。

そういうことなので、上の記事の 2 つ目の回答を参考に、Primary / Unique キー制約違反例外をキャッチし、エラーメッセージを表示する機能を実装してみました。

それが以下のコードです。上の画像は SQL Server のテーブルの ProductName 列に付与した Unique キー制約違反を補足してエラーメッセージを表示したものです。

エンティティクラス (Model)

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Mvc5App2.Models
{
    public class ProductModel
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int ProductId { get; set; }

        [Required]
        [StringLength(128)]
        [Index(IsUnique = true)]
        public string ProductName { get; set; }

        [Required]
        public decimal UnitPrice { get; set; }
    }
}

コンテキストクラス

using Mvc5App2.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace Mvc5App2.DAL
{
    public class ProductContext : DbContext
    {
        public ProductContext() : base("name=DefaultConnection")
        {
        }

        public DbSet<ProductModel> Products { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        }
    }
}

上のエンティティクラスとコンテキストクラスから、EF6 Code First の機能を利用して生成された SQL Server のテーブルの構造は以下の通りです。

生成されたテーブルの構造

Controller / Action Method

スキャフォールディング機能を利用して生成した controller のコードの中の Create アクションメソッドに、上に紹介した記事の Primary / Unique キー制約違反例外を補足するコードを実装したものです。

using System.Data.Entity;
using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;
using Mvc5App2.DAL;
using Mvc5App2.Models;
using System.Data.Entity.Infrastructure;
using System.Data.SqlClient;

namespace Mvc5App2.Controllers
{
    public class ProductModelsController : Controller
    {
        private ProductContext db = new ProductContext();

        // ・・・中略・・・

        // GET: ProductModels/Create
        public ActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Create(
                [Bind(Include = "ProductId,ProductName,UnitPrice")] 
                ProductModel productModel)
        {
            if (ModelState.IsValid)
            {
                db.Products.Add(productModel);

                try
                {
                    await db.SaveChangesAsync();
                }
                catch (DbUpdateException e)
                when (e.InnerException?.InnerException is SqlException sqlEx &&
                        (sqlEx.Number == 2601 || sqlEx.Number == 2627))
                {
                    if (sqlEx.Number == 2627)
                    {
                        ModelState.AddModelError("ProductId", "PK 制約違反");
                    }

                    if (sqlEx.Number == 2601)
                    {
                        ModelState.AddModelError("ProductName", "Unique 制約違反");
                    }

                    return View(productModel);
                }
                return RedirectToAction("Index");
            }

            return View(productModel);
        }
    }

    // ・・・中略・・・
}

View

スキャフォールディング機能を利用して生成した create.cshtml のコードのままで手は加えていません。

@model Mvc5App2.Models.ProductModel

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>ProductModel</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.ProductId,
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ProductId,
                    new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ProductId, "",
                    new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ProductName,
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ProductName,
                    new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ProductName, "",
                    new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.UnitPrice,
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.UnitPrice,
                    new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.UnitPrice, "",
                    new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Primary / Unique キー両方に制約違反がある場合は Primary キーの制約違反だけしか補足できません。両方補足して両方のエラーメッセージを表示する方法は今のところ分かりません。今後の課題ということで。

Tags: , , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar