EntityFramework Core 3.1で、ClientSetNullの挙動を確認してみた

EntityFramework Core(以降、EF Core)の外部キーの ON DELETE について調べたところ、 ClientSetNull の存在を知りました。
連鎖削除 - EF Core | Microsoft Docs

いつからこのような設定があるのか調べたところ、EF Core 2.0から導入されたようです。
EF Core 2.0の非互換的変更

stackoverflowを読んだところ、MS SQL ServerのON DELETEまわりでエラーが出なくなるようでした。
cascade - EF Core - why ClientSetNull is default OnDelete behavior for optional relations (rather than SetNull) - Stack Overflow

そこで今回、ClientSetNullの挙動を確認してみました。

目次

 

環境

 
なお、今回の確認は

  • Consoleアプリケーションを作成
  • そのテストコードで、ClientSetNullを使って確認

としました。

また、Dockerを用意し、SQL ServerPostgreSQLとでOn Deleteまわりの挙動の違いを確認してみます。

 

長いのでまとめ

  • ClientSetNullは ON DELETE NO ACTION だが、Nullが設定される
  • SQL Serverの場合、外部キーの状況によってはSetNullではなくClientSetNullを使う
    • multiple cascade pathsな関係を持つモデルなど
  • データベースによっては、multiple cascade pathsな関係を持つモデルも利用可能
  • On Deleteを動作させるには、関係するモデルをInclude()で読み込んでおく

 

環境構築

プロジェクト作成まで

dotnetコマンドで作成していきます。

# EFCoreSampleディレクトリの中に、slnファイルを作成
$ dotnet new sln -o EFCoreSample
cd EFCoreSample/

# MyAppプロジェクトにConsoleアプリケーションを作成
dotnet new console -o MyApp

# ソリューションに、MyAppプロジェクトを追加
dotnet sln add ./MyApp/

# プロジェクトディレクトリへ移動
cd MyApp/

 
また、Consoleアプリケーションテンプレートでは、csprojファイルのLanguage Versionが 7.3 になっていたため、 8.0 へと変更しておきます。

Riderであれば、 csproj ファイルで右クリック > コンテキストメニューProperties > ApplicationのLanguageから変更できます。

 

データベースまわりのNuGetパッケージを追加

SQLServer向けにEF Core関係のパッケージを追加します。

# マイグレーション向けのツールを追加
$ dotnet add package Microsoft.EntityFrameworkCore.Design

# SQLServer向け
$ dotnet add package Microsoft.EntityFrameworkCore.SqlServer

 
続いて、PostgreSQL向けをインストールします。

Npgsql(.NET Access to PostgreSQL)で提供しているパッケージを使います。

$ dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

 
なお、今回はVisualStudioではなくRiderを使うため、パッケージマネージャーツール(Microsoft.EntityFrameworkCore.Tools)は不要です。
Entiy Framework Core のインストール - EF Core | Microsoft Docs

 

ログまわりのNuGetパッケージを追加

EF CoreからSQLが実行された時の内容を知りたいため、ログへの出力を考えます。

xUnit.netを使う場合、デフォルトではテストが並列で実行されるため、 Console.WriteLine ではなく ITestOutputHelper を使うとあります。

If you used xUnit.net 1.x, you may have previously been writing output to Console, Debug, or Trace. When xUnit.net v2 shipped with parallelization turned on by default, this output capture mechanism was no longer appropriate; it is impossible to know which of the many tests that could be running in parallel were responsible for writing to those shared resources. Users who are porting code from v1.x to v2.x should use one of the two new methods instead.

Capturing Output > xUnit.net

 
ITestOutputHelper での出力先はOutputとなるため、SQLの実行ログをOutputへ出力する方法を探してみました。

以下のstackoverflowはあったものの、うまく実装できませんでした。
c# - Entity Framework Core: Log queries for a single db context instance - Stack Overflow

他の方法として、 dotnet test --logger:"console;verbosity=detailed" とすることで、Outputの内容をコンソールへ出力できました。
.NET Core tests produce no output · Issue #1141 · xunit/xunit

ただ

  • テストコードで output.WriteLine するタイミング
  • SQLの実行ログを出力するタイミング

がずれてしまい、EF Coreのメソッドを実行するとどのSQLが実行されるのか分かりづらくなりました。

 
一方、 Console.WriteLine で出力すればタイミングが合っていそうでした。

そのため、現時点ではxUnit.netの ITestOutputHelper を使う方法は諦め、Console.WriteLineを使うことにしました。

一応、 [Collection] 属性を使うことでテストが直列に実行されるようでしたので、これをテストクラスへと付与することにします。
c# - Execute unit tests serially (rather than in parallel) - Stack Overflow

 
SQLのログをコンソールへ出力するためには、 AddConsole() を可能にするためのパッケージを追加します。
ASP .NET Core 3.1 + EntityFramework Core を使って、一対多のリレーションがあるデータをレスポンスしてみた - メモ的な思考的な

$ dotnet add package Microsoft.Extensions.Logging.Console

 

コンソールアプリケーションの起動確認

dotnet runHello worldができればOKです。

$ dotnet run
Hello World!

 

Dockerでのデータベース起動

SQL Server on Linux

Dockerイメージが提供されているため、これを使います。
Microsoft SQL Server - Docker Hub

今回はこんな感じで起動します。

項目 設定値
SQL Serverバージョン 2019
イメージ mcr.microsoft.com/mssql/server:2019-latest
コンテナ名 ms_sql_ef_core
接続ユーザー sa
接続ユーザーのパスワード Your_password123
接続ポート ローカルの1433ポートを、Dockerの1433ポートへ接続

コマンドはこちら。

$ docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Your_password123' -p 1433:1433 --name ms_sql_ef_core -d mcr.microsoft.com/mssql/server:2019-latest

 
なお、Dockerを停止してコンテナを削除するコマンドはこちら。

# 確認 (表示結果は一部省略)
$ docker ps
CONTAINER ID        IMAGE                                        NAMES
6856b27fda46        mcr.microsoft.com/mssql/server:2019-latest   ms_sql_ef_core

# コンテナ停止
$ docker container stop ms_sql_ef_core

# コンテナ削除
$ docker rm ms_sql_ef_core

 
また、Riderでも接続できるか確認します。

Riderでは、 View > ToolWindows > Database を開きます。

DataSourceとして、Microsoft SQL Server を以下の内容で追加します。

項目 設定値
Name 任意(mssql_sample)
Port 1433 (Dockerにあわせる)
User sa
Password Your_password123

 
入力後、 TestConnection ボタンで接続し、以下のような感じで表示されればOKです。

DBMS: Microsoft SQL Server (ver. 15.00.2070) Case sensitivity: plain=mixed, delimited=mixed Driver: Microsoft JDBC Driver 7.4 for SQL Server (ver. 7.4.1.0, JDBC4.2) Ping: 317 ms SSL: no

 

PostgreSQL

同じく、Dockerで起動します。

項目
Host localhost
接続ポート ローカルの55432をDockerの5432に接続
接続ユーザ postgres
接続パスワード Your_password123
データベース efcore

起動と確認のコマンドです。

# 起動
docker run -e 'POSTGRES_PASSWORD=Your_password123' -e 'POSTGRES_DB=efcore' -p 55432:5432 --name postgres_ef_core -d postgres:12.1-alpine

# 確認
$ docker ps
CONTAINER ID        IMAGE
31b44774de4b        postgres:12.1-alpine

 

こちらも同じくRiderで接続できればOKです。

項目 設定値
Host localhost
Port 55432
User postgres
Password Your_password123
Database efcore

 

Consoleアプリから、EF Core経由でSQL Serverへ接続

実装

今回は HelloEfCore というディレクトリ作成し、

  • HelloEfCoreディレクトリの中に実装
  • Program.csからその実装を呼び出す

という形にします。

 

まずは、EF Coreのテーブルを作るために、いわゆるASP.NET Coreのモデルを作成します。

namespace MyApp.HelloEfCore
{
    public class HelloModel
    {
        public int Id { get; set; }
        public string Content { get; set; }
    }
}

 

続いて、DbContextを作成します。

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace MyApp.HelloEfCore
{
    public class HelloContext: DbContext
    {
        public DbSet<HelloModel> HelloModels { get; set; }
        
        public static readonly ILoggerFactory MyLoggerFactory
            = LoggerFactory.Create(builder =>
            {
                builder
                    .AddFilter(DbLoggerCategory.Database.Command.Name, LogLevel.Information)
                    .AddConsole();
            });
        
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder
                .EnableSensitiveDataLogging()
                .UseLoggerFactory(MyLoggerFactory)
                // Databaseは、新規に "efcore" を作成する
                .UseSqlServer(@"Server=tcp:.;Database=efcore;User=sa;Password=Your_password123;");
        }
    }
}

 
なお、SQL Serverのデータベースは、新規作成の efcore を使います。

デフォルトの master では、データベースを削除するときにエラーが出るためです。

dotnet ef database drop

Option 'SINGLE_USER' cannot be set in database 'master'.

 
また、 optionsBuilder.EnableSensitiveDataLogging() を使うことで、SQLの実行ログが

Executed DbCommand (64ms) [Parameters=[@p0='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;

から

Executed DbCommand (77ms) [Parameters=[@p0='Hello World' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;

へと、パラメータの値 @p0='Hello World' が出力されるようにしています。
DbContextOptionsBuilder.EnableSensitiveDataLogging Method (Microsoft.EntityFrameworkCore) | Microsoft Docs

 
さて、モデルとDbContextを使って、SQL Serverへデータを保存するクラスを作成します。

なお、今回は C# 8.0 のため、 using 変数宣言を使ってみます。
リソースの破棄 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

namespace MyApp.HelloEfCore
{
    public class HelloEfCore
    {
        public static void InsertMsSqlServer()
        {
            using var context = new HelloContext();
            
            context.HelloModels.Add(new HelloModel
            {
                Content = "Hello World!"
            });
            context.SaveChanges();
        }
    }
}

 
最後に、Program.csから、HelloEfCoreクラスのInsertMsSqlServer()メソッドを呼びます。

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            HelloEfCore.HelloEfCore.InsertMsSqlServer();
        }
    }
}

 

マイグレーション

dotnetコマンドを使って、マイグレーションを実行します。

使用するDbContextを明示するため、 --context オプションで HelloContext を指定します。

$ dotnet ef migrations add InitialCreate --context HelloContext
...
Done. To undo this action, use 'ef migrations remove'

 
マイグレーションファイルができたら、SQL Serverへと反映します。

$ dotnet ef database update --context HelloContext

...
Applying migration '20200102100945_InitialCreate'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [HelloModels] (
          [Id] int NOT NULL IDENTITY,
          [Content] nvarchar(max) NULL,
          CONSTRAINT [PK_HelloModels] PRIMARY KEY ([Id])
      );
...
Done.

 
Riderでも、SQL Server上のテーブルを確認できます。

f:id:thinkAmi:20200102213602p:plain:w400

 
続いて、コンソールアプリケーションを実行します。

$ dotnet run
...
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (60ms) [Parameters=[@p0='Hello World!' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [HelloModels] ([Content])
      VALUES (@p0);
      SELECT [Id]
      FROM [HelloModels]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

ログに実行されたSQLが表示されています。

Riderでも、SQL Serverの登録データを確認できます。

f:id:thinkAmi:20200102213623p:plain:w400

 
以上で、EF CoreからSQL Serverへ接続し、実行されるSQLをログに出力できる環境ができました。

 

単一FKを持つモデル間でのSetNullとClientSetNullの違い

では、本題のSetNullとClientSetNullの違いについて見ていきます。

今回、Blogに対してAuthorとしてのUserが紐づくモデルを用意します。

そして、Userを削除したときに、外部キー参照しているBlogがどうなるかを見ます。

 

モデル
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace MyApp.ClientSetNull
{
    public interface IUser<T>
    {
        string Name { get; set; }
        List<T> AuthoredBlogs { get; set; }
    }
    public interface IBlog
    {
        int Id { get; set; }
        int? AuthorId { get; set; }
    }
    
    // User - Blog 間に、FKが1つの場合
    public class SingleFkUser : IUser<SingleFkBlog>
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public List<SingleFkBlog> AuthoredBlogs { get; set; }
    }

    public class SingleFkBlog: IBlog
    {
        public int Id { get; set; }
        public string Content { get; set; }
        
        public int? AuthorId { get; set; }
        public SingleFkUser Author { get; set; }
    }
}

 

DbContext

ポイントは

  • テストコードでSetNull/ClientSetNullを切り替えられるよう、コンストラクタを追加
  • OnModelCreatingで、Fluent APIを使ってUserとBlogの関連を定義

です。

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace MyApp.ClientSetNull
{
    public class SingleContext : DbContext
    {

        private readonly DeleteBehavior _deleteBehavior;
        private readonly bool _isRequired;

        // SetNull/ClientSetNullを切り替えられるよう、引数を追加したコンストラクタを用意
        public SingleContext(DbContextOptions<SingleContext> options, 
            DeleteBehavior behavior, bool isRequired) : base(options)
        {
            _deleteBehavior = behavior;
            _isRequired = isRequired;
        }

        public DbSet<SingleFkUser> SingleFkUsers { get; set; }
        public DbSet<SingleFkBlog> SingleFkBlogs { get; set; }

        public static readonly ILoggerFactory MyLoggerFactory
            = LoggerFactory.Create(builder =>
            {
                builder
                    .AddFilter(DbLoggerCategory.Database.Command.Name, LogLevel.Information)
                    .AddConsole();
            });
        
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder
                .EnableSensitiveDataLogging()
                .UseLoggerFactory(MyLoggerFactory);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // コンストラクタで受け取った内容で、User - Blog 間の関連を定義
            modelBuilder.Entity<SingleFkBlog>()
                .HasOne(m => m.Author)
                .WithMany(m => m.AuthoredBlogs)
                .OnDelete(_deleteBehavior)
                .IsRequired(_isRequired);
        }
    }
}

 

テストコード
準備

今回はxUnitを使ってテストコードを書くため、環境を準備します。

まずは、slnファイルのあるディレクトリで、テストプロジェクトを作成します。

$ ls -al
-rw-r--r--  EFCoreSample.sln
-rw-r--r--  EFCoreSample.sln.DotSettings.user
drwxr-xr-x  MyApp

# xunitテンプレートを使ってプロジェクトを作成
$ dotnet new xunit -n MyApp.Test

# ソリューションに追加
$ dotnet sln add MyApp.Test/MyApp.Test.csproj 
プロジェクト `MyApp.Test/MyApp.Test.csproj` をソリューションに追加しました。

 
テストプロジェクトのディレクトリへと移動し、テストプロジェクトから、テスト対象プロジェクトへの参照を追加します。

$ cd MyApp.Test/

$ dotnet add reference ../MyApp/MyApp.csproj 
参照 `..\MyApp\MyApp.csproj` がプロジェクトに追加されました。

 

xunitテンプレートでインストールされるNuGetパッケージですが

  • xunitのバージョンは、 2.4.0
  • Microsoft.NET.Test.Sdkは、16.2.0

と、最新バージョンとは異なります。

このままではテストの周辺ツールが動作しないため、これらのパッケージをバージョンアップします。

dotnet add package コマンドでバージョンを指定しないで実行することで、最新になります。 -v でバージョンを指定しても良いです。

$ dotnet add package xunit
$ dotnet add package Microsoft.NET.Test.Sdk

 

テスト環境ができたか、動作確認をします。

テンプレートに用意されている UnitTest1.cs にテストコードを追加

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        // 以下の一行を追加
        Assert.True(true);
    }
}

 
テストコードのディレクトリで dotnet test 実行したところ、テストがパスしました。

$ dotnet test
/path/to/MyApp.Test/bin/Debug/netcoreapp3.1/MyApp.Test.dll(.NETCoreApp,Version=v3.1) のテスト実行
Microsoft (R) Test Execution Command Line Tool Version 16.3.0
Copyright (c) Microsoft Corporation.  All rights reserved.

テスト実行を開始しています。お待ちください...

合計 1 個のテスト ファイルが指定されたパターンと一致しました。
                                                                                                                                           
テストの実行に成功しました。
テストの合計数: 1
     成功: 1
合計時間: 2.5209 秒

 
確認できましたので、今後UnitTest1.Test1()は実行しないよう、スキップ設定を行います。
Comparing xUnit.net to other frameworks > xUnit.net

public class UnitTest1
{
    // Factの引数Skipを追加
    [Fact(Skip = "実施不要")]
    public void Test1()
    {
        Assert.True(true);
    }
}

 
テストを再実行すると、スキップされました。

$ dotnet test
...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。
[xUnit.net 00:00:00.92]     MyApp.Test.UnitTest1.Test1 [SKIP]
 ! MyApp.Test.UnitTest1.Test1 [1ms]
                                                                                                                                           
テストの実行に成功しました。
テストの合計数: 1
    スキップ: 1
合計時間: 2.1947 秒

 

以上で環境が整いました。

 

全体像

テストコード自体はGithubにあります。
https://github.com/thinkAmi-sandbox/EntityFramework_Core_Sample/tree/master/MyApp.Test

これ以降は説明に必要なものだけ記載していきます。

 
まずは全体像です。

xUnitのTheoryを使って、DeleteBehaviorの値などを切り替えてデータベースを作成後に、検証していきます。

// Theoryですべてをテストすると、ON DELETEが一つのもの(例:NOT NULL)に制限されるので、1つずつアンコメントして試す
[InlineData("単一FK - 任意 - SetNull", DeleteBehavior.SetNull, false)]
// [InlineData("単一FK - 任意 - ClientSetNull", DeleteBehavior.ClientSetNull, false, ConnectionStrings.SqlServer)]
// [InlineData("単一FK - 任意 - Cascade", DeleteBehavior.Cascade, false, ConnectionStrings.SqlServer)]
[Theory]
public void SingleForeignKeyTest(string title, DeleteBehavior deleteBehavior, bool isRequired)
{
    // 初期データ作成
    CreateSingleData(deleteBehavior, isRequired);
    
    Console.WriteLine($"{Environment.NewLine}================= {title} =================");
    
    using var context = CreateSingleContext(deleteBehavior, isRequired);

    // Userを取得
    var user = context.SingleFkUsers.Include(u => u.AuthoredBlogs).First();

    // 検証
    Validate(context, user);

    // 検証後の確認
    using var afterContext = CreateSingleContext(deleteBehavior, isRequired);
    PrintSingleBlogs("STEP4", afterContext);
}

なお、複数のInlineDataを有効にした場合、CREATE文は同じものができてしまいました。 (SetNullを渡していても、CASCADEになる等)

今回は検証ということもあり、今回はInlineDataを一つずつ削除していく形を取りました。詳しい方がいればお知らせを。。。

 

データ作成

1つのUserと、それを外部キー参照しているBlogを2つ作成します。  

private void CreateSingleData(DeleteBehavior deleteBehavior, bool isRequired)
{
    using var context = CreateSingleContext(deleteBehavior, isRequired);
    
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();
        
    var user = new SingleFkUser { Name = "Foo"};
    context.SingleFkUsers.Add(user);

    var blog = new List<SingleFkBlog>
    {
        new SingleFkBlog
        {
            Content = "Single User Blog 1",
            Author = user,
            AuthorId = user.Id
        },
        new SingleFkBlog
        {
            Content = "Single User Blog 2",
            Author = user,
            AuthorId = user.Id
        }
    };

    context.SingleFkBlogs.AddRange(blog);
    context.SaveChanges();
}

 

テーブル作成
private SingleContext CreateSingleContext(DeleteBehavior deleteBehavior, bool isRequired)
{
    var opt = new DbContextOptionsBuilder<SingleContext>()
        .EnableSensitiveDataLogging()
        .UseSqlServer(@"Server=tcp:.;Database=efcore;User=sa;Password=Your_password123;")
        .Options;

    return new SingleContext(opt, deleteBehavior, isRequired);
}

 

検証

EF Coreの各処理ごとに、UserやBlogの内容を出力します。

private void Validate<T>(DbContext context, IUser<T> user)
{
    try {
        PrintEntities(context, "STEP-1", user);
        Console.WriteLine("==== SQL実行確認 ====");

        context.Remove(user);
        PrintEntities(context, "STEP-2", user);
        
        context.SaveChanges();
        PrintEntities(context, "STEP-3", user);
    }
    catch (Exception e)
    {
        Console.WriteLine("---- 例外発生 ----");
        Console.WriteLine(e.ToString());
    }
}

 

実行結果

dotnet test でテストコードを実行した時の結果を見ていきます。

SetNullの場合

CREATE TABLEの結果です。 ON DELETE SET NULL が設定されています。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [SingleFkUsers] (
          [Id] int NOT NULL IDENTITY,
          [Name] nvarchar(max) NULL,
          CONSTRAINT [PK_SingleFkUsers] PRIMARY KEY ([Id])
      );
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [SingleFkBlogs] (
          [Id] int NOT NULL IDENTITY,
          [Content] nvarchar(max) NULL,
          [AuthorId] int NULL,
          CONSTRAINT [PK_SingleFkBlogs] PRIMARY KEY ([Id]),
          CONSTRAINT [FK_SingleFkBlogs_SingleFkUsers_AuthorId] FOREIGN KEY ([AuthorId]) 
              REFERENCES [SingleFkUsers] ([Id]) 
              ON DELETE SET NULL
      );

 
続いて、削除処理をした時のログです。

データ登録直後です。

STEP-1
    User:状態 'Unchanged'、参照Author件数 '2'
    AuthoredBlog:状態 'Unchanged'、Id '1'、外部キー(AuthorId) '1'
    AuthoredBlog:状態 'Unchanged'、Id '2'、外部キー(AuthorId) '1'

context.Remove(user);context.SaveChanges(); 後に

  • 2レコードの AuthorId に、 null をセット
  • 1レコードを削除

が実行されています。

また、Entityの状態も、変化しています。

STEP User AuthoredBlogs
1 Unchanged Unchanged
2 Deleted Modified
3 Detached Unchanged

ログはこちら。

STEP-2
    User:状態 'Deleted'、参照Author件数 '2'
    AuthoredBlog:状態 'Modified'、Id '1'、外部キー(AuthorId) 'null'
    AuthoredBlog:状態 'Modified'、Id '2'、外部キー(AuthorId) 'null'
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@p1='1', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [SingleFkBlogs] SET [AuthorId] = @p0
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [SingleFkBlogs] SET [AuthorId] = @p0
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (7ms) [Parameters=[@p2='1'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DELETE FROM [SingleFkUsers]
      WHERE [Id] = @p2;
      SELECT @@ROWCOUNT;
STEP-3
    User:状態 'Detached'、参照Author件数 '2'
    AuthoredBlog:状態 'Unchanged'、Id '1'、外部キー(AuthorId) 'null'
    AuthoredBlog:状態 'Unchanged'、Id '2'、外部キー(AuthorId) 'null'
STEP4
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.0 initialized 'SingleContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: SensitiveDataLoggingEnabled 
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [s].[Id], [s].[AuthorId], [s].[Content]
      FROM [SingleFkBlogs] AS [s]
Id: '1' / AuthorId: 'null'
Id: '2' / AuthorId: 'null'

 

ClientSetNullの場合

テーブルは ON DELETE NO ACTION で設定されています。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [SingleFkBlogs] (
          [Id] int NOT NULL IDENTITY,
          [Content] nvarchar(max) NULL,
          [AuthorId] int NULL,
          CONSTRAINT [PK_SingleFkBlogs] PRIMARY KEY ([Id]),
          CONSTRAINT [FK_SingleFkBlogs_SingleFkUsers_AuthorId] FOREIGN KEY ([AuthorId]) 
              REFERENCES [SingleFkUsers] ([Id]) 
              ON DELETE NO ACTION
      );

 
ただ、NO ACTION にもかかわらず、SetNullと同じSQLが実行されています。

Entityの状態も同じです。

STEP User AuthoredBlogs
1 Unchanged Unchanged
2 Deleted Modified
3 Detached Unchanged

ログです。

STEP-2
    User:状態 'Deleted'、参照Author件数 '2'
    AuthoredBlog:状態 'Modified'、Id '1'、外部キー(AuthorId) 'null'
    AuthoredBlog:状態 'Modified'、Id '2'、外部キー(AuthorId) 'null'
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@p1='1', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [SingleFkBlogs] SET [AuthorId] = @p0
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [SingleFkBlogs] SET [AuthorId] = @p0
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (8ms) [Parameters=[@p2='1'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DELETE FROM [SingleFkUsers]
      WHERE [Id] = @p2;
      SELECT @@ROWCOUNT;
STEP-3
    User:状態 'Detached'、参照Author件数 '2'
    AuthoredBlog:状態 'Unchanged'、Id '1'、外部キー(AuthorId) 'null'
    AuthoredBlog:状態 'Unchanged'、Id '2'、外部キー(AuthorId) 'null'

 

Cascadeの場合

参考までに、Cascadeの場合のログです。

3つのDELETEが実行されています。また、Entityの状態も異なります。

STEP User AuthoredBlogs
1 Unchanged Unchanged
2 Deleted Deleted
3 Detached Detached

ログはこちら。

STEP-2
    User:状態 'Deleted'、参照Author件数 '2'
    AuthoredBlog:状態 'Deleted'、Id '1'、外部キー(AuthorId) '1'
    AuthoredBlog:状態 'Deleted'、Id '2'、外部キー(AuthorId) '1'
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DELETE FROM [SingleFkBlogs]
      WHERE [Id] = @p0;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[@p0='2'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DELETE FROM [SingleFkBlogs]
      WHERE [Id] = @p0;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (6ms) [Parameters=[@p1='1'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DELETE FROM [SingleFkUsers]
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;
STEP-3
    User:状態 'Detached'、参照Author件数 '2'
    AuthoredBlog:状態 'Detached'、Id '1'、外部キー(AuthorId) '1'
    AuthoredBlog:状態 'Detached'、Id '2'、外部キー(AuthorId) '1'

 

multiple cascade pathsな関係を持つモデルでの違い

続いて、multiple cascade pathsな関係を持つモデルで試してみます。

 

multiple cascade pathsな関係のモデル

例えば、Blogが

  • Author -> Userを参照
  • Contributor -> Userを参照

と、2つの外部キーで1つのUserモデルを参照しているものです。

// User - Blog 間に、FKが2つの場合
// multiple cascade pathsな関係
public class MultiFkUser : IUser<MultiFkBlog>
{
    public int Id { get; set; }
    public string Name { get; set; }

    [InverseProperty("Author")]
    public List<MultiFkBlog> AuthoredBlogs { get; set; }
    
    [InverseProperty("Contributor")]
    public List<MultiFkBlog> ContributedToBlogs { get; set; }
    
}

public class MultiFkBlog: IBlog
{
    public int Id { get; set; }
    public string Content { get; set; }
    
    public int? AuthorId { get; set; }
    public MultiFkUser Author { get; set; }
    
    public int? ContributorId { get; set; }
    public MultiFkUser Contributor { get; set; }
}

今回、これらを使って確認してみます。

 

SQL Serverでの、SetNullやCascadeを確認

MS SQL Serverの場合、 multiple cascade paths な関係を持つモデルでは、ON DELETE SET NULLや ON DELETE CASCADE を設定できません。

テストコードで試したところ、CREATE TABLEの実行時に例外が発生しました。

 

SetNull
fail: Microsoft.EntityFrameworkCore.Database.Command[20102]
      Failed executing DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [MultiFkBlogs] (
          [Id] int NOT NULL IDENTITY,
          [Content] nvarchar(max) NULL,
          [AuthorId] int NULL,
          [ContributorId] int NULL,
          CONSTRAINT [PK_MultiFkBlogs] PRIMARY KEY ([Id]),
          CONSTRAINT [FK_MultiFkBlogs_MultiFkUsers_AuthorId] FOREIGN KEY ([AuthorId]) 
              REFERENCES [MultiFkUsers] ([Id]) 
              ON DELETE SET NULL,
          CONSTRAINT [FK_MultiFkBlogs_MultiFkUsers_ContributorId] FOREIGN KEY ([ContributorId]) 
              REFERENCES [MultiFkUsers] ([Id]) 
              ON DELETE SET NULL
      );
  エラー メッセージ:
   Microsoft.Data.SqlClient.SqlException : Introducing FOREIGN KEY constraint 'FK_MultiFkBlogs_MultiFkUsers_ContributorId' on table 'MultiFkBlogs' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.

 

Cascade
fail: Microsoft.EntityFrameworkCore.Database.Command[20102]
      Failed executing DbCommand (10ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [MultiFkBlogs] (
          [Id] int NOT NULL IDENTITY,
          [Content] nvarchar(max) NULL,
          [AuthorId] int NULL,
          [ContributorId] int NULL,
          CONSTRAINT [PK_MultiFkBlogs] PRIMARY KEY ([Id]),
          CONSTRAINT [FK_MultiFkBlogs_MultiFkUsers_AuthorId] FOREIGN KEY ([AuthorId]) 
              REFERENCES [MultiFkUsers] ([Id]) 
              ON DELETE CASCADE,
          CONSTRAINT [FK_MultiFkBlogs_MultiFkUsers_ContributorId] FOREIGN KEY ([ContributorId]) 
              REFERENCES [MultiFkUsers] ([Id]) 
              ON DELETE CASCADE
      );
  エラー メッセージ:
   Microsoft.Data.SqlClient.SqlException : Introducing FOREIGN KEY constraint 'FK_MultiFkBlogs_MultiFkUsers_ContributorId' on table 'MultiFkBlogs' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.

 

SQLServerでの、ClientSetNull

一方、ClientSetNullであれば、例外は発生しません。

ただし、IncludeですべてのFKをたぐる場合と、一部のFKのみたぐる場合で結果が異なりました。

var user = withInclude
    // すべてのFKを含む
    ? context.MultiFkUsers
        .Include(u => u.AuthoredBlogs)
        .Include(u => u.ContributedToBlogs)
        .First()
    // 一部のFKだけ含む
    : context.MultiFkUsers
        .Include(u => u.AuthoredBlogs)
        .First();

 
そこで、

  1. AuthorとContributorが同じで、削除対象
  2. Authorだけ削除対象
  3. Contributorだけ削除対象
  4. Author、Contributorともに違う

の4種類のデータを用意し、それぞれがどうなるかを確認します。

var userTarget = new MultiFkUser {Name = "Target"};
var userFoo = new MultiFkUser {Name = "Foo"};
var userBar = new MultiFkUser {Name = "Bar"};
context.MultiFkUsers.AddRange(userTarget, userFoo, userBar);

var blogs = new List<MultiFkBlog>
{
    // AuthorとContributorが同じで、削除対象
    new MultiFkBlog
    {
        Content = "Same User Blog",
        Author = userTarget,
        AuthorId = userTarget.Id,
        Contributor = userTarget,
        ContributorId = userTarget.Id
    },
    // Authorだけ削除対象
    new MultiFkBlog
    {
        Content = "Only Author User Blog",
        Author = userTarget,
        AuthorId = userTarget.Id,
        Contributor = userFoo,
        ContributorId = userFoo.Id
    },
    // Contributorだけ削除対象
    new MultiFkBlog
    {
        Content = "Only Contributor User Blog",
        Author = userFoo,
        AuthorId = userFoo.Id,
        Contributor = userTarget,
        ContributorId = userTarget.Id
    },
    // Author、Contributorともに違う
    new MultiFkBlog
    {
        Content = "Another User Blog",
        Author = userFoo,
        AuthorId = userFoo.Id,
        Contributor = userBar,
        ContributorId = userBar.Id
    }
};

 

Include()で全部のFKを指定した場合

例外は発生せず、

Id: '1' / AuthorId: 'null' /ContributorId: 'null'
Id: '2' / AuthorId: 'null' /ContributorId: '2'
Id: '3' / AuthorId: '2' /ContributorId: 'null'
Id: '4' / AuthorId: '2' /ContributorId: '3'

と、必要な外部キーに NULL が設定されました。

# テーブルの定義
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [MultiFkBlogs] (
          [Id] int NOT NULL IDENTITY,
          [Content] nvarchar(max) NULL,
          [AuthorId] int NULL,
          [ContributorId] int NULL,
          CONSTRAINT [PK_MultiFkBlogs] PRIMARY KEY ([Id]),
          CONSTRAINT [FK_MultiFkBlogs_MultiFkUsers_AuthorId] FOREIGN KEY ([AuthorId]) 
              REFERENCES [MultiFkUsers] ([Id]) 
              ON DELETE NO ACTION,
          CONSTRAINT [FK_MultiFkBlogs_MultiFkUsers_ContributorId] FOREIGN KEY ([ContributorId]) 
              REFERENCES [MultiFkUsers] ([Id]) 
              ON DELETE NO ACTION
      );

STEP-1
    User:状態 'Unchanged'、参照Author件数 '2'
    AuthoredBlog:状態 'Unchanged'、Id '1'、外部キー(AuthorId) '1'
    AuthoredBlog:状態 'Unchanged'、Id '2'、外部キー(AuthorId) '1'
==== SQL実行確認 ====
STEP-2
    User:状態 'Deleted'、参照Author件数 '2'
    AuthoredBlog:状態 'Modified'、Id '1'、外部キー(AuthorId) 'null'
    AuthoredBlog:状態 'Modified'、Id '2'、外部キー(AuthorId) 'null'
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p2='1', @p0=NULL (DbType = Int32), @p1=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [MultiFkBlogs] SET [AuthorId] = @p0, [ContributorId] = @p1
      WHERE [Id] = @p2;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [MultiFkBlogs] SET [AuthorId] = @p0
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[@p1='3', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [MultiFkBlogs] SET [ContributorId] = @p0
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (8ms) [Parameters=[@p2='1'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DELETE FROM [MultiFkUsers]
      WHERE [Id] = @p2;
      SELECT @@ROWCOUNT;
STEP-3
    User:状態 'Detached'、参照Author件数 '2'
    AuthoredBlog:状態 'Unchanged'、Id '1'、外部キー(AuthorId) 'null'
    AuthoredBlog:状態 'Unchanged'、Id '2'、外部キー(AuthorId) 'null'
STEP4
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.0 initialized 'MultiContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: SensitiveDataLoggingEnabled 
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [m].[Id], [m].[AuthorId], [m].[Content], [m].[ContributorId]
      FROM [MultiFkBlogs] AS [m]

 

Include()で一部のFKのみ指定した場合

CREATE TABLEやデータ投入は成功したものの、最後のDELETE時に例外が発生しました。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@p2='1', @p0=NULL (DbType = Int32), @p1=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [MultiFkBlogs] SET [AuthorId] = @p0, [ContributorId] = @p1
      WHERE [Id] = @p2;
      SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [MultiFkBlogs] SET [AuthorId] = @p0
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;
fail: Microsoft.EntityFrameworkCore.Database.Command[20102]
      Failed executing DbCommand (11ms) [Parameters=[@p2='1'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DELETE FROM [MultiFkUsers]
      WHERE [Id] = @p2;
      SELECT @@ROWCOUNT;
fail: Microsoft.EntityFrameworkCore.Update[10000]
      An exception occurred in the database while saving changes for context type 'MyApp.ClientSetNull.MultiContext'.
      Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
       ---> Microsoft.Data.SqlClient.SqlException (0x80131904): The DELETE statement conflicted with the REFERENCE constraint "FK_MultiFkBlogs_MultiFkUsers_ContributorId". The conflict occurred in database "efcore", table "dbo.MultiFkBlogs", column 'ContributorId'.

 
エラーメッセージは

The DELETE statement conflicted with the REFERENCE constraint "FK_MultiFkBlogs_MultiFkUsers_ContributorId". The conflict occurred in database "efcore", table "dbo.MultiFkBlogs", column 'ContributorId'.

とあり、公式ドキュメントの

EF Core モデルに構成されている削除動作は、EF Core を使用してプリンシパル エンティティが削除され、依存エンティティがメモリ内に読み込まれている場合 (つまり、追跡されている依存エンティティの場合) にのみ適用されます。 対応する連鎖動作をデータベースに設定し、コンテキストによって追跡されていないデータに対して必要なアクションが適用されるようにする必要があります。 EF Core を使用してデータベースを作成すると、この連鎖動作が設定されます。

連鎖削除 - EF Core | Microsoft Docs

という挙動に合致しています。

 

PostgreSQLでの、SetNullやCascadeを確認

SQL Serverではmultiple cascade paths な関係を持つモデルを許可していませんが、データベースによっては許可しています。

そこで、PostgreSQLで試してみます。

SetNull

InlineData 属性で、データベースを切り替えるだけで試してみます。

// [InlineData("複数FK - SetNull - 任意 - Includeあり - MSSQLServer", 
//     DeleteBehavior.SetNull, false, true, DbEngine.MsSqlServer)]
[InlineData("複数FK - SetNull - 任意 - Includeあり - PostgreSQL", 
    DeleteBehavior.SetNull, false, true, DbEngine.PostgreSql)]

 
例外は発生せず、最後まで実行できました。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (14ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "MultiFkBlogs" (
          "Id" integer NOT NULL GENERATED BY DEFAULT AS IDENTITY,
          "Content" text NULL,
          "AuthorId" integer NULL,
          "ContributorId" integer NULL,
          CONSTRAINT "PK_MultiFkBlogs" PRIMARY KEY ("Id"),
          CONSTRAINT "FK_MultiFkBlogs_MultiFkUsers_AuthorId" FOREIGN KEY ("AuthorId") 
              REFERENCES "MultiFkUsers" ("Id") 
              ON DELETE SET NULL,
          CONSTRAINT "FK_MultiFkBlogs_MultiFkUsers_ContributorId" FOREIGN KEY ("ContributorId") 
              REFERENCES "MultiFkUsers" ("Id") 
              ON DELETE SET NULL
      );

実行結果です。

STEP-2
    User:状態 'Deleted'、参照Author件数 '2'
    AuthoredBlog:状態 'Modified'、Id '1'、外部キー(AuthorId) 'null'
    AuthoredBlog:状態 'Modified'、Id '2'、外部キー(AuthorId) 'null'
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (6ms) [Parameters=[@p2='1', @p0=NULL (DbType = Int32), @p1=NULL (DbType = Int32), @p4='2', @p3=NULL (DbType = Int32), @p6='3', @p5=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      UPDATE "MultiFkBlogs" SET "AuthorId" = @p0, "ContributorId" = @p1
      WHERE "Id" = @p2;
      UPDATE "MultiFkBlogs" SET "AuthorId" = @p3
      WHERE "Id" = @p4;
      UPDATE "MultiFkBlogs" SET "ContributorId" = @p5
      WHERE "Id" = @p6;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[@p7='1'], CommandType='Text', CommandTimeout='30']
      DELETE FROM "MultiFkUsers"
      WHERE "Id" = @p7;
STEP-3
    User:状態 'Detached'、参照Author件数 '2'
    AuthoredBlog:状態 'Unchanged'、Id '1'、外部キー(AuthorId) 'null'
    AuthoredBlog:状態 'Unchanged'、Id '2'、外部キー(AuthorId) 'null'
STEP4
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.0 initialized 'MultiContext' using provider 'Npgsql.EntityFrameworkCore.PostgreSQL' with options: SensitiveDataLoggingEnabled 
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT m."Id", m."AuthorId", m."Content", m."ContributorId"
      FROM "MultiFkBlogs" AS m
Id: '4' / AuthorId: '2' /ContributorId: '3'
Id: '1' / AuthorId: 'null' /ContributorId: 'null'
Id: '2' / AuthorId: 'null' /ContributorId: '2'
Id: '3' / AuthorId: '2' /ContributorId: 'null'

 

Cascade

こちらもCREATE TABLEが成功しています。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (12ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "MultiFkBlogs" (
          "Id" integer NOT NULL GENERATED BY DEFAULT AS IDENTITY,
          "Content" text NULL,
          "AuthorId" integer NULL,
          "ContributorId" integer NULL,
          CONSTRAINT "PK_MultiFkBlogs" PRIMARY KEY ("Id"),
          CONSTRAINT "FK_MultiFkBlogs_MultiFkUsers_AuthorId" FOREIGN KEY ("AuthorId") 
              REFERENCES "MultiFkUsers" ("Id") 
              ON DELETE CASCADE,
          CONSTRAINT "FK_MultiFkBlogs_MultiFkUsers_ContributorId" FOREIGN KEY ("ContributorId") 
              REFERENCES "MultiFkUsers" ("Id") 
              ON DELETE CASCADE
      );

実行結果です。

multiple cascade paths な関係を持つモデルでもCascade Deleteができています。

STEP-2
    User:状態 'Deleted'、参照Author件数 '2'
    AuthoredBlog:状態 'Deleted'、Id '1'、外部キー(AuthorId) '1'
    AuthoredBlog:状態 'Deleted'、Id '2'、外部キー(AuthorId) '1'
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@p0='1', @p1='2'], CommandType='Text', CommandTimeout='30']
      DELETE FROM "MultiFkBlogs"
      WHERE "Id" = @p0;
      DELETE FROM "MultiFkBlogs"
      WHERE "Id" = @p1;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[@p2='1'], CommandType='Text', CommandTimeout='30']
      DELETE FROM "MultiFkUsers"
      WHERE "Id" = @p2;
STEP-3
    User:状態 'Detached'、参照Author件数 '2'
    AuthoredBlog:状態 'Detached'、Id '1'、外部キー(AuthorId) '1'
    AuthoredBlog:状態 'Detached'、Id '2'、外部キー(AuthorId) '1'
STEP4
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.0 initialized 'MultiContext' using provider 'Npgsql.EntityFrameworkCore.PostgreSQL' with options: SensitiveDataLoggingEnabled 
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT m."Id", m."AuthorId", m."Content", m."ContributorId"
      FROM "MultiFkBlogs" AS m
Id: '4' / AuthorId: '2' /ContributorId: '3'

 

まとめ (再掲)

  • ClientSetNullは ON DELETE NO ACTION だが、Nullが設定される
  • SQL Serverの場合、外部キーの状況によってはSetNullではなくClientSetNullを使う
    • multiple cascade pathsな関係を持つモデルなど
  • データベースによっては、multiple cascade pathsな関係を持つモデルも利用可能
  • On Deleteを動作させるには、関係するモデルをInclude()で読み込んでおく

 

その他参考

本題とは関係ない内容ですが、参考になったものをメモしておきます。

 

Null条件演算子やNull合体演算子

項目がnullだったら null という文字列を出力したい時、Null条件演算子(?.)やNull合体演算子(??)を使うことで簡潔に書けました。

Console.WriteLine($"Id: '{blog.Id}' / AuthorId: '{blog.AuthorId?.ToString() ?? "null"}'");

 

switch式

データベースエンジンによってオプション設定を変えたい時、switch式を使うことで簡潔に書けました。
C# 8.0 switch 式 | ++C++; // 未確認飛行 C ブログ

var opt = dbEngine switch
            {
                DbEngine.MsSqlServer => new DbContextOptionsBuilder<MultiContext>()
                    .EnableSensitiveDataLogging()
                    .UseSqlServer(@"Server=tcp:.;Database=efcore;User=sa;Password=Your_password123;")
                    .Options,
                DbEngine.PostgreSql => new DbContextOptionsBuilder<MultiContext>()
                    .EnableSensitiveDataLogging()
                    .UseNpgsql(@"Host=localhost;Database=efcore;Port=55432;Username=postgres;Password=Your_password123")
                    .Options,
                _ => throw new Exception()
                
            };
SQLのログ出力について

以下のstackoverflowに従うと実装できそうな気もしますが、試せていません。。。 Get SQL code from an Entity Framework Core IQueryable - Stack Overflow

 

ソースコード

GitHubに上げました。MyApp/ClientSetNullMyApp/ClientSetNullTest あたりです。
https://github.com/thinkAmi-sandbox/EntityFramework_Core_Sample