EntityFramework Core 3.1 にて、null許容参照型機能の有効・無効ごとに、NOT NULL制約の状況を確認してみた

EntityFramework Core (以降、EF Core)のドキュメントを見ていたところ、C#8.0以降のnull許容参照型機能により、NOT NULL制約の付き方が変わるようでした。
必須およびオプションのプロパティ-EF Core | Microsoft Docs

そこで今回、null許容参照型機能の有効・無効ごとに、どんな感じになるのかを試してみました。

 

目次

 

環境

前回の記事の環境を、今回も引き続き使います。

 

null許容参照型に対する設定について

null許容参照型機能の有効化/無効化は、次のうちのどちらかにより可能です。

  • プロジェクトファイル (*.csproj)
    • プロジェクト全体の設定を変更
  • nullable ディレクティブ
    • ディレクティブがある箇所の設定を変更

 

プロジェクトファイル (*.csproj) での設定

以下のドキュメントに記載があります。
null 許容参照型を使用して設計する | Microsoft Docs

csprojファイルの中にある

  • LangVersion
  • Nullable

を、以下のように指定することで、null許容参照型機能が有効になります。

<PropertyGroup>
  <TargetFramework>netcoreapp3.1</TargetFramework>
  <LangVersion>8</LangVersion>
  <Nullable>enable</Nullable>
</PropertyGroup>

 
ちなみに、Rider IDEを使っている場合には、csprojファイルのプロパティから変更できます。

csprojファイルを右クリックし、コンテキストメニューから Properties... を選択します。

f:id:thinkAmi:20191219231313p:plain:w400

 
ApplicationのLanguageの中に、設定箇所があります。

f:id:thinkAmi:20191219231358p:plain:w400

 

nullableディレクティブの場合

null許容参照型機能を一部だけ設定したい場合は、nullableディレクティブを使うことになります。
null 許容参照型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

つまり

#nullable enable
    public class RqBlog4
    {
        public int Id { get; set; }
        public string? Url { get; set; }
    }
    
    public class RqBlog5
    {
        public int Id { get; set; }
        public string Url { get; set; }
    }
#nullable restore

と書くと、RqBlog4・RqBlog5の中だけ、null許容参照型機能が有効になります。

 

ここまでで設定方法を見てきました。

次はnull許容参照型機能の有効/無効により、どのように動作が異なるかを確認してみます。

 

用意したモデルコード

以下の5種類を用意しました。

  • RqBlog1クラス
    • null許容参照型を使っていない場合
  • RqBlog2クラス
    • null許容参照型を使っていないが、 [Require] Data Annotationを使っている場合
  • RqBlog3クラス、 RqPost3クラス
    • null許容参照型を外部キーに使っていない場合
  • RqBlog4クラス
    • null許容参照型を使っている & #nullable enable を使っている場合
  • RqBlog5クラス
    • null許容参照型を使っていない & #nullable enable を使っている場合

 
実際のソースコードはこんな感じです。

フィールド RqBlog*.Url 、もしくは外部キーの RqPost3.RqBlogFk がどのように変化するかを見ます。

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

namespace EFCoreRelationSample.Models
{
    public class RqBlog1
    {
        public int Id { get; set; }
        public string Url { get; set; }
    }

    public class RqBlog2
    {
        public int Id { get; set; }
        
        [Required]
        public string Url { get; set; }
    }
    
    public class RqBlog3
    {
        [Key]
        public string Url { get; set; }
        
        // コレクションナビゲーションプロパティ
        public List<RqPost3> Posts { get; set; }
    }

    public class RqPost3
    {
        public int Id { get; set; }
        public string Content { get; set; }

        // 外部キー
        [ForeignKey("RqBlog3Url")]
        public string RqBlogFk { get; set; }
        
        // 参照ナビゲーションプロパティ
        public RqBlog3 Blog { get; set; }
    }

#nullable enable
    public class RqBlog4
    {
        public int Id { get; set; }
        public string? Url { get; set; }
    }
    
    public class RqBlog5
    {
        public int Id { get; set; }
        public string Url { get; set; }
    }
#nullable restore
    
}

 

null許容参照型機能を無効にしたままの場合(デフォルト)

デフォルトのままで、マイグレーションを作成・適用した時のSQLを見てみます。

f:id:thinkAmi:20191219231508p:plain:w400

 

null許容参照型を使っていない場合

Urlには NOT NULL制約がありません。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog1List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqBlog1List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );

 

null許容参照型を使っていないが、 [Require] Data Annotationを使っている場合

Urlに、NOT NULL制約が付きました。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog2List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqBlog2List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL
      );

 

null許容参照型を外部キーに使っていない場合

外部キーである RqBlogFk には、NOT NULL制約がありません。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog3List" (
          "Url" TEXT NOT NULL CONSTRAINT "PK_RqBlog3List" PRIMARY KEY
      );

# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqPost3List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqPost3List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "RqBlogFk" TEXT NULL,
          "BlogUrl" TEXT NULL,
          CONSTRAINT "FK_RqPost3List_RqBlog3List_BlogUrl" FOREIGN KEY ("BlogUrl") REFERENCES "RqBlog3List" ("Url") ON DELETE RESTRICT
      );

 

null許容参照型を使っている & #nullable enable を使っている場合

Urlには NOT NULL制約がありません。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog4List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqBlog4List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );

 

null許容参照型を使っていない & #nullable enable を使っている場合

nullableディレクティブの影響により、null許可参照型機能が有効となり、UrlにNOT NULL制約が付きました。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog5List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqBlog5List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL
      );

 

null許容参照型機能を有効にした場合

csprojファイルを編集し、null許容参照型機能を有効にしたあと、マイグレーションを作成・実行してみます。

f:id:thinkAmi:20191219231529p:plain:w400  

null許容参照型を使っていない場合

先ほどとは異なり、UrlにNOT NULL制約が付きました。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog1List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqBlog1List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL
      );

 

null許容参照型を使っていないが、 [Require] Data Annotationを使っている場合

こちらは変わらず、UrlはNOT NULL制約が付いたままでした。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog2List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqBlog2List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL
      );

 

null許容参照型を外部キーに使っていない場合

先ほどとは異なり、外部キー BlogUrl にNOT NULL制約が付きました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog3List" (
          "Url" TEXT NOT NULL CONSTRAINT "PK_RqBlog3List" PRIMARY KEY
      );

# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqPost3List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqPost3List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NOT NULL,
          "RqBlogFk" TEXT NOT NULL,
          "BlogUrl" TEXT NOT NULL,
          CONSTRAINT "FK_RqPost3List_RqBlog3List_BlogUrl" FOREIGN KEY ("BlogUrl") REFERENCES "RqBlog3List" ("Url") ON DELETE CASCADE
      );

 

null許容参照型を使っている & #nullable enable を使っている場合

先程と同じく、UrlにはNOT NULL制約はありませんでした。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog4List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqBlog4List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );

 

null許容参照型を使っていない & #nullable enable を使っている場合

先程と同じく、UrlにはNOT NULL制約が付いていました。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RqBlog5List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RqBlog5List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL
      );

 

以上より、null許容参照型機能を無効から有効に切り替えた場合は、実装方法によってはNULL許可からNOT NULL制約付与へと、意味が逆になるということが分かりました。

そのため、EF CoreにおけるNull 許容の参照型の使用については、

null 許容の参照型に対する EF Core のサポートについて説明し、それらを操作するためのベストプラクティスについて説明します。

Null 許容の参照型の使用-EF Core | Microsoft Docs

と書かれているページを参照すると良さそうです。

 

ソースコード

GitHubに上げました。 EFCoreRelationSample/Models/NullableFunction.cs が今回のファイルです。
https://github.com/thinkAmi-sandbox/ASP_Net_Core-sample

EntityFramework Core 3.1 を使って、色々な一対多のリレーションを試してみた

前回、EntityFramework Core (以下EF Core)を使って、一対多のリレーションがあるデータを作成してみました。
ASP .NET Core 3.1 + EntityFramework Core を使って、一対多のリレーションがあるデータをレスポンスしてみた - メモ的な思考的な

 
そんな中、公式ドキュメントを読んでいると、いろんなパターンの一対多のリレーションを定義する方法が記載されていました。
リレーションシップ-EF Core | Microsoft Docs

ただ、EF CoreからどんなSQLDDL文が発行されるのか分からなかったことから、今回試してみることにしました。

 

目次

 

環境

前回の記事の環境を、今回も引き続き使います。

 

規約による定義(単一ナビゲーションプロパティ)

親クラスに単一ナビゲーションプロパティを定義するだけで、リレーションができるのは楽です。

ただ、裏側ではどんなDDLが発行されるのか気になったため、試してみます。

 

前回同様、Blog <-> Postでリレーションを作成するためのモデル

// Blog
public class BlogOfSingleNavProp
{
    public int Id { get; set; }
    public string Url { get; set; }
    
    // 単一ナビゲーションプロパティ(Single Navigation Property)のみ定義
    public List<PostOfSingleNavProp> PostsOfFk { get; set; }
}

// Post
public class PostOfSingleNavProp
{
    public int Id { get; set; }
    public string Content { get; set; }
}

を用意します。

次に、マイグレーションを作成・DBへ適用します。

# マイグレーションの作成
$ dotnet ef migrations add AddSingleNavigationProperty

# DBへの適用
$ dotnet ef database update

 
dotnet ef database update 時に発行されるDDLがログに出力されるため、内容を確認します。

Postにフィールド BlogOfSingleNavPropId が自動生成され、Blogへの外部キーが設定されました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "BlogOfSingleNavProps" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_BlogOfSingleNavProps" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );
      
# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "PostOfSingleNavProps" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_PostOfSingleNavProps" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogOfSingleNavPropId" INTEGER NULL,   # 自動生成された列
          CONSTRAINT "FK_PostOfSingleNavProps_BlogOfSingleNavProps_BlogOfSingleNavPropId" 
              FOREIGN KEY ("BlogOfSingleNavPropId") 
              REFERENCES "BlogOfSingleNavProps" ("Id") 
              ON DELETE RESTRICT
      );
      
# 自動生成されたPostのIndex
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_PostOfSingleNavProps_BlogOfSingleNavPropId" 
          ON "PostOfSingleNavProps" ("BlogOfSingleNavPropId");

 
スペースの関係上、以降のパターンは

のみを記載します。

 

Data Annotation によるリレーションの定義

Data Annotationによるリレーションは、2つの方法が用意されています。

  • [ForeignKey]
  • [InverseProperty]

ともに、データアノテーション名前空間 System.ComponentModel.DataAnnotations.Schema で定義されています。
https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations.schema?view=netcore-3.1

今回はそれぞれ試してみます。

 

[ForeignKey] によるリレーション定義

[ForeignKey] の用途としては、主に外部キーの名前がEF Coreの命名規則と異なる場合に使うようです。

また、 [ForeignKey] は、以下のいずれかのパターンで付与します。

  • 依存エンティティ(子)の外部キーに付与
  • 依存エンティティ(子)の参照ナビゲーションプロパティに付与
  • プリンシパルエンティティ(親)の参照ナビゲーションプロパティに付与

なお、いずれも発行されるDDLは同じになります。

 

依存エンティティの外部キーに付与
public class Blog1
{
    public int Id { get; set; }
    public string Url { get; set; }
    public List<Post1> Posts { get; set; }
}

public class Post1
{
    public int Id { get; set; }
    public string Content { get; set; }

    // 依存エンティティの外部キーに付与
    [ForeignKey("Blog")]
    public int BlogFk { get; set; }
    
    // 参照ナビゲーションプロパティ
    public Blog1 Blog { get; set; }
}

 
結果です。 BlogFk の参照先が Blog.Id になっています。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog1List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog1List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );

# Post      
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post1List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post1List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogFk" INTEGER NOT NULL,
          CONSTRAINT "FK_Post1List_Blog1List_BlogFk" 
              FOREIGN KEY ("BlogFk") 
              REFERENCES "Blog1List" ("Id") 
              ON DELETE CASCADE
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post1List_BlogFk" ON "Post1List" ("BlogFk");

 

依存エンティティの参照ナビゲーションプロパティに付与
public class Blog2
{
    public int Id { get; set; }
    public string Url { get; set; }
    public List<Post2> Posts { get; set; }
}

public class Post2
{
    public int Id { get; set; }
    public string Content { get; set; }

    // 外部キー
    public int BlogFk { get; set; }

    // 参照ナビゲーションプロパティに付与
    [ForeignKey("BlogFk")]
    public Blog2 Blog { get; set; }
}

 
こちらも同じ結果となりました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog2List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog2List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );

# Post      
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post2List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post2List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogFk" INTEGER NOT NULL,
          CONSTRAINT "FK_Post2List_Blog2List_BlogFk" 
              FOREIGN KEY ("BlogFk") 
              REFERENCES "Blog2List" ("Id") 
              ON DELETE CASCADE
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post2List_BlogFk" ON "Post2List" ("BlogFk");

 

プリンシパルエンティティの参照ナビゲーションプロパティに付与
public class Blog3
{
    public int Id { get; set; }
    public string Url { get; set; }
    
    // コレクションナビゲーションプロパティ
    [ForeignKey("BlogFk")]
    public List<Post3> Posts { get; set; }
}

public class Post3
{
    public int Id { get; set; }
    public string Content { get; set; }

    public int BlogFk { get; set; }
    public Blog3 Blog { get; set; }
}

 

こちらも同じ結果となりました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog3List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog3List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL

# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post3List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post3List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogFk" INTEGER NOT NULL,
          CONSTRAINT "FK_Post3List_Blog3List_BlogFk" 
              FOREIGN KEY ("BlogFk") 
              REFERENCES "Blog3List" ("Id") 
              ON DELETE CASCADE
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post3List_BlogFk" ON "Post3List" ("BlogFk");

 

[InverseProperty] によるリレーション定義

[InverseProperty] の用途としては、一つの親クラスに対して、子クラスから複数の外部キーを定義する場合になります。

以下は、子の

  • Author
  • Contributor

という2つの項目が、それぞれ別の外部キー制約として定義する例です。

public class Post4
{
    public int Id { get; set; }
    public string Content { get; set; }

    public int AuthorId { get; set; }
    public User4 Author { get; set; }

    public int ContributorId { get; set; }
    public User4 Contributor { get; set; }
}

public class User4
{
    public int Id { get; set; }
    public string Name { get; set; }

    // Author向けのリレーション
    [InverseProperty("Author")]
    public List<Post4> AuthoredPosts { get; set; }
    
    // Contributor向けのリレーション
    [InverseProperty("Contributor")]
    public List<Post4> ContributedToPosts { get; set; }
}

 
結果です。

Postの

  • AuthorId
  • ContributorId

に対し、それぞれUserのIdが外部キー制約として設定されました。

# User
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "User4List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_User4List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL
      );
      
# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post4List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post4List" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "AuthorId" INTEGER NOT NULL,
          "ContributorId" INTEGER NOT NULL,
          CONSTRAINT "FK_Post4List_User4List_AuthorId" 
              FOREIGN KEY ("AuthorId") 
              REFERENCES "User4List" ("Id") ON DELETE CASCADE,
          CONSTRAINT "FK_Post4List_User4List_ContributorId" 
              FOREIGN KEY ("ContributorId") 
              REFERENCES "User4List" ("Id") ON DELETE CASCADE
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post4List_AuthorId" ON "Post4List" ("AuthorId");
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post4List_ContributorId" ON "Post4List" ("ContributorId");

 

外部キーで主キー以外のプロパティを参照する

主キーとは別のフィールドを、外部キーの参照先として設定したい場合です。

この時は、Fluent APIを使い、プリンシパルキープロパティを構成します。

プリンシパルキープロパティは、単一・複数あるため、それぞれ試してみます。

 

単一プリンシパルキーの構成

以下の例は、

  • 主キー:Blog5.Id
  • 外部キー:Post5.BlogUrl
  • 外部キーの参照先:Blog5.Url

として設定したい場合のモデルです。

public class Blog5
{
    public int Id { get; set; }      // 主キー
    public string Url { get; set; }  // 外部キーの参照先
    public string Description { get; set; }

    public List<Post5> Posts { get; set; }
}

public class Post5
{
    public int Id { get; set; }
    public string Title { get; set; }

    public string BlogUrl { get; set; }  // 外部キー
    public Blog5 Blog { get; set; }
}

 

Fluent APIは、DbContextの OnModelCreating() をオーバーライドして実装します。

modelBuilder.Entity<Post5>()
    .HasOne(m => m.Blog)  // 一側
    .WithMany(m => m.Posts)  // 多側
    .HasForeignKey(m => m.BlogUrl)  // 外部キーのフィールド
    .HasPrincipalKey(m => m.Url);   // 外部キーから参照する親のフィールド

 
結果です。

外部キーから参照する親のフィールド(Url)に対し、UNIQUE制約が追加されています。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog5List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog5List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL,
          "Description" TEXT NULL,
          CONSTRAINT "AK_Blog5List_Url"  // 制約が追加
              UNIQUE ("Url")
      );
      
# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post5List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post5List" PRIMARY KEY AUTOINCREMENT,
          "Title" TEXT NULL,
          "BlogUrl" TEXT NULL,
          CONSTRAINT "FK_Post5List_Blog5List_BlogUrl" 
              FOREIGN KEY ("BlogUrl") 
              REFERENCES "Blog5List" ("Url")  // 外部キーは親のUrl
              ON DELETE RESTRICT
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_Post5List_BlogUrl" ON "Post5List" ("BlogUrl");

 

複数プリンシパルキーの構成

複合プリンシパルキーは、複合外部キーとして親のフィールドを参照したい時に使います。

以下の例は、

  • 主キー:Blog6.Id
  • 複合外部キー
    • Post6.BlogUrlとPost6.Author
  • 複合外部キーの参照先
    • Post6.BlogUrl -> Blog6.Url
    • Post6.BlogAuthor -> Blog6.Author

として設定したい場合のモデルです。

public class Blog6
{
    public int Id { get; set; }         // 主キー
    public string Url { get; set; }     // BlogUrlの参照先
    public string Author { get; set; }  // BlogAuthorの参照先
    public string Description { get; set; }

    public List<Post6> Posts { get; set; }
}

public class Post6
{
    public int Id { get; set; }
    public string Title { get; set; }

    public string BlogUrl { get; set; }     // 複合外部キー
    public string BlogAuthor { get; set; }  // 複合外部キー
    public Blog6 Blog { get; set; }
}

 
単一の場合と同様、Fluent APIにて構成します。

modelBuilder.Entity<Post6>()
    .HasOne(m => m.Blog)
    .WithMany(m => m.Posts)
    .HasForeignKey(m => new {m.BlogUrl, m.BlogAuthor})
    .HasPrincipalKey(m => new {m.Url, m.Author});

 
結果です。

Blogには UNIQUE ("Url", "Author") という制約が追加されています。

Postにも、 FOREIGN KEY ("BlogUrl", "BlogAuthor") という外部キー制約が追加されています。

また、複合外部キーの場合、Indexの追加はありませんでした。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Blog6List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Blog6List" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NOT NULL,
          "Author" TEXT NOT NULL,
          "Description" TEXT NULL,
          CONSTRAINT "AK_Blog6List_Url_Author" 
              UNIQUE ("Url", "Author")
          
# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Post6List" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Post6List" PRIMARY KEY AUTOINCREMENT,
          "Title" TEXT NULL,
          "BlogUrl" TEXT NULL,
          "BlogAuthor" TEXT NULL,
          CONSTRAINT "FK_Post6List_Blog6List_BlogUrl_BlogAuthor" 
              FOREIGN KEY ("BlogUrl", "BlogAuthor") 
              REFERENCES "Blog6List" ("Url", "Author") 
              ON DELETE RESTRICT
      );
      
# Index:なし

 

ナチュラルキーの主キーに対する、外部キー制約

EF Coreでは、主キーをサロゲートキーではなくナチュラルキーでも定義できます。

そこで、ナチュラルキーの主キーに対する外部キー制約も試してみます。

 

単一外部キーの場合

以下のモデルは

  • 主キーは、Blog・Postともにナチュラルキー
    • Blogは Url
    • Postは Title
  • Postの外部キー( UrlFk )は、Blogの主キー(ナチュラルキー)である Url を参照している

という定義です。

public class NkBlog1
{
    [Key]
    public string Url { get; set; }

    public List<NkPost1> Posts { get; set; }
}

public class NkPost1
{
    [Key]
    public string Title { get; set; }
    public string Content { get; set; }

    [ForeignKey("Blog")]
    public string UrlFk { get; set; }
    public NkBlog1 Blog { get; set; }
}

 
このモデルをマイグレーションすると、以下のDDLが発行されました。

  • Blogの Url が主キーになる
  • Postの UrlFk がBlogの Url を参照した外部キーになる

ということが分かります。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "NkBlog1List" (
          "Url" TEXT NOT NULL CONSTRAINT "PK_NkBlog1List" PRIMARY KEY
      );

# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "NkPost1List" (
          "Title" TEXT NOT NULL CONSTRAINT "PK_NkPost1List" PRIMARY KEY,
          "Content" TEXT NULL,
          "UrlFk" TEXT NULL,
          CONSTRAINT "FK_NkPost1List_NkBlog1List_UrlFk" 
              FOREIGN KEY ("UrlFk") 
              REFERENCES "NkBlog1List" ("Url") 
              ON DELETE RESTRICT
      );
      
# Index
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_NkPost1List_UrlFk" ON "NkPost1List" ("UrlFk");

 

複合外部キーの場合

以下のモデルを使って

  • 主キーは、Blog・Postともにナチュラルキー
    • Blogは UrlAuthor
    • Postは UrlAuthorTitle
  • Postの外部キーは2つ(UrlとAuthor)で、Blogの主キー(ナチュラルキー)であるUrlとAuthorを参照している

を定義したいケースです。

public class NkBlog2
{
    public string Url { get; set; }     // 複合主キー(ナチュラルキー)
    public string Author { get; set; }  // 複合主キー(ナチュラルキー)

    public string Description { get; set; }
    
    public List<NkPost2> Posts { get; set; }

}

public class NkPost2
{
    public string Url { get; set; }     // 複合主キー(ナチュラルキー & 外部キー)
    public string Author { get; set; }  // 複合主キー(ナチュラルキー & 外部キー)
    public string Title { get; set; }   // 複合主キー(ナチュラルキー、これだけ外部キー参照なし)

    public string Content { get; set; }
    
    public NkBlog2 Blog { get; set; }
}

 
この場合、Fluent APIを使ってDbContextに定義します。

まずは、Blogの複合主キー(ナチュラルキー)の定義です。

// Blogの複合主キー(ナチュラルキー)
modelBuilder.Entity<NkBlog2>()
    .HasKey(m => new {m.Url, m.Author});

 
続いて、Blogの複合主キーを参照する、Postの複合外部キーの定義です。

// Postの複合外部キー
modelBuilder.Entity<NkPost2>()
    .HasOne(p => p.Blog)
    .WithMany(m => m.Posts)
    .HasForeignKey(m => new {m.Url, m.Author});

 
最後に、Postの複合主キーを定義します。

// Postの複合主キー
modelBuilder.Entity<NkPost2>()
    .HasKey(m => new {m.Url, m.Author, m.Title});

 
結果です。

Blogは CONSTRAINT "PK_NkBlog2List" PRIMARY KEY ("Url", "Author") と、主キーが複合ナチュラルキーになりました。

# Blog
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "NkBlog2List" (
          "Url" TEXT NOT NULL,
          "Author" TEXT NOT NULL,
          "Description" TEXT NULL,
          CONSTRAINT "PK_NkBlog2List" PRIMARY KEY ("Url", "Author")
      );

 
Postは

  • 複合主キー: PRIMARY KEY ("Url", "Author", "Title"),
  • 複合外部キー: FOREIGN KEY ("Url", "Author")

と定義されました。

# Post
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "NkPost2List" (
          "Url" TEXT NOT NULL,
          "Author" TEXT NOT NULL,
          "Title" TEXT NOT NULL,
          "Content" TEXT NULL,
          CONSTRAINT "PK_NkPost2List" 
              PRIMARY KEY ("Url", "Author", "Title"),
          CONSTRAINT "FK_NkPost2List_NkBlog2List_Url_Author"
              FOREIGN KEY ("Url", "Author") 
              REFERENCES "NkBlog2List" ("Url", "Author") 
              ON DELETE CASCADE
      );

なお、Indexの定義はありませんでした。

 

ソースコード

GitHubに上げました。

  • Models/BlogPostOfFkDataAnnotation.cs
  • Models/BlogPostOfInversePropDataAnnotation.cs
  • Models/BlogPostOfPrincipal.cs
  • Models/BlogPostOfSingleNavProp.cs
  • Models/NkBlogPostOfFk.cs
  • Models/NkBlogPostOfMultiFk.cs
  • Models/MyContext.cs

あたりが今回のファイルです。
https://github.com/thinkAmi-sandbox/ASP_Net_Core-sample

ASP .NET Core 3.1 + EntityFramework Core を使って、一対多のリレーションがあるデータをレスポンスしてみた

ASP.NET Core 3.1 + EntityFramework Coreを使ってアプリを書いた際、1モデルのデータを返す方法はチュートリアルにありました。

 

一方、一対多のリレーションがあるデータをどうやってレスポンスすれば良いか、最初のうちは分かりませんでした。

そのため、色々と試したことをメモしておきます。

 

目次

 

環境

 

環境構築

まずは、MacASP.NET Core プロジェクトを作るまでの環境構築です。

 

.NET Core SDKのインストール

.NET Core SDKをHomebrewで管理しようと調べてみたところ、caskにありました。

$ brew search dotnet-sdk
==> Casks
dotnet-sdk

 

そのため、caskを使ってインストールしました。

$ brew cask install dotnet-sdk
...
==> Linking Binary 'dotnet' to '/usr/local/bin/dotnet'.
🍺  dotnet-sdk was successfully installed!

 

これで dotnet コマンドが使えるようになりました。

$ dotnet --version
3.1.100

 
なお、SDKのバージョンアップする場合は、以下のコマンドで行います。

$ brew cask upgrade dotnet-sdk
...
==> Linking Binary 'dotnet' to '/usr/local/bin/dotnet'.
==> Purging files for version 3.0.101,1b9f265d-ba27-4f0c-8b4d-1fd42bd8448b:2bbd64abddeeea91149df3aa39d049ae of Cask dotnet-sdk
🍺  dotnet-sdk was successfully upgraded!

 

ソリューションとプロジェクトを作る

今回は、dotnetコマンドを使って作成します。

 

ソリューションファイルの作成
$ dotnet new sln

 

プロジェクトファイルの作成

今回は、 EFCoreRelationSample というプロジェクトを、Web APIテンプレートを使って作成します。

 

まずは用意されているテンプレートを確認します。今回は webapi を使えば良さそうです。

$ dotnet new --list
...

Templates                                         Short Name                                 
------------------------------------------------------------                            
ASP.NET Core Web App (Model-View-Controller)      mvc                              
...
ASP.NET Core Web API                              webapi
...

 

プロジェクトを作成し、ソリューションに追加します。

# プロジェクトを作成
$ dotnet new webapi -o EFCoreRelationSample

# ソリューションに追加
dotnet sln add ./EFCoreRelationSample

 

EntityFramework CoreまわりのNuGetパッケージを追加

まずはSDKのグローバルに、EntityFramework Core (以下、 EF Core )まわりのコマンドを追加します。
EF Core ツールリファレンス (.NET CLI)-EF Core | Microsoft Docs

$ dotnet tool install --global dotnet-ef
次のコマンドを使用してツールを呼び出せます。dotnet-ef
ツール 'dotnet-ef' (バージョン '3.1.0') が正常にインストールされました。

なお、 dotnet ef コマンドは .NET Core SDKに同梱されていません。
Announcing Entity Framework Core 3.1 and Entity Framework 6.4 | .NET Blog

 

続いて、EF Core関連のパッケージを追加します。

# プロジェクトのあるディレクトリへ移動
$ cd EFCoreRelationSample/

# 今回SQLiteを使うため、SQLiteパッケージを追加
$ dotnet add package Microsoft.EntityFrameworkCore.SQLite

# マイグレーションなどのために追加
$ dotnet add package Microsoft.EntityFrameworkCore.Design

 
なお、 Microsoft.EntityFrameworkCore.SQLite の制限内容はこちら。 SQLite Database Provider - Limitations - EF Core | Microsoft Docs

 
また、Microsoft.EntityFrameworkCore.Design を追加する目的はこちら。
c# - Purpose of package "Microsoft.EntityFrameworkCore.Design" - Stack Overflow

 

ここまでで、ASP.NET Core & EF Coreの環境ができました。

 

モデルまわりの作成

今回使用するモデルまわりを作成します。

 

モデルの作成

今回作成する一対多のリレーションを持つモデルとして、BlogとPostを使います。公式チュートリアルとほぼ同じ型です。
リレーションシップ-EF Core | Microsoft Docs

 

テンプレートにはモデルを入れるディレクトリがないため、

  • Controllersと同列に Models というディレクトリを作成
  • Modelsの中にモデルファイル BlogPostOfFk.cs を作成

とします。

 

2つのモデルのリレーションについて、今回はナビゲーションプロパティを使って定義します。

EF Coreの規則として、親側にだけ定義すれば、自動でリレーションが貼られるようです。
https://docs.microsoft.com/ja-jp/ef/core/modeling/relationships?tabs=data-annotations#single-navigation-property

 
ただ、後述のControllerにて親から子をたぐる処理を書く際、ナビゲーションプロパティが定義されていない状態ではコンパイルが通りませんでした。

var blog = await _context.BlogOfFks
    .Include(p => p.PostsOfFk)  // この p.PostOfFk が参照できなくてエラー
    .ToListAsync();
return Ok(blog);

 
そのため、以下のように明示的に定義します。なお、外部キーについては定義しなくても動作するようです。

using System.Collections.Generic;

namespace EFCoreRelationSample.Models
{
    public class BlogOfFk
    {
        public int Id { get; set; }
        public string Url { get; set; }
        
        public List<PostOfFk> PostsOfFk { get; set; }
    }

    public class PostOfFk
    {
        public int Id { get; set; }
        public string Content { get; set; }

        public BlogOfFk BlogOfFk { get; set; }
    }
}

 

DbContextの作成

モデルができたので、次はDBと接続するためのDbContextを作成します。

DbContextの適切な置き場が分からなかったため、今回は Models の下に MyContext.cs として作成しました。

DbContextを継承した MyContext クラスを作成し、

  • コンストラク
  • DbSet型のpublicプロパティ

を用意します。

using Microsoft.EntityFrameworkCore;

namespace EFCoreRelationSample.Models
{
    public class MyContext : DbContext
    {
        public MyContext(DbContextOptions<MyContext> options) : base(options) {}
        
        // モデルクラスへのアクセス (DbSet<TModel>) 型のpublicプロパティ
        public DbSet<BlogOfFk> BlogOfFks { get; set; }
        public DbSet<PostOfFk> PostOfFks { get; set; }
    }
}

 

接続文字列の作成 (appsettings.json)

次に、SQLiteへの接続するための接続文字列を用意します。

今回は、プロジェクトルート直下にある appsettings.json の中に記載します。

接続するSQLiteは、プロジェクトルート直下の WebApplication.db とします。

{
  // ...
  "AllowedHosts": "*",
  
  // 以下を追加
  "ConnectionStrings": {
    "MyContext": "Data Source=./WebApplication.db"
  }
}

なお、SQLiteファイルはEF Coreのマイグレーションにより自動生成されます。

 

Startup.csに、DbContextと接続文字列を追加

今回使用するDbContextと接続文字列を追加します。

SQLiteへ接続するため、 UseSqlite() を使います。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // 以下を追加
    services.AddDbContext<MyContext>(options => options.UseSqlite(
        Configuration.GetConnectionString("MyContext")));
}

 

マイグレーションの作成

モデルまわりができたため、EF Coreによるマイグレーションを作成します。

 

マイグレーションファイルの作成

以下のコマンドを実行すると、 Migrations ディレクトリの中にマイグレーションファイルが作成されます。

$ dotnet ef migrations add InitialCreate

今回は

  • MyContextModelSnapshot.cs
  • <タイムスタンプ>_InitialCreate.cs

の2ファイルが作成されました。

 

データベースへの反映

マイグレーションファイルの内容をSQLiteへ反映します。

$ dotnet ef database update

 
実行時のログより、BlogOfFk・PostOfFk モデルを元に、2つのテーブルを作成するSQLが発行されたことが分かります。

BlogOfFk

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "BlogOfFks" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_BlogOfFks" PRIMARY KEY AUTOINCREMENT,
          "Url" TEXT NULL
      );

 

PostOfFk

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "PostOfFks" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_PostOfFks" PRIMARY KEY AUTOINCREMENT,
          "Content" TEXT NULL,
          "BlogOfFkId" INTEGER NULL,
          CONSTRAINT "FK_PostOfFks_BlogOfFks_BlogOfFkId" FOREIGN KEY ("BlogOfFkId") 
              REFERENCES "BlogOfFks" ("Id") ON DELETE RESTRICT
      );

モデルにはナビゲーションプロパティだけ定義したものの、子モデルの外部キー BlogOfFkId が自動生成されました。

一方、モデルに定義した

  • コレクションナビゲーションプロパティ (BlogOfFk)
  • 参照ナビゲーションプロパティ (PostsOfFk)

は、テーブルのフィールドとして作成されませんでした。

 

初期データの投入

初期データを投入するため、データのシード処理を実装します。
データシード処理-EF Core | Microsoft Docs

投入方法はいくつかあるようですが、今回はDbContextの中で実装します。

OnModelCreating() メソッドをオーバーライドして実装します。

namespace EFCoreRelationSample.Models
{
    public class MyContext : DbContext
    {
        // ...
        
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // データのシードを定義
            // 親データ
            modelBuilder.Entity<BlogOfFk>().HasData(
                new {Id = 1, Url = "https://example.com/foo"},
                new {Id = 2, Url = "https://example.com/bar"}
            );
            
            // 子データ
            modelBuilder.Entity<PostOfFk>().HasData(
                new {Id = 1, Content = "ふー", BlogOfFkId = 1},
                new {Id = 2, Content = "ばー", BlogOfFkId = 1}
            );
        }
    }
}

 

初期データを投入するため、

  • 先ほど作成したマイグレーションファイル (Migrations/ の下) を削除
  • SQLite (WebApplication.db) を削除
  • dotnet ef migrations add InitialCreate
  • dotnet ef database update

を実行したところ

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO "BlogOfFks" ("Id", "Url")
      VALUES (1, 'https://example.com/foo');
      
...

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO "PostOfFks" ("Id", "BlogOfFkId", "Content")
      VALUES (1, 1, 'ふー');

などのログが表示され、シード処理が行われていることが確認できました。

 
Rider IDEで確認しても、以下のようにデータが存在しています。

BlogOfFk

f:id:thinkAmi:20191217221650p:plain:w400

 

PostOfFk

f:id:thinkAmi:20191217221704p:plain:w400

 

EF Coreから発行したSQLをコンソールへ出力

コントローラを作成する前に、EF Coreで発行したSQLをコンソールで確認する設定を追加しておきます。

基本的には以下の記事の通りですが、.NET Core 3.1版では少々変更がありました。
【C#】Entity Framework Core の SQL の実行ログを確認する - Qiita

 

NuGetパッケージを追加

コンソールへ出力するためのNuGetパッケージを追加します。

$ dotnet add package Microsoft.Extensions.Logging.Console

 

DbContextへ追加

DbContextへ

  • staticなreadonlyフィールドを追加
  • OnConfiguring()をオーバーライドして実装

を追加します。

ただ、.NET Core 3.1ではLoggerFactoryの追加方法が変わったようです。

 

そのため

  • LoggerFactory.Create()で生成
  • AddFilter()で、 DbLoggerCategory.Database.Command.Name を対象にする
  • AddConsole()で、コンソールへ出力

とします。

static readonly ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddFilter(DbLoggerCategory.Database.Command.Name, LogLevel.Information)
        .AddConsole();
});

// UseLoggerFactory で上記のロガーファクトリを登録する。
// Insert 文の値をロギングするために EnableSensitiveDataLogging を許可する。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
    optionsBuilder
        .EnableSensitiveDataLogging()
        .UseLoggerFactory(loggerFactory);

 

コントローラの作成

あとはコントローラを作成すれば完成です。

 

失敗:Include()を使用したリレーションデータのレスポンス

コントローラに必要な

  • DbContextフィールド
  • コンストラクタで、DbContextフィールドへの設定
  • HTTP GETでアクセスした時のメソッド

を実装します。

以下は、 /BlogPost/p2c にアクセスした時に、BlogOfFkモデルからPostOfFkモデルをたぐって表示する例となります。

namespace EFCoreRelationSample.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class BlogPostController : ControllerBase
    {
        private readonly MyContext _context;

        public BlogPostController(MyContext context)
        {
            this._context = context;
        }

        // 1 -> N の順でたぐる場合
        [HttpGet("p2c", Name = "Parent2Child")]
        public async Task<IActionResult> P2C()
        {
            var blog = await _context.BlogOfFks
                .Include(p => p.PostsOfFk)
                .ToListAsync();
            return Ok(blog);
        }
    }
}

 

Rider IDEで実行し、ブラウザで https://localhost5000/api/BlogPost/p2c へアクセスすると

An unhandled exception occurred while processing the request.
JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32.

というエラーが表示されました。

 
おそらく、モデルに

  • コレクションナビゲーションプロパティ (BlogOfFk)
  • 参照ナビゲーションプロパティ (PostsOfFk)

の2つが実装されているため、循環してしまったものと考えられました。

 

対応ですが、ASP.NET Core 2系では

のように、 SerializerSettings.ReferenceLoopHandling を設定すれば良いようでした。

 
ただ、ASP.NET Core 3系では、JSONライブラリに変更が入りました。

その影響で、SerializerSettings.ReferenceLoopHandling を設定するには、NuGetパッケージ Microsoft.AspNetCore.Mvc.NewtonsoftJson を追加する必要がありそうでした。

 
NewtonsoftJson の追加も考えましたが、今回は AutoMapper を使うことで対応することにしました。

 

AutoMapperを使った、リレーションデータのレスポンス

AutoMapperを使うにあたり、以下の記事が参考になりました。
ASP.NET CoreでAutoMapperを使う - ryuichi111stdの技術日記

ただ、ASP.NET Core3.1では少々変更されていました。

そのため、上記記事とstackoverflowを参考に

  • NuGetパッケージの追加
  • DTOクラスを追加
  • Profileクラスを追加
  • Startup.csへの追加
  • コントローラへの追加

を行いました。
c# - How to set up Automapper in ASP.NET Core - Stack Overflow

 

NuGetパッケージの追加

必要なNuGetパッケージを追加します。

# AutoMapper本体を追加
dotnet add package AutoMapper

# 拡張も入れる
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

 

DTOクラスを追加

ブラウザ側へ返す時のデータ型となるDTOクラスを追加します。

今回は DTO/BlogPostOfFKDTO.cs として追加しました。

また、DTOのプロパティ名はモデルと同一としました。

using System.Collections.Generic;
using EFCoreRelationSample.Models;

namespace EFCoreRelationSample.DTO
{
    public class PostOfFkDTO
    {
        // Postのフィールド
        public int Id { get; set; }
        public string Content { get; set; }
        
        // FKのフィールド
        public BlogOfFk BlogOfFk { get; set; }
    }

    public class BlogOfFkDTO
    {
        // Blogのフィールド
        public int Id { get; set; }
        public string Url { get; set; }

        // FK(Post)のフィールド
        public List<PostOfFk> PostsOfFk { get; set; }
    }   
}

 

Profileクラスを追加

次に、モデルとDTOを結びつけるProfileクラスを追加します。

置き場所が分からなかったため、今回は DTO/MyProfile.cs として作成しました。

Profileクラスでは CreateMap() を使って、モデルとDTOを結びつけます。

using AutoMapper;
using EFCoreRelationSample.Models;

namespace EFCoreRelationSample.DTO
{
    public class MyProfile : Profile
    {
        public MyProfile()
        {
            CreateMap<BlogOfFk, BlogOfFkDTO>();
            CreateMap<PostOfFk, PostOfFkDTO>();
        }
    }
}

 

Startup.csへの追加

ConfigureServices() に、 AddAutoMapper() を追加します。

public void ConfigureServices(IServiceCollection services)
{
    ...
    // 【追加】AutoMapperを追加
    services.AddAutoMapper(typeof(MyProfile));
}

 

コントローラへの追加

まず、

  • IMapper型のフィールドを追加
  • コンストラクタに IMapper 型の引数を追加

を行います。

public class BlogPostController : ControllerBase
{

    private readonly IMapper _mapper;

    public BlogPostController(MyContext context, IMapper mapper)
    {
        this._context = context;
        this._mapper = mapper;
    }
    ...
}

 

続いてコントローラのメソッドを書き換えます。

ただ、今回はAutoMapper有無を比較できるよう P2CWithAutoMapper() を新しく作成してみました。

AutoMapperを使う場合は、 ProjectTo() を追加することで、DTOへの変換を行っています。

[HttpGet("p2c-auto", Name = "Parent2ChildWithAutoMapper")]
public async Task<IActionResult> P2CWithAutoMapper()
{
    var blogs = await _context.BlogOfFks
        .Include(p => p.PostsOfFk)
        .ProjectTo<BlogOfFkDTO>(_mapper.ConfigurationProvider)  // 追加
        .ToListAsync();
    return Ok(blogs);
}

ちなみに、同じような機能に Map() もあります。 ProjectTo() との違いは、こちらが参考になりました。
LINQ to Entities の遅延評価と AutoMapper が便利という話 - Qiita  

Rider IDEASP.NET Coreを起動した時の動作を変更

あとは確認するだけです。

ただ、Rider IDEASP.NET Coreを起動した時の動作を変更したいため、 Properties/launchSettings.json を修正します。

  • launchBrowser で、起動した時にブラウザも自動で開くか
  • launchUrl で、ブラウザで最初にアクセスするURLを指定

などができます。

"EFCoreRelationSample": {
  "commandName": "Project",
  "launchBrowser": true,
  "launchUrl": "api/BlogPost",
  "applicationUrl": "https://localhost:5001;http://localhost:5000",
  "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development"
  }
}

 

動作確認

ASP.NET Coreアプリを起動し、ブラウザで https://localhost5000/api/BlogPost/p2c-auto を開くと、リレーションデータのJSONが表示されました。

f:id:thinkAmi:20191217221737p:plain

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/ASP_Net_Core-sample

なお、マイグレーションファイルは自動生成されたものなので、GitHubには含めていません。

Pythonで、unittest.mock.patchを使って定数を差し替える

小ネタです。

以前、 unittest.mock.patch を使ってデコレータを差し替えたことがありました。
Pythonで、unittest.mock.patchを使ってデコレータを差し替える - メモ的な思考的な

 
今回は、unittest.mock.patch を使って定数を差し替えてみます。

 
目次

 

環境

  • Python 3.7.5
  • pytest 5.3.1
    • テストランナーとしてのみ、使用

 

定数の差し替え

デコレータの時と同様、今回の定数の差し替えでも unittest.mock.patch() を使います。
https://docs.python.org/ja/3/library/unittest.mock.html#unittest.mock.patch

 
デコレータや関数の場合は、 side_effectreturn_value を使いました。

ただ、定数の場合は単にオブジェクトの差し替えだけのため、第2引数(もしくは new キーワード引数)で、差し替え後の値を渡してあげます。

 
たとえば、 target.py というファイルに、以下のようなプロダクションコードがあるとします。

DEBUG = True

def get_mode():
    return 'デバッグ' if DEBUG else 'プロダクション'

これに対し、定数 DEBUGFalse に差し替えたテストコードを書きたいとします。

 
この場合、

from target import get_mode
from unittest.mock import patch


@patch('target.DEBUG', False)
def test_production():
    actual = get_mode()
    assert actual == 'プロダクション'


@patch('target.DEBUG', new=False)
def test_production_with_new_keyword():
    actual = get_mode()
    assert actual == 'プロダクション'

とすることで、定数 DEBUG の値が TrueからFalseへと差し替わります。

 
また、もしテストの一時期だけDEBUGの値を差し替えたいという場合には、patchをwith文とともに使います。

def test_patch_using_with():
    # with文の中では、定数の値がFalseになる
    with patch('target.DEBUG', False) as p:
        actual1 = get_mode()
        assert actual1 == 'プロダクション'

    # with文を抜けると、定数の値がTrueに戻る
    actual2 = get_mode()
    assert actual2 == 'デバッグ'

 

その他参考

unit testing - How to patch a constant in python - Stack Overflow

 

ソースコード

Githubに上げました。ディレクトe.g._patch_constant が今回のものです。
https://github.com/thinkAmi-sandbox/python_mock-sample

ついカッとなって、飲み会座席のくじ引きアプリを作った話

この記事は、 JSL (日本システム技研) Advent Calendar 2019 - Qiita 12/2の記事です。

昨年同様、今年もJSLメンバーによるアドベントカレンダーをお届けします。

 
今回の記事は、ついカッとなって作成したアプリについて、ゆるく書いていきます。

目次

 

作ったもの

歓迎会の前日の雑談で「自分はなぜか最近の飲み会で役付きの方の隣が多いですー」という話題になりました。

そこで、ついカッとなって、翌朝までに、飲み会座席のくじ引きアプリを作ることにしました。

 

作成するまでの流れ

まず、欲しい機能としては

  • 自分の社員番号を入力したら、席番号を表示する
  • ランダムで席番号を割り当てる
  • 一回表示した社員番号の場合は、同じ席番号を表示する
  • 存在しない社員番号の場合は、エラーを表示する

があれば良さそうでした。

 
そこで、Python & Python製WebフレームワークのDjangoを使うことにより、各機能を実現できそうでした。

 

一方、最低限動けばいいだろうと考え、

あたりは、がんばらない方針としました。

 
機能と実現方法さえ決まれば後は実装するだけなので、夕食後に実装し始めて22時半くらいには完成しました。

ただ、翌朝までという時間制限を加えることで、ハッカソン的になったことから

と、作るだけで満足してしまいました。。。

 

アプリの画像

トップ

f:id:thinkAmi:20191202184442p:plain:w200

 

座席決定

f:id:thinkAmi:20191202184457p:plain:w200

 

存在しない場合

f:id:thinkAmi:20191202184514p:plain:w200

 

社内への公開から運用へ

満足したとはいえ、お蔵入りするのもなーと迷う気持ちがありました。

社内Slackでは分報を運用しているため、気軽に

最近の飲み会、くじ引きから自由席になった結果、なぜか役付きの人の近くにしか行けず。。。 ランダム性がほしいと思って、昨夜ついカッとなってくじ引きアプリを作ったけど、アプリ作ったら満足してしまった。。。

と書いてみました。

するとありがたいことに、@_sin_tanaka より

一旦ランダムでやってみる感じでいいと思います!せっかく作ったものなので!

と後押しがありました。

そこで、社内の雑談チャンネルにて

f:id:thinkAmi:20191202183005p:plain:w450

と書いたところ、ありがたいことに @chinoppy0727 の手により運用してもらえることになりました。

 

とはいえ、上記の通り、かなり機能を絞ったまま公開してしまったため

  • アプリ仕様や使い方のドキュメントが無い
  • 社員場号だけが使われるので、社員番号が不明だと、誰か分からない
  • Django adminによる管理画面作ってなかった
    • fixtureは用意していた
  • 「実際の物理席に、座席番号を書いて置く必要がある」という制約

など、かなり運用はつらい感じだったと思います。。。

 

運用に乗ってみて

ランダムで座席を決めているはずなのに、役付の人のまわりには変わり映えしないメンバーが集まるという、現実不具合がありました*1

そのため、歓迎会が始まるまではつらい気持ちになりました*2

 
とはいえ、自分が作ったものが使われると嬉しかったこともあり、カッとなって作るというのもたまには良いなーと感じました。

あと、今年度は開発合宿もあるので、このようなものがまた作れるかもしれないのも楽しみです。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi/nomikai_kuji

*1:自分はおすそわけできた

*2:歓迎会で飲み始めたら忘れました

AWS CDKで、cdk deployしたらエラー「NoSuchBucket: The specified bucket does not exist」

先日、S3に不要なバケットがたまっていたため、バケットを全削除をするスクリプトを作成・実行しました。

その後、AWS CDK + Pythonで、 cdk deploy したところ、

 ❌  <your_backet_name> failed: NoSuchBucket: The specified bucket does not exist
The specified bucket does not exist

とエラーになり、AWS CDKを使ったデプロイができなくなりました。

 
この時の対応でしばらく悩んだため、メモを残します。

 

目次

 

環境

 

調査と対応

前述の通りバケットを全削除してしまったため、必要なバケット <your_backet_name> が無いせいかと考えました。

そこで、手動でエラーが出ている名称のバケットを作成したものの、エラーは変わりませんでした。

これにより、他にも消してはマズいバケットを消してしまったのかなと考えました。

 

CloudFormationのスタックを見たところ、 CDKToolkit という気になるスタックがありました。

スタックの説明には

The CDK Toolkit Stack. It was created by `cdk bootstrap` and manages resources necessary for managing your Cloud Applications with AWS CDK.

とありました。

Getting Startedページに書かれていないため忘れていましたが、AWS CDKでデプロイする前に、 cdk bootstrap をやっていたことを思い出しました。
Getting Started With the AWS CDK - AWS Cloud Development Kit (AWS CDK)

 

そこで、改めて cdk bootstrap したところ、エラーが変わりました。

$ cdk bootstrap
 ⏳  Bootstrapping environment aws://<account>/<region>...
CDKToolkit: creating CloudFormation changeset...
 0/2 | 8:22:48 PM | UPDATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket 
0/2 Currently in progress: StagingBucket
 1/2 | 8:24:34 PM | UPDATE_FAILED        | AWS::S3::Bucket | StagingBucket The specified bucket does not exist (Service: Amazon S3; Status Code: 404; Error Code: NoSuchBucket; Request ID: xxx; S3 Extended Request ID: xxx)
 1/2 | 8:24:34 PM | UPDATE_ROLLBACK_IN_P | AWS::CloudFormation::Stack | CDKToolkit The following resource(s) failed to update: [StagingBucket]. 
 ❌  Environment aws://<account>>/<region> failed bootstrapping: Error: The stack named CDKToolkit is in a failed state: UPDATE_ROLLBACK_COMPLETE
The stack named CDKToolkit is in a failed state: UPDATE_ROLLBACK_COMPLETE

 

ただ、新規で cdk bootstrap を行ったつもりが、ログには UPDATE_IN_PROGRESS と出ていました。

そのため、もしかしたら、スタック CDKToolkit が存在しているせいかもしれないと考えました。

スタック CDKToolkit について、AWSドキュメントの記載は以下です。

Bootstrapping your AWS Environment
Before you can use the AWS CDK you must bootstrap your AWS environment to create the infrastructure that the AWS CDK CLI needs to deploy your AWS CDK app. Currently the bootstrap command creates only an Amazon S3 bucket.

You incur any charges for what the AWS CDK stores in the bucket. Because the AWS CDK does not remove any objects from the bucket, the bucket can accumulate objects as you use the AWS CDK. You can get rid of the bucket by deleting the CDKToolkit stack from your account.

https://docs.aws.amazon.com/cdk/latest/guide/tools.html

 

そこで、スタック CDKToolkit を削除した後、 cdk bootstrap を行ったところ、成功しました。

$ cdk bootstrap
 ⏳  Bootstrapping environment aws://<account>/<region>...
CDKToolkit: creating CloudFormation changeset...
 0/2 | 8:31:37 PM | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket 
 0/2 | 8:31:39 PM | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket Resource creation Initiated
 1/2 | 8:32:01 PM | CREATE_COMPLETE      | AWS::S3::Bucket | StagingBucket 
 2/2 | 8:32:03 PM | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit 
 ✅  Environment aws://<account>/<region> bootstrapped.

 

次に、 cdk deploy してみたところ、デプロイが成功しました。

$ cdk deploy
<your_stack>: deploying...
<your_stack>: creating CloudFormation changeset...
 0/3 | 8:33:12 PM | CREATE_IN_PROGRESS   | AWS::IAM::User     | xxx (xxx) 
 0/3 | 8:33:12 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata 
 0/3 | 8:33:13 PM | CREATE_IN_PROGRESS   | AWS::IAM::User     | xxx (xxx) Resource creation Initiated
 0/3 | 8:33:14 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
 1/3 | 8:33:14 PM | CREATE_COMPLETE      | AWS::CDK::Metadata | CDKMetadata 

 ✅  <your_stack>

 

これより、AWS CDKを使っている場合は、S3のバケット CDKToolkit を削除してはいけないことが分かりました。

DjangoのListViewで、対象データがない場合は404ページを表示する

DjangoでListViewを使う場合、デフォルトではモデルにデータがない時は HTTP200 で、データがない状態で表示されます。

f:id:thinkAmi:20191029062825p:plain:w400

 
ただ、モデルにデータがない場合に HTTP404 を表示したい時はどうするか、調べたことをメモしておきます。

 

目次

 

環境

 

調査

公式ドキュメントにより、ListViewが継承している MultipleObjectMixin が持つ allow_empty を使えば良さそうでした。

 
stackoverflowにも、同じような回答がありました。
Django: get_object_or_404 for ListView - Stack Overflow  
 
そこで、以下のような

  • urls.py
  • models.py
  • todo_list.html

を用意し、 allow_empty の挙動を試してみます。

urls.py

urlpatterns = [
    path('', NotFoundListView.as_view(), name='todo_list'),
]

models.py

from django.db import models


class Todo(models.Model):
    title = models.CharField(max_length=100)
    content = models.CharField(max_length=255)

todo_list.html

<body>
<main class="container">
    <table>
        <thead>
        <tr>
            <th>ID</th>
            <th>タイトル</th>
            <th>内容</th>
        </tr>
        </thead>
        <tbody>
        {% for todo in todo_list %}
            <tr>
                <td>{{ todo.id }}</td>
                <td>{{ todo.title }}</td>
                <td>{{ todo.content }}</td>
        </tbody>
    </table>
    {% endfor %}
</main>
</body>

 

allow_empty=Trueの場合

使うListViewはこんな感じです。

from django.views.generic import ListView
from listview404.models import Todo


class NotFoundListView(ListView):
    allow_empty = True  # デフォルトのまま
    model = Todo

 

結果はこちら。HTTP200です。

f:id:thinkAmi:20191029062825p:plain:w400

 

allow_empty=Falseの場合

allow_empty のみ変更します。

from django.views.generic import ListView
from listview404.models import Todo


class NotFoundListView(ListView):
    allow_empty = False  # 変更
    model = Todo

 

HTTP404 が表示されました。

f:id:thinkAmi:20191029062859p:plain:w400

 

ソースコード

Githubに上げました。 listview404 が今回のアプリです。
https://github.com/thinkAmi-sandbox/Django22_generic_view-sample