その他の変更の追跡の機能
このドキュメントでは、変更の追跡に関連するその他の機能とシナリオについて説明します。
ヒント
このドキュメントは、エンティティの状態と、EF Core での変更の追跡に関する基本を理解していることが前提となっています。 これらのトピックの詳細については、「EF Core での変更の追跡」を参照してください。
ヒント
このドキュメントに含まれているすべてのコードは、GitHub からサンプル コードをダウンロードすることで実行およびデバッグできます。
Add
と AddAsync
Entity Framework Core (EF Core) では、そのメソッドを使用するとデータベースとのやりとりが発生する可能性がある場合は常に、非同期メソッドを提供します。 また、ハイ パフォーマンスの非同期アクセスがサポートされていないデータベースを使用する場合にオーバーヘッドを避けるために同期メソッドも提供します。
通常、DbContext.Add と DbSet<TEntity>.Add がデータベースにアクセスすることはありません。これらのメソッドは、本質的に、エンティティの追跡を開始するだけだからです。 ただし、値生成の形式によっては、キー値を生成するために、データベースへのアクセスが行われる場合があります。 それを実行する、EF Core に付属している値ジェネレーターは、HiLoValueGenerator<TValue> だけです。 このジェネレーターの使用は一般的ではなく、既定で構成されることはありません。 つまり、アプリケーションの大部分には、AddAsync
ではなく、Add
を使用する必要があるということです。
Update
、Attach
、Remove
などの他の同様のメソッドでは非同期オーバーロードは発生しません。それらによって新しいキー値が生成されることはなく、データベースにアクセスする必要がないからです。
AddRange
、UpdateRange
、AttachRange
、および RemoveRange
DbSet<TEntity> と DbContext には、Add
、Update
、Attach
、Remove
の代替バージョンとして、1 回の呼び出しで複数のインスタンスを指定できるものが用意されています。 これらのメソッドは、それぞれ AddRange、UpdateRange、AttachRange、RemoveRange です。
これらのメソッドは、便宜上提供されているものです。 "範囲" メソッドを使用すると、同等の非範囲メソッドを複数回呼び出すのと同じ機能が得られます。 この 2 つの方法に、パフォーマンス上の大きな違いはありません。
Note
これは EF6 とは異なります。EF6 では、AddRange
と Add
の両方で DetectChanges
が自動的に呼び出されていて、Add
を複数回呼び出すと DetectChanges が 1 回ではなく複数回呼び出されていました。 このため、EF6 では AddRange
がより効率的でした。 EF Core では、これらのメソッドのいずれを使用しても、DetectChanges
は自動的に呼び出されません。
DbContext および Dbcontext メソッドの比較
Add
、Update
、Attach
、Remove
などの多くのメソッドは、DbSet<TEntity> と DbContext の両方に実装があります。 これらのメソッドは、通常のエンティティ型の場合とまったく同じように動作します。 これは、エンティティの CLR 型が EF Core モデルの 1 つのエンティティ型にのみマップされるためです。 したがって、モデル内でエンティティが適合する場所は CLR 型によって完全に定義されているので、使用する DbSet は暗黙的に決定することができます。
この規則に対して例外が発生するのは、主に多対多の結合エンティティ向けに使われる、共有型のエンティティ型を使う場合です。 共有型のエンティティ型を使用する場合は、使用する EF Core モデル型に対して、最初に DbSet を作成する必要があります。 DbSet に対して Add
、Update
、Attach
、Remove
などのメソッドを使用すれば、どの EF コア モデル型が使用されているかについて曖昧になることはありません。
多対多リレーションシップにある結合エンティティに対して、既定では共有型のエンティティ型が使用されます。 また、共有型のエンティティ型を、多対多リレーションシップで使用するように明示的に構成することもできます。 たとえば、次のコードでは、Dictionary<string, int>
を結合エンティティ型としてを構成します。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.SharedTypeEntity<Dictionary<string, int>>(
"PostTag",
b =>
{
b.IndexerProperty<int>("TagId");
b.IndexerProperty<int>("PostId");
});
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, int>>(
"PostTag",
j => j.HasOne<Tag>().WithMany(),
j => j.HasOne<Post>().WithMany());
}
新しい結合エンティティ インスタンスを追跡することで、2 つのエンティティを関連付ける方法については、「外部キーとナビゲーションの変更」を参照してください。 次のコードでは、結合エンティティに使用される Dictionary<string, int>
共有型のエンティティ型に対してこれを実行します。
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
DbContext.Set<TEntity>(String) を使って PostTag
エンティティ型の DbSet が作成されていることに注意してください。 次に、この DbSet を使用して、新しい結合エンティティ インスタンスで Add
を呼び出すことができます。
重要
規約により結合エンティティ型に使用されている CLR 型は、パフォーマンスを向上させるために将来のリリースで変更される可能性があります。 特定の結合エンティティ型には依存しないでください。ただし、それが、上記のコード内で Dictionary<string, int>
に対して行われているように、明示的に構成されている場合を除きます。
プロパティおよびフィールド アクセスの比較
エンティティ プロパティへのアクセスには、既定でプロパティのバッキング フィールドが使われます。 これは効率的であり、またプロパティのゲッターとセッターの呼び出しで副作用がトリガーされるのを回避します。 たとえば、これを使用すれば、遅延読み込みによって無限ループがトリガーされるのを防ぐことができます。 モデル内でバッキング フィールドを構成する方法の詳細については、「バッキング フィールド」を参照してください。
場合によっては、EF Core よる副作用の生成は、EF Core でプロパティ値が変更される場合に望ましい場合があります。 たとえば、エンティティへのデータ バインド時には、プロパティを設定すると、フィールドを直接設定したときには発生しない U.I. への通知が生成されることがあります。 これを実現するには、次に対して PropertyAccessMode を変更します。
- モデル内のすべてのエンティティ型。ModelBuilder.UsePropertyAccessMode を使用します
- 特定のエンティティ型のすべてのプロパティとナビゲーション。EntityTypeBuilder<TEntity>.UsePropertyAccessMode を使用します
- 特定のプロパティ。PropertyBuilder.UsePropertyAccessMode を使用します
- 特定のナビゲーション。NavigationBuilder.UsePropertyAccessMode を使用します
プロパティ アクセス モードが Field
および PreferField
である場合、EF Core からプロパティ値へのアクセスはバッキング フィールドを介して行われます。 同様に、Property
および PreferProperty
である場合、EF Core からプロパティ値へのアクセスはそのゲッターとセッターを介して行われます。
Field
または Property
が使用されている場合に、EF Core からそれぞれ、フィールドまたはプロパティのゲッター/セッターを介して値にアクセスできないときは、EF Core によって例外がスローされます。 これにより、EF Core ではいつも確実にフィールドまたはプロパティ アクセスが使用されます (それを想定している場合)。
一方、PreferField
および PreferProperty
モードでは、優先アクセスを使用できない場合、それぞれプロパティまたはバッキング フィールドを使用するようにフォール バックされます。 PreferField
が既定値です。 つまり、EF Core では可能な限りフィールドを使用しますが、代わりにプロパティにそのゲッターまたはセッターを介してアクセスする必要がある場合でも失敗することはありません。
FieldDuringConstruction
および PreferFieldDuringConstruction
では、エンティティ インスタンスを作成する場合のみバッキング フィールドを使用するように EF Core を構成します。 これにより、ゲッターおよびセッターの副作用なしでクエリを実行できるようになりますが、EF Core によってプロパティが後で変更されると、これらの副作用が発生します。
次の表は、さまざまなプロパティ アクセス モードについてまとめたものです。
PropertyAccessMode | 基本設定 | 優先順位 (エンティティ作成) | フォールバック | フォールバック (エンティティ作成) |
---|---|---|---|---|
Field |
フィールド | フィールド | スロー | スロー |
Property |
プロパティ | プロパティ | スロー | スロー |
PreferField |
フィールド | フィールド | プロパティ | プロパティ |
PreferProperty |
プロパティ | プロパティ | フィールド | フィールド |
FieldDuringConstruction |
プロパティ | フィールド | フィールド | スロー |
PreferFieldDuringConstruction |
プロパティ | フィールド | フィールド | プロパティ |
一時的な値
新しいエンティティを追跡する場合は、EF Core によって一時的なキー値が作成されます。そのエンティティは、SaveChanges が呼び出されたときにデータベースによって生成された実際のキー値を得ることになります。 これらの一時的な値の使用方法の概要については、「EF Core での変更の追跡」を参照してください。
一時的な値へのアクセス
一時的な値は変更トラッカーに格納されます。エンティティ インスタンスに直接設定されることはありません。 ただし、これらの一時的な値は、追跡対象のエンティティにアクセスするためのさまざまなメカニズムを使用する場合に公開されます。 たとえば、次のコードでは、EntityEntry.CurrentValues を使って一時的な値にアクセスします。
using var context = new BlogsContext();
var blog = new Blog { Name = ".NET Blog" };
context.Add(blog);
Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");
このコードの出力は次のようになります。
Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643
PropertyEntry.IsTemporary を使って、一時的な値を確認できます。
一時的な値の操作
一時的な値を明示的に操作すると便利な場合があります。 たとえば、新しいエンティティのコレクションを Web クライアント上で作成してから、サーバーにシリアル化して戻します。 これらのエンティティ間のリレーションシップを設定するための 1 つの方法として、外部キーの値があります。 次のコードではこの方法を使用して、SaveChanges が呼び出されたときに実際のキー値を生成できるようにしたままで、新しいエンティティのグラフを外部キーによって関連付けます。
var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };
var posts = new List<Post>
{
new Post
{
Id = -1,
BlogId = -1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = -2,
BlogId = -2,
Title = "Disassembly improvements for optimized managed debugging",
Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
}
};
using var context = new BlogsContext();
foreach (var blog in blogs)
{
context.Add(blog).Property(e => e.Id).IsTemporary = true;
}
foreach (var post in posts)
{
context.Add(post).Property(e => e.Id).IsTemporary = true;
}
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
次のことに注意してください。
- 負の数値は、一時的なキー値として使用されます。これは必須ではありませんが、キーの競合を防ぐための一般的な規則です。
Post.BlogId
FK プロパティには、関連付けられたブログの PK と同じ負の値が割り当てられます。- 各エンティティが追跡された後に IsTemporary を設定することで、PK 値は一時的なものとしてマークされます。 これが必要とされる理由は、アプリケーションによって指定されたキー値がいずれも実際のキー値と見なされることにあります。
SaveChanges を呼び出す前に 変更トラッカーのデバッグ ビューを確認すると、PK 値が一時的なものとしてマークされ、ナビゲーションの修正などの投稿が適切なブログに関連付けられていることがわかります。
Blog {Id: -2} Added
Id: -2 PK Temporary
Name: 'Visual Studio Blog'
Posts: [{Id: -2}]
Blog {Id: -1} Added
Id: -1 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -1}]
Post {Id: -2} Added
Id: -2 PK Temporary
BlogId: -2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: -2}
Tags: []
Post {Id: -1} Added
Id: -1 PK Temporary
BlogId: -1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: -1}
SaveChanges を呼び出した後で、これらの一時的な値は、データベースによって生成された実際の値で置き換えられています。
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Posts: [{Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
既定値の操作
EF Core を使用すると、SaveChanges が呼び出されたときに、プロパティにはその既定値がデータベースから設定されます。 キー値の生成の場合と同様に、EF Core では、値が明示的に設定されていない場合にのみ、データベースからの既定値を使用します。 たとえば、次のエンティティ型について考えてみます。
public class Token
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime ValidFrom { get; set; }
}
ValidFrom
プロパティは、データベースからの既定値を取得するように構成されています。
modelBuilder
.Entity<Token>()
.Property(e => e.ValidFrom)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
この型のエンティティを挿入すると、明示的な値が設定されていない場合に、データベースは値の生成を EF Core から許可されます。 次に例を示します。
using var context = new BlogsContext();
context.AddRange(
new Token { Name = "A" },
new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
変更トラッカー デバッグ ビューを見ると、最初のトークンにはデータベースによって生成された ValidFrom
が使用されていますが、2 番目のトークンには明示的に設定された値が使用されています。
Token {Id: 1} Unchanged
Id: 1 PK
Name: 'A'
ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
Id: 2 PK
Name: 'B'
ValidFrom: '11/11/1111 11:11:11 AM'
Note
データベースの既定値を使用するには、データベースの列に既定値制約が構成されている必要があります。 これは、HasDefaultValueSql または HasDefaultValue を使うと、EF Core の移行によって自動的に行われます。 EF Core の移行を使用しない場合は、他の方法で列に既定値制約を必ず作成してください。
Null 許容のプロパティの使用
EF Core では、プロパティ値をその型の CLR の既定値と比較することにより、プロパティが設定されているかどうかを判断できます。 これは、ほとんどの場合うまく機能しますが、CLR の既定値をデータベースに明示的に挿入できないことを意味しています。 たとえば、次のように整数型プロパティを持つエンティティについて考えてみます。
public class Foo1
{
public int Id { get; set; }
public int Count { get; set; }
}
ここで、プロパティはデータベースの既定値 -1 を持つように構成されています。
modelBuilder
.Entity<Foo1>()
.Property(e => e.Count)
.HasDefaultValue(-1);
その目的は、明示的な値が設定されていない場合は必ず既定値 -1 が使用されるようにすることにあります。 ただし、値を 0 (整数型の CLR 既定値) に設定することは、EF Core で値を設定しないことと区別がつきません。つまり、このプロパティに 0 を挿入することはできません。 次に例を示します。
using var context = new BlogsContext();
var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);
Count
が明示的に 0 に設定されているインスタンスには、引き続きデータベースから既定値が与えられることに注意してください。これは、意図したものではありません。 これに対処する簡単な方法は、Count
プロパティを Null 許容にすることです。
public class Foo2
{
public int Id { get; set; }
public int? Count { get; set; }
}
これにより、CLR の既定値は 0 ではなく null になります。つまり、明示的に設定されたときに 0 が挿入されるようになります。
using var context = new BlogsContext();
var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Null 許容バッキング フィールドの使用
プロパティを Null 許容にすることの問題は、それがドメイン モデルで概念的に Null 許容にならない可能性があるということです。 このため、プロパティを強制的に Null 許容にすると、モデルが危険にさらされます。
プロパティは null 非許容のままとし、バッキング フィールドだけを null 許容とすることができます。 次に例を示します。
public class Foo3
{
public int Id { get; set; }
private int? _count;
public int Count
{
get => _count ?? -1;
set => _count = value;
}
}
これにより、プロパティが明示的に 0 に設定されている場合は CLR の既定値 (0) を挿入できるようになり、ドメイン モデルでは、プロパティを Null 許容として公開する必要がありません。 次に例を示します。
using var context = new BlogsContext();
var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
bool プロパティの Null 許容バッキング フィールド
このパターンは、ストアで生成された既定値を持つ bool プロパティを使用する場合に特に便利です。 bool
の CLR 既定値は "false" であるため、通常のパターンを使用して "false" を明示的に挿入できないことになります。 たとえば、User
エンティティ型について考えてみます。
public class User
{
public int Id { get; set; }
public string Name { get; set; }
private bool? _isAuthorized;
public bool IsAuthorized
{
get => _isAuthorized ?? true;
set => _isAuthorized = value;
}
}
IsAuthorized
プロパティは、データベースの既定値 "true" を使用して構成されます。
modelBuilder
.Entity<User>()
.Property(e => e.IsAuthorized)
.HasDefaultValue(true);
IsAuthorized
プロパティは、挿入前に明示的に "true" にも "false" にも設定できます。また、未設定のままにすることもできます。その場合、データベースの既定値が使用されます。
using var context = new BlogsContext();
var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!
context.AddRange(userA, userB, userC);
context.SaveChanges();
SQLite を使用した場合の SaveChanges からの出力を見ると、Mac にはデータベースの既定値が使用されているが、Alice と Baxter には明示的な値が設定されています。
-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
スキーマの既定値のみ
EF Core で既定値を挿入に使用するのでなく、それらの値を EF Core の移行によって作成されたデータベース スキーマ内で使用するのが便利な場合があります。 これは、プロパティを PropertyBuilder.ValueGeneratedNever として構成することで実現できます。次に例を示します。
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();
.NET