WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

PostgreSQL で EF6 Code First

by WebSurfer 23. July 2022 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

null 許容許容参照型と EF Code First

by WebSurfer 12. May 2022 15:15

Visual Studio 2022 のテンプレートを使って .NET 6.0 アプリのプロジェクトを作ると、デフォルトで「Null 許容」オプションが有効にされています。その状態での EF Code First による SQL Server データベース生成に新発見 (自分が知らなかっただけですが) があったので備忘録として書いておきます。

「Null 許容」オプション

EF Code First でデータベースを生成すると、元となるコードのクラス定義の中のプロパティの型と付与する属性によって生成されるデータベースの列の型と NULL 可/不可が決まってきます。新発見というのは「Null 許容」オプションの有効化によって、生成される列の NULL 可/不可が以前と違ってくるということです。

値型の場合は「Null 許容」オプションの有効/無効は関係なく結果は以前と同じになります。すなわちデフォルトでデータベースの当該列は NULL 不可になります。NULL 可にしたい場合は Nullable<T> 型(例えば int? とか DateTime? など)をプロパティの型に使います。

違うのは参照型の場合です。「Null 許容」オプションが有効にされていると、例えばプロパティの型を string とすると当該データベースの列は NULL 不可に、string? とすると NULL 可になります。

以前 (null 許容参照型が使えない時代または「Null 許容」オプションが無効) は string 型は NULL 可になりました。NULL 不可にしたい場合は当該プロパティに RequiredAttribute 属性を付与していました。

実際にアプリを作って試してみましたので具体例を以下に書きます。

Visual Studio 2022 のテンプレートでフレームワークを .NET 6.0 としてコンソールアプリを作成します。その状態で上の画像のように「Null 許容」オプションが有効化されています。

NuGet パッケージ Microsoft.EntityFrameworkCore.SqlServer と Microsoft.EntityFrameworkCore.Tool をインストールします。前者は SQL Server 用の EF Core 本体、後者は Migration 操作を行うためのツールです。

NuGet パッケージ

Microsoft のドキュメント「新しいデータベースの Code First」と同様なコンテキストクラスとエンティティクラスを実装します。コードは以下の通りです。(null 許容参照型対応のため = null! を追加するなどしていますが基本は同じ)

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; } = null!;

    public virtual List<Post>? Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; } = null!;

    public string? Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog? Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; } = null!;
    public DbSet<Post> Posts { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlServer("接続文字列");
        }
    }
}

上のコードの Blog クラスの Name プロパティ、Post クラスの Title, Content プロパティの型を string / string? と使い分けている点に注目してください。これをベースに Migration 操作によって SQL Server にデータベースを生成した結果が以下の画像です。

SQL Server にデータベース生成

プロパティが string 型になっている Name, Title プロパティに対応する SQL Server データベースの Name 列と Title 列は NULL 不可に、string? 型になっている Content プロパティに対応するデータベースの Content 列は NULL 可になっています。

以前 (null 許容参照型が使えない時代または「Null 許容」オプションが無効) は、上にも書きましたが、プロパティの型が参照型の場合はデータベースの当該列は NULL 可になります。NULL 不可にする場合は RequiredAttribute 属性を付与します。試しに、以下のように #nullable disable を付与したクラス定義を追加し、Migration 操作で SQL Server に Products テーブルを生成してみました。

#nullable disable
public class Product
{
    public int ProductId { get; set; }

    [Required]
    public string ProductName { get; set; }

    public string Decription { get; set; }

    [Column(TypeName = "decimal(18,4)")]
    public decimal UnitPrice { get; set; }
}

結果は以下の通りです。プロパティの型が string の ProductName, Description に該当する列の NULL 可/不可を見てください。プロパティに RequiredAttribute 属性を付与しないと NULL 可になります。

#nullable disable で生成

既存のデータベースからリバースエンジニアリングで生成したコンテキストクラス、エンティティクラスも「Null 許容」オプションが有効化されている場合は上の Blog, Post クラスと同様になります。

試しに Microsoft のサンプルデータベース Northwind の Categories テーブルからリバースエンジニアリングでコンテキストクラス、エンティティクラスを生成してみました。Categories テーブルは以下の内容になっています。CategoryName 列が NULL 不可、Description 列が NULL 可になっているところに注目してください。

Northwind の Categories テーブル

上の Categories テーブルからリバースエンジニアリングを使ってデータアノテーション属性を含めてエンティティクラスを生成すると以下の通りとなります。データベースのテーブルの各列の NULL 可/不可と、生成されたクラス定義の各プロパティの型を見てください。データベースの列が NULL 可の場合はプロパティの型は null 許容(? を付与されている)となっています。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace MvcCore6App2.Models
{
    [Index("CategoryName", Name = "CategoryName")]
    public partial class Category
    {
        public Category()
        {
            Products = new HashSet<Product>();
        }

        [Key]
        [Column("CategoryID")]
        public int CategoryId { get; set; }
        [StringLength(15)]
        public string CategoryName { get; set; } = null!;
        [Column(TypeName = "ntext")]
        public string? Description { get; set; }
        [Column(TypeName = "image")]
        public byte[]? Picture { get; set; }

        [InverseProperty("Category")]
        public virtual ICollection<Product> Products { get; set; }
    }
}

この記事の本題の話は以上ですが、このクラス/プロパティ定義をそのまま ASP.NET MVC の Model として使った場合、ユーザー入力の検証がどうなるのかが気になります。それも調べましたので以下に書いておきます。

以前は string 型のプロパティに該当するテキストボックスへのユーザー入力を必須とする場合、その項目の当該プロパティに RequiredAttribute 属性を付与していました。それにより未入力の場合は検証 NG となってエラーメッセージが表示されます。

「Null 許容」オプションが有効化されている場合、string 型の項目は必須入力になるはずですが、上のコードではプロパティには RequiredAttribute 属性は付与されていません。そこはどうなるのかが疑問でした。

実際にアプリを動かして試してみると、RequiredAttribute 属性は付与されてなくても、未入力の場合は検証 NG となってエラーメッセージが表示されました。

View から生成される html ソースを調べてみると、当該 input 要素には data-val-required="The xxx field is required." という検証属性が付与され、未入力の場合は検証機能が働いてエラーメッセージが表示されるようになっていました。

エラーメッセージを任意のものに変えたい場合は、プロパティに RequiredAttribute を付与して ErrorMessage にメッセージを設定します。そうすると data-val-required 属性に設定される文字列が ErrorMessage に設定したものに置き換わります。

このあたりは先の記事「int 型プロパティの検証、エラーメッセージ」に書いた int 型の場合と同じになっているようです。

最後にもう一つ、こんなことをする人はいないかもしれませんが、string? 型のプロパティ(null 可)に RequiredAttribute 属性を付与(null 不可)するとどうなるかを書いておきます。

そのような設定をすると、EF Core を使って SQL Server からデータを取得する際当該列のデータに NULL が含まれていると、

System.Data.SqlTypes.SqlNullValueException: Data is Null. This method or property cannot be called on Null values.

・・・という例外がスローされます。下の画像を見てください。

SqlNullValueException

Microsoft のドキュメントによると、SqlNullValueException は「System.Data.SqlTypes 構造体の Value プロパティが null に設定されている場合にスローされる例外」ということだそうです。

メカニズムは不明ですが、string? 型のプロパティに RequiredAttribute 属性を付与し EF Core で SQL Server からデータを取得してくるときに、データに NULL があると「SqlTypes 構造体の Value プロパティが null に設定」ということになるようです。

SqlNullValueException の説明と RequiredAttribute 属性を設定したことが結びつかなくて、解決に悩んで無駄な時間を費やすことになるかもしれませんので注意してください。(実は自分がそうでした)

Tags: , , ,

ADO.NET

Code First で外部キープロパティの定義

by WebSurfer 11. September 2016 14:08

先に、Entity Framework Code First の機能を利用して SQL Server データベースに親子関係のあるテーブルを生成し、ASP.NET MVC4 で登録・編集・削除を行うという以下の 3 つの記事を書きました。

  1. MVC4 EF Code First(Entity Framework Code First の機能を利用して SQL Server 2008 Express に MVC4 アプリケーション用のテーブルを生成)
  2. 親子関係のあるデータ登録(Create アクションメソッド / ビューを追加して、上記 1 で作成したテーブルにデータを登録)
  3. 親子関係のあるデータの編集・削除(Edit および Delete アクションメソッド / ビューを追加して、上記 2 で登録したデータを編集・削除)

上の記事では、テーブル生成のベースとなる Child クラスに Parent クラスを参照するナビゲーションプロパティと外部キープロパティは定義していませんでした。(具体的には上記 1 の記事の Model のコードを見てください)

それに、Microsoft の文書「Code First の規約」に書いてある "依存オブジェクトを表す型には、ナビゲーションプロパティに加えて外部キープロパティを含めることをお勧めします" に従って、ナビゲーションプロパティと外部キープロパティを定義するとどのような影響があるかを書きます。

追加後のコードは以下の通りです。上で言う「依存オブジェクトを表す型」は Child クラスですので、それにナビゲーションプロパティと外部キープロパティを追加します。外部キープロパティは int 型とし、Children テーブルに生成される外部キーフィールドを NULL 不可に(連鎖削除を設定)します。

public class Child
{
    public int Id { get; set; }

    [Required(ErrorMessage = "{0} は必須")]
    [StringLength(5, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "Child Name")]
    public string Name { get; set; }

    // 外部キープロパティを追加
    // int 型なので外部キーフィールドは NULL 不可になる
    public int ParentId { get; set; }

    // ナビゲーションプロパティを追加
    public virtual Parent Parent { get; set; } 
}

その後、上記 1 の記事「MVC4 EF Code First」に書いた手順に従って、Controller と View を作って Controller のアクションメソッドを呼び出すと、以下とおり Parents テーブルと Children テーブルが自動的に生成されます。

生成されたテーブル

Child クラスへの int 型外部キープロパティ ParentId の追加により、Children テーブルの外部キーフィールドがプロパティ名と同名の ParentId となり NULL 不可に設定されたことが上記 1 の記事で作成したデータベースと異なる点です。

ASP.NET MVC4 アプリからこれらのテーブルへのデータの登録は、上記 2 の記事「親子関係のあるデータ登録」の手順どおりで可能です。

編集・削除は上記 3 の記事「親子関係のあるデータの編集・削除」とは若干異なってきます。注意すべき点を以下に箇条書きにします。

編集 (Edit)

  • Child クラスに ParentId プロパティが定義されているので ParentId の値もポストする必要がある。Id と同様に View に隠しフィールドを追加して対応する。そうしないと、モデルバインディングの際 ParentId プロパティにはゼロがバインドされ、db.Entry(postedParent).State を Modified に設定する時に参照整合性制約違反でエラーになる。
  • UpdateModel(db.Parents.Find(id)) の後 db.SaveChanges するのはエラーになる。Children テーブルの関連するレコードの外部キーフィールド ParentId を NULL に書き換え、ポストされたデータでレコードを新たに作り INSERT するという動きになるが、外部キーフィールド ParentId は NULL 不可なのでエラーになる。(先の記事の例のように外部キーフィールドが NULL 可であればエラーは出ない。外部キーフィールドが NULL の余計なレコードは残るが DB の整合性は保たれる)
  • db.Entry(親).State を Modified にしただけでは、先の記事の例と同様、親しか更新されない。コードを書いて子の State も Modified に設定する必要がある。(実は、連鎖削除だけでなく連鎖更新もされるのではと期待したがダメでした)
  • 親も子も無条件で State を Modified に設定すると、変更する必要のないレコードまで UPDATE されてしまう。先の記事の例ではそうコーディングしたが、考え直した方がよさそう。具体的な案としては、db.Parents.Find(id) で Parent オブジェクトを取得し、そのプロパティをポストされた値(アクションメソッドの引数 postedParent から取得)で一つ一つ書き換えるのがよさそう。そうすると、前の値と変更になった場合のみ自動的に Modified マークが付けられ、db.SaveChanges で更新される。(前の値と変わらなければ Unchanged マークのままなので更新されない) この方法を取れば、View の隠しフィールドで Id や ParentId をポストする必要もなくなる。

削除 (Delete)

  • Microsoft の文書「Code First の規約」によると、"依存エンティティの外部キーで null 値が許容されない場合、Code First はリレーションシップに連鎖削除を設定します" とのこと。外部キーフィールド ParentId が NULL 不可なので、フレームワークはリレーションシップに連鎖削除を設定しているはず。実際に削除を試してみると、親だけ Remove すれば連鎖的に子も削除されることを確認できた。(上記 3 の記事のように、子を Remove するのは不要)
  • 同じ Microsoft の文書には "依存エンティティの外部キーで null 値が許容される場合は、Code First はリレーションシップに連鎖削除を設定しないため、プリンシパルが削除されると外部キーが null に設定されます" と書いてあるが、上記 3 の記事(外部キーは NULL 可)で試した限りではそのようにはならない。子を持つ親を削除しようとすると FK 制約に引っかかって SqlException がスローされる。

Tags: ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後ブログ2を追加し、ここはプログラミング関係、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  October 2022  >>
MoTuWeThFrSaSu
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar