EntityFramework Core (以降、EF Core)では、テーブルのフィールドのNOT NULL制約を制御する方法の一つとして、Fluent APIがあります。
Entity Properties - EF Core | Microsoft Docs
そんな中、フィールドの型が int
と int?
では、Fluent APIの挙動が異なっていたため、メモを残します。
目次
環境
- macOS 10.13.6
- .NET Core 3.1.0
- SDK 3.1.100
- EntityFramework Core 3.1.0
- Microsoft.Extensions.Logging.Console 3.1.0
- xunit 2.4.1
以下の記事の環境を、一部を除いてそのまま流用しています。
EntityFramework Core 3.1で、ClientSetNullの挙動を確認してみた - メモ的な思考的な
異なる点は、データベースを「SQL Server on Linux / PostgreSQL」から、手軽なSQLiteへの変更です。
そのため、MyAppプロジェクトに、SQLite向けのNuGetパッケージを追加しています。
$ dotnet add package Microsoft.EntityFrameworkCore.SQLite
確認するパターン
今回は、以下のパターンをテストコードを使って、発行されるSQLを確認します。
パターン | IsRequired()対象項目の型 | IsRequiredの値 |
---|---|---|
NULL不許可型に対し、NOT NULL制約 | int | true |
NULL不許可型に対し、NULL許可 | int | false |
Nullable型に対し、NOT NULL制約 | int? | true |
Nullable型に対し、NULL許可 | int? | false |
また、外部キーのint/int?型についても確認します。
長いのでまとめ
int型/int?型のフィールドについては以下の結果となりました。
パターン | IsRequired()対象項目の型 | IsRequiredの値 | 結果 |
---|---|---|---|
NULL不許可型に対し、NOT NULL制約 | int | true | NOT NULL制約 |
NULL不許可型に対し、NULL許可 | int | false | 例外が発生 |
Nullable型に対し、NOT NULL制約 | int? | true | NOT NULL制約 |
Nullable型に対し、NULL許可 | int? | false | NULL許可 |
また、外部キーについては以下となりました。
int型のフィールドとは異なり、int型の外部キーに対して、Fluent APIの IsRequired(false)
を設定しても、例外が発生しない & NOT NULL制約となりました。
外部キーの型 | IsRequiredの値 | 結果 | 例外発生 |
---|---|---|---|
int | true | NOT NULL制約 | 無し |
int | false | NOT NULL制約 | 無し |
int? | true | NOT NULL制約 | 無し |
int? | false | NULL許可 | 無し |
確認用の実装
エンティティクラス
3種類用意します。
int型のエンティティクラス
public class NotNullEntity { public int Id { get; set; } public int NotNullField { get; set; } }
int?型のエンティティクラス
public class NullableEntity { public int Id { get; set; } public int? NullableField { get; set; } }
int/int?型の外部キーを持つエンティティクラス群
外部キーと型については
- 外部キーAuthorFkが、int型
- 外部キーContributorFkが、int?型
としました。
public class User { public int Id { get; set; } public List<Blog> AuthoredBlogs { get; set; } public List<Blog> ContributedToBlogs { get; set; } } public class Blog { public int Id { get; set; } [ForeignKey("Author")] public int AuthorFk { get; set; } public User Author { get; set; } [ForeignKey("Contributor")] public int? ContributorFk { get; set; } public User Contributor { get; set; } }
DbContext
前回の記事のように、
- ログ出力部分はベースへ移動
- 3種類のエンティティクラスに対し、それぞれDbContextを用意
としました。
ログ出力用のベースDbContext
LoggerFactoryを使って、コンソールへSQLの実行ログを出力しています。
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace MyApp.IsRequired { public class IsRequiredBaseContext<T> : DbContext where T : DbContextOptions { protected bool _isRequired; public IsRequiredBaseContext(T options, bool isRequired) : base(options) { _isRequired = isRequired; } 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); } } }
int型のDbContext
NOT NULL制約を外部から指定できるよう、
とします。
using Microsoft.EntityFrameworkCore; namespace MyApp.IsRequired { public class NotNullContext : IsRequiredBaseContext<DbContextOptions<NotNullContext>> { public NotNullContext(DbContextOptions<NotNullContext> options, bool isRequired) : base(options, isRequired) {} public DbSet<NotNullEntity> NotNullModels { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<NotNullEntity>() .Property(m => m.NotNullField) .IsRequired(_isRequired); } } }
int?型のDbContext
内容は、int型のDbContextと同じです。
using Microsoft.EntityFrameworkCore; namespace MyApp.IsRequired { public class NullableContext : IsRequiredBaseContext<DbContextOptions<NullableContext>> { public NullableContext(DbContextOptions<NullableContext> options, bool isRequired) : base(options, isRequired) {} public DbSet<NullableEntity> NullableModels { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<NullableEntity>() .Property(m => m.NullableField) .IsRequired(_isRequired); } } }
int/int?型の外部キーを持つDbContext
外部キー(AuthorFk/ContributorFk)に対する NOT NULL制約の制御は
modelBuilder.Entity<Blog>() .HasOne(m => m.Author) .WithMany(m => m.AuthoredBlogs) .IsRequired(_isRequired);
にて行います。
全体像はこちら。
using Microsoft.EntityFrameworkCore; namespace MyApp.IsRequired { public class UserBlogContext : IsRequiredBaseContext<DbContextOptions<UserBlogContext>> { public UserBlogContext(DbContextOptions<UserBlogContext> options, bool isRequired) : base(options, isRequired) {} public DbSet<User> NotNullUsers { get; set; } public DbSet<Blog> NotNullBlogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasOne(m => m.Author) .WithMany(m => m.AuthoredBlogs) .IsRequired(_isRequired); modelBuilder.Entity<Blog>() .HasOne(m => m.Contributor) .WithMany(m => m.ContributedToBlogs) .IsRequired(_isRequired); } } }
結果を確認するテストコード
DbContextを生成する部分は、3種類のDbContextがあっても型だけが違うので、ジェネリックを使っています。
なお、ジェネリックでは、引数を持つコンストラクタは利用できません。
そのため、以下を参考にして、 Activator.CreateInstance()
にてジェネリッククラスのインスタンス化をします。
ジェネリックスで引数を持つコンストラクタを使用してインスタンスを生成する - Qiita
ただし、 Activator.CreateInstance()
はパフォーマンス問題があるため注意が必要です。今回は検証するだけなので、気にしないことにします。
補足: new() 制約 | ジェネリック - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
using System; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using MyApp.IsRequired; using Xunit; namespace MyApp.Test.IsRequiredTest { public enum Pattern { NotNull, Nullable, Fk } public class IsRequiredFieldTest { [InlineData("NotNull型int - IsRequiredが true の場合", Pattern.NotNull, true)] // [InlineData("NotNull型int - IsRequiredが false の場合", Pattern.NotNull, false)] // [InlineData("Nullable型int? - IsRequiredが true の場合", Pattern.Nullable, true)] // [InlineData("Nullable型int? - IsRequiredが false の場合", Pattern.Nullable, false)] // [InlineData("int/int?のFK - IsRequiredが true の場合", Pattern.Fk, true)] // [InlineData("int/int?のFK - IsRequiredが false の場合", Pattern.Fk, false)] [Theory] public void Nullable型やNotNull型のIsRequiredテスト(string title, Pattern pattern, bool isRequired) { Console.WriteLine($"{Environment.NewLine}==================={Environment.NewLine}" + $"{title}{Environment.NewLine}" + "==================="); var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); try { using DbContext context = pattern switch { Pattern.NotNull => CreateContext<NotNullContext>(connection, isRequired), Pattern.Nullable => CreateContext<NullableContext>(connection, isRequired), Pattern.Fk => CreateContext<UserBlogContext>(connection, isRequired), _ => throw new Exception() }; context.Database.EnsureCreated(); } finally { connection.Close(); } } private T CreateContext<T>(SqliteConnection connection, bool isRequired) where T : DbContext { var options = new DbContextOptionsBuilder<T>() .EnableSensitiveDataLogging() .UseSqlite(connection) .Options; // Activator.CreateInstanceはパフォーマンス問題があることに注意 // 参考:https://ufcpp.net/study/csharp/sp2_generics.html#new-constrants return (T) Activator.CreateInstance(typeof(T), options, isRequired); } } }
このテストコードは
$ dotnet test --filter "FullyQualifiedName~MyApp.Test.IsRequiredTest.IsRequiredTest"
で実行できます。
以上で準備は終わりです。
結果
NULL不許可型に対し、NOT NULL制約
NOT NULL制約ができました。
=================== NotNull型int - IsRequiredが true の場合 =================== ... info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "NotNullModels" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_NotNullModels" PRIMARY KEY AUTOINCREMENT, "NotNullField" INTEGER NOT NULL );
NULL不許可型に対し、NULL許可
int型にNULL許可したところ、例外が発生しました。
エラーメッセージに
the type of the property is 'int' which is not a nullable type
とあるので、int型ではNULL許可ができないようです。
=================== NotNull型int - IsRequiredが false の場合 =================== ... X MyApp.Test.IsRequiredTest.IsRequiredTest.Nullable型やNotNull型のIsRequiredテスト(title: "NotNull型int - IsRequiredが false の場合", pattern: NotNull, isRequired: False) [526ms] エラー メッセージ: System.InvalidOperationException : The property 'NotNullField' on entity type 'NotNullEntity' cannot be marked as nullable/optional because the type of the property is 'int' which is not a nullable type. Any property can be marked as non-nullable/required, but only properties of nullable types and which are not part of primary key can be marked as nullable/optional.
Nullable型に対し、NOT NULL制約
int?型でも NOT NULL 制約ができました。
=================== Nullable型int? - IsRequiredが true の場合 =================== ... info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "NullableModels" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_NullableModels" PRIMARY KEY AUTOINCREMENT, "NullableField" INTEGER NOT NULL );
Nullable型に対し、NULL許可
int?型であれば、NULL許可できました。
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "NullableModels" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_NullableModels" PRIMARY KEY AUTOINCREMENT, "NullableField" INTEGER NULL );
外部キー int型/int?型で、NOT NULL制約
int型/int?型ともに、NOT NULL制約ができました。
=================== int/int?のFK - IsRequiredが true の場合 =================== ... info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "NotNullUsers" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_NotNullUsers" PRIMARY KEY AUTOINCREMENT ); info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "NotNullBlogs" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_NotNullBlogs" PRIMARY KEY AUTOINCREMENT, "AuthorFk" INTEGER NOT NULL, "ContributorFk" INTEGER NOT NULL, CONSTRAINT "FK_NotNullBlogs_NotNullUsers_AuthorFk" FOREIGN KEY ("AuthorFk") REFERENCES "NotNullUsers" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_NotNullBlogs_NotNullUsers_ContributorFk" FOREIGN KEY ("ContributorFk") REFERENCES "NotNullUsers" ("Id") ON DELETE CASCADE );
外部キー int型/int?型で、NULL許可
挙動に違いが出ました。
- int型は、NOT NULL制約
- int?型は、NULL許可
int型のフィールドの時とは異なり、int型の外部キーでは例外が出ないまま、NOT NULL制約が付きました。
=================== int/int?のFK - IsRequiredが false の場合 =================== ... info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "NotNullUsers" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_NotNullUsers" PRIMARY KEY AUTOINCREMENT ); info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "NotNullBlogs" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_NotNullBlogs" PRIMARY KEY AUTOINCREMENT, "AuthorFk" INTEGER NOT NULL, "ContributorFk" INTEGER NULL, CONSTRAINT "FK_NotNullBlogs_NotNullUsers_AuthorFk" FOREIGN KEY ("AuthorFk") REFERENCES "NotNullUsers" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_NotNullBlogs_NotNullUsers_ContributorFk" FOREIGN KEY ("ContributorFk") REFERENCES "NotNullUsers" ("Id") ON DELETE RESTRICT );
まとめ
int型/int?型のフィールドについては以下の結果となりました。
パターン | IsRequired()対象項目の型 | IsRequiredの値 | 結果 |
---|---|---|---|
NULL不許可型に対し、NOT NULL制約 | int | true | NOT NULL制約 |
NULL不許可型に対し、NULL許可 | int | false | 例外が発生 |
Nullable型に対し、NOT NULL制約 | int? | true | NOT NULL制約 |
Nullable型に対し、NULL許可 | int? | false | NULL許可 |
また、外部キーについては以下となりました。
int型のフィールドとは異なり、int型の外部キーに対して、Fluent APIの IsRequired(false)
を設定しても、例外が発生しない & NOT NULL制約となりました。
外部キーの型 | IsRequiredの値 | 結果 | 例外発生 |
---|---|---|---|
int | true | NOT NULL制約 | 無し |
int | false | NOT NULL制約 | 無し |
int? | true | NOT NULL制約 | 無し |
int? | false | NULL許可 | 無し |
ソースコード
Githubに上げました。 MyApp/IsRequired
や MyApp.Test/IsRequired
ディレクトリが今回のものです。
https://github.com/thinkAmi-sandbox/EntityFramework_Core_Sample