DbContext インスタンスは、データベースから返されたエンティティを自動的に追跡します。 これらのエンティティに加えられた変更は、SaveChanges が呼び出されたときに検出され、必要に応じてデータベースが更新されます。 詳細については、「 基本的な保存 と 関連データ 」を参照してください。
ただし、エンティティは、1 つのコンテキスト インスタンスを使用してクエリを実行した後、別のインスタンスを使用して保存される場合があります。 これは多くの場合、エンティティの照会、クライアントへの送信、変更、要求内のサーバーへの返送、保存が行われる Web アプリケーションなどの "切断" シナリオで発生します。 この場合、2 番目のコンテキスト インスタンスは、エンティティが新しい (挿入する必要があります) か、既存の (更新する必要があるか) を認識する必要があります。
ヒント
この記事の サンプル は、GitHub で確認できます。
ヒント
EF Core では、特定の主キー値を持つ任意のエンティティのインスタンスを 1 つだけ追跡できます。 各作業単位に対して有効期間の短いコンテキストを使用し、コンテキストが空の状態から始まり、エンティティが関連付けられ、それらのエンティティが保存されるようにし、その後コンテキストが破棄されるようにすることが、この問題を回避する最善の方法です。
新しいエンティティの識別
クライアントが新しいエンティティを識別する
対処する最も簡単なケースは、エンティティが新規か既存かをクライアントがサーバーに通知する場合です。 たとえば、多くの場合、新しいエンティティを挿入する要求は、既存のエンティティを更新する要求とは異なります。
このセクションの残りの部分では、挿入するか更新するかを他の方法で決定する必要がある場合について説明します。
自動生成されたキーを使用する
自動的に生成されるキーの値は、エンティティを挿入または更新する必要があるかどうかを判断するために使用されることがよくあります。 キーが設定されていない場合 (つまり、CLR の既定値が null、ゼロなど) の場合、エンティティは新規であり、挿入する必要があります。 一方、キー値が設定されている場合は、既に保存されている必要があり、更新する必要があります。 つまり、キーに値がある場合、エンティティはクエリされ、クライアントに送信され、更新されます。
エンティティ型がわかっている場合は、未設定のキーを簡単に確認できます。
public static bool IsItNew(Blog blog)
=> blog.BlogId == 0;
ただし、EF には、任意のエンティティ型とキー型に対してこれを行う組み込みの方法もあります。
public static bool IsItNew(DbContext context, object entity)
=> !context.Entry(entity).IsKeySet;
ヒント
エンティティが追加された状態であっても、エンティティがコンテキストによって追跡されるとすぐに、キーが設定されます。 これは、エンティティのグラフを走査し、TrackGraph API を使用する場合など、それぞれの操作を決定するときに役立ちます。 キー値は、エンティティを追跡するための呼び出しが行われる 前に 、ここで示されている方法でのみ使用する必要があります。
その他のキーを使用する
キー値が自動的に生成されない場合は、新しいエンティティを識別するために、他のいくつかのメカニズムが必要です。 これには、次の 2 つの一般的な方法があります。
- エンティティのクエリ
- クライアントからフラグを渡す
エンティティを照会するには、Find メソッドを使用します。
public static async Task<bool> IsItNew(BloggingContext context, Blog blog)
=> (await context.Blogs.FindAsync(blog.BlogId)) == null;
クライアントからフラグを渡すための完全なコードを示すことは、このドキュメントの範囲外です。 Web アプリでは、通常、異なるアクションに対して異なる要求を行うか、要求で何らかの状態を渡してからコントローラーで抽出することを意味します。
1 つのエンティティの保存
挿入または更新が必要かどうかがわかっていれば、[追加] または [更新] を適切に使用できます。
public static async Task Insert(DbContext context, object entity)
{
context.Add(entity);
await context.SaveChangesAsync();
}
public static async Task Update(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}
ただし、エンティティが自動生成されたキー値を使用する場合、Update メソッドは両方のケースで使用できます。
public static async Task InsertOrUpdate(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}
通常、Update メソッドはエンティティを挿入ではなく更新対象としてマークします。 ただし、エンティティに自動生成されたキーがあり、キー値が設定されていない場合、エンティティは自動的に挿入対象としてマークされます。
エンティティが自動生成されたキーを使用していない場合、アプリケーションはエンティティを挿入するか更新するかを決定する必要があります。次に例を示します。
public static async Task InsertOrUpdate(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs.FindAsync(blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
}
await context.SaveChangesAsync();
}
手順は次のとおりです。
- Find が null を返す場合、データベースにはこの ID を持つブログがまだ含まれていないため、Add マークを挿入用に呼び出します。
- Find がエンティティを返した場合、エンティティはデータベースに存在し、コンテキストは現在、既存のエンティティを追跡しています
- 次に、SetValues を使用して、このエンティティのすべてのプロパティの値をクライアントから取得したプロパティに設定します。
- SetValues 呼び出しは、必要に応じてエンティティを更新するようにマークします。
ヒント
SetValues は、追跡対象エンティティの値が異なるプロパティのみを変更済みとしてマークします。 つまり、更新が送信されると、実際に変更された列のみが更新されます。 (何も変更されていない場合は、更新プログラムはまったく送信されません)。
グラフの操作
識別子の解決
前述のように、EF Core では、特定の主キー値を持つ任意のエンティティのインスタンスを 1 つだけ追跡できます。 グラフを操作するときは、この不変性が維持されるようにグラフを作成することが理想的であり、コンテキストは 1 つの作業単位にのみ使用する必要があります。 グラフに重複が含まれている場合は、グラフを EF に送信する前にグラフを処理して、複数のインスタンスを 1 つに統合する必要があります。 これは、インスタンスの値とリレーションシップが競合する場合は簡単ではない可能性があるため、競合の解決を回避するために、アプリケーション パイプラインでできるだけ早く重複を統合する必要があります。
すべての新規/既存のエンティティ
グラフを操作する例として、関連する投稿のコレクションと共にブログを挿入または更新する方法があります。 グラフ内のすべてのエンティティを挿入する必要がある場合、またはすべて更新する必要がある場合、プロセスは 1 つのエンティティについて上記と同じです。 たとえば、次のように作成されたブログと投稿のグラフです。
var blog = new Blog
{
Url = "http://sample.com", Posts = new List<Post> { new Post { Title = "Post 1" }, new Post { Title = "Post 2" }, }
};
次のように挿入できます。
public static async Task InsertGraph(DbContext context, object rootEntity)
{
context.Add(rootEntity);
await context.SaveChangesAsync();
}
Add の呼び出しにより、ブログと挿入されるすべての投稿がマークされます。
同様に、グラフ内のすべてのエンティティを更新する必要がある場合は、Update を使用できます。
public static async Task UpdateGraph(DbContext context, object rootEntity)
{
context.Update(rootEntity);
await context.SaveChangesAsync();
}
ブログとそのすべての投稿は、更新対象としてマークされます。
新規エンティティと既存エンティティの組み合わせ
自動生成されたキーを使用すると、挿入が必要なエンティティと更新が必要なエンティティがグラフに混在している場合でも、挿入と更新の両方に更新を再度使用できます。
public static async Task InsertOrUpdateGraph(DbContext context, object rootEntity)
{
context.Update(rootEntity);
await context.SaveChangesAsync();
}
更新では、キー値が設定されていない場合は、グラフ、ブログ、または投稿内のすべてのエンティティが挿入対象としてマークされ、他のすべてのエンティティは更新対象としてマークされます。
以前と同様に、自動生成されたキーを使用しない場合は、クエリと一部の処理を使用できます。
public static async Task InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts
.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
}
await context.SaveChangesAsync();
}
削除の処理
多くの場合、エンティティが存在しない場合は削除する必要があるため、削除は処理が難しい場合があります。 これに対処する 1 つの方法は、エンティティが実際に削除されるのではなく、削除済みとしてマークされるように "論理的な削除" を使用することです。 削除は更新プログラムと同じになります。 論理的な削除は、 クエリ フィルターを使用して実装できます。
真の削除の場合、一般的なパターンは、クエリパターンを拡張して、グラフの差分を取ることです。 例えば次が挙げられます。
public static async Task InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts
.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
foreach (var post in existingBlog.Posts)
{
if (!blog.Posts.Any(p => p.PostId == post.PostId))
{
context.Remove(post);
}
}
}
await context.SaveChangesAsync();
}
TrackGraph
内部的には、追加、アタッチ、および更新では、グラフ トラバーサルが使用され、エンティティを追加 (挿入)、変更済み (更新)、変更なし (何もしない)、または削除済み (削除) としてマークする必要があるかどうかがエンティティごとに決定されます。 このメカニズムは、TrackGraph API を介して公開されます。 たとえば、クライアントがエンティティのグラフを送り返すときに、その処理方法を示すフラグを各エンティティに設定するとします。 その後、TrackGraph を使用してこのフラグを処理できます。
public static async Task SaveAnnotatedGraph(DbContext context, object rootEntity)
{
context.ChangeTracker.TrackGraph(
rootEntity,
n =>
{
var entity = (EntityBase)n.Entry.Entity;
n.Entry.State = entity.IsNew
? EntityState.Added
: entity.IsChanged
? EntityState.Modified
: entity.IsDeleted
? EntityState.Deleted
: EntityState.Unchanged;
});
await context.SaveChangesAsync();
}
フラグは、例をわかりやすくするためにエンティティの一部としてのみ表示されます。 通常、フラグはリクエストに含まれるデータ転送オブジェクト(DTO)やその他の状態の一部になります。
.NET