接続解除エンティティ

DbContext インスタンスでは、データベースから返されるエンティティを自動的に追跡します。 必要に応じて SaveChanges が呼び出され、データベースが更新されたときに、これらのエンティティに対して行われた変更が検出されます。 詳細については、「Basic Save」(基本の保存) および「Related Data」(関連データ) を参照してください。

しかし、エンティティが 1 つのコンテキスト インスタンスを使って照会され、別のインスタンスを使って保存される場合があります。 これは、エンティティの照会、クライアントへの送信、変更、要求内でのサーバーへの返送、および保存が行われる Web アプリケーションなどの "接続解除" シナリオで、頻繁に発生します。 この場合、2 番目のコンテキスト インスタンスでは、エンティティが新しいか (挿入する必要がある) または既存か (更新する必要があるか) を把握する必要があります。

ヒント

この記事のサンプルは GitHub で確認できます。

ヒント

EF Core では、指定されたプライマリ キー値を持つ任意のエンティティの 1 つのインスタンスしか追跡できません。 これを回避する最善の方法は、各作業単位に一時的なコンテキストを使用して、最初は空のコンテキストにエンティティをアタッチし、それらのエンティティを保存してから、コンテキストが消去および破棄されるようにすることです。

新しいエンティティの識別

クライアントが新しいエンティティを識別する

最も処理しやすいのは、エンティティが新規か既存かをクライアントからサーバーに知らせるタイミングです。 たとえば、新しいエンティティを挿入するための要求は、多くの場合、既存のエンティティを更新するための要求とは異なります。

このセクションでは、これ以降、別の方法で挿入か更新かを判断する必要があるケースを取り上げます。

自動生成キーを利用する

自動生成されたキーの値は、エンティティを挿入する必要があるか、または更新する必要があるかを判断するために使用されることが、よくあります。 キーが設定済みでない (つまり、null や 0 などの CLR 既定値をまだ保持している) 場合、エンティティは常に新規となり、挿入する必要があります。 一方、キー値が設定済みの場合、エンティティは常に既に事前保存済みであり、更新する必要があります。 つまり、キーが値を保持している場合、エンティティは照会され、クライアントに送信されて、更新されるようになります。

エンティティ型がわかっている場合、設定されていないキーのチェックは簡単です。

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 bool IsItNew(BloggingContext context, Blog blog)
    => context.Blogs.Find(blog.BlogId) == null;

クライアントからフラグを渡す完全なコードの提示は、このドキュメントでは行いません。 Web アプリでは、多くの場合、さまざまなアクションに対して異なる要求を行うか、または要求内で何らかの状態を渡して、コントローラーでその状態を抽出することになります。

単一のエンティティを保存する

挿入または更新のどちらが必要かがわかったら、次のように Add または Update を適切に使用できます。

public static void Insert(DbContext context, object entity)
{
    context.Add(entity);
    context.SaveChanges();
}

public static void Update(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

ただし、エンティティが自動生成されたキー値を使用する場合は、両方のケースで Update メソッドを使用できます。

public static void InsertOrUpdate(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

Update メソッドは通常、挿入ではなく、更新用のエンティティをマークします。 ただし、自動生成されたキーをエンティティが保持しており、設定済みのキー値がない場合、そのエンティティは自動的に挿入用にマークされます。

エンティティが自動生成されたキーを使用していない場合、アプリケーションでは、エンティティの挿入または更新のどちらが必要かを、次の例のように判断する必要があります。

public static void InsertOrUpdate(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs.Find(blog.BlogId);
    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
    }

    context.SaveChanges();
}

この場合の手順は、次のようになります。

  • Find で null が返された場合、データベースがこの ID のブログを既に含んでいるわけではないため、Add を呼び出して挿入用にマークします。
  • Find がエンティティを返した場合は、エンティティがデータベースに存在しており、コンテキストは既存のエンティティを追跡するようになります。
    • SetValues を使用して、このエンティティ上のすべてのプロパティの値を、クライアントから受信したプロパティに設定します。
    • SetValues の呼び出しでは、必要に応じて更新されるエンティティをマークします。

ヒント

SetValues は、追跡されたエンティティのプロパティに別の値を保持しているプロパティを、変更済みとしてマークすることしか行いません。 これは、更新が送信されると、実際に変更されたそれらの列のみが更新されることを意味します (また、何も変更されなかった場合は、更新もまったく送信されません)。

グラフを操作する

識別子の解決

上述したように、EF Core では、指定されたプライマリ キー値を持つ任意のエンティティの 1 つのインスタンスしか追跡できません。 グラフを操作するとき、この不変の条件が維持されるようにグラフが理想的に作成される必要があり、コンテキストは 1 つの作業単位のみに対して使用される必要があります。 グラフに重複値が含まれている場合、EF に送信して複数のインスタンスを 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 void InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    context.SaveChanges();
}

Add の呼び出しでは、ブログとすべての投稿が挿入されるようにマークします。

同様に、グラフ内のすべてのエンティティが更新される必要がある場合は、Update を使用できます。

public static void UpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

ブログとそのすべての投稿が、更新されるようにマークされます。

新規と既存のエンティティの混合

自動生成されたキーを使用する場合、挿入を必要とするエンティティと更新を必要とするエンティティがグラフ内に混在していても、挿入と更新の両方に Update を再使用できます。

public static void InsertOrUpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

Update は、設定されたキー値がない場合は、グラフ、ブログ、または投稿内のエンティティを挿入用にマークし、その他のエンティティはすべて更新用にマークします。

前述のように、自動生成キーを使用しない場合は、クエリおよびいくつかの処理を使用できます。

public static void InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(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);
            }
        }
    }

    context.SaveChanges();
}

削除の処理

エンティティの不在は、エンティティが削除されていることを意味することがよくあるため、削除は慎重に扱う必要があります。 これに対処する方法の 1 つは、エンティティが実際に削除されるのではなく、削除としてマークされるように、"論理的な削除" を使用することです。 これで、削除は更新と同様になります。 論理的な削除は、クエリ フィルターを使用して実装できます。

実際の削除では、一般的なパターンとしてクエリ パターンの拡張機能を使用して、本質的なグラフの差分特定を行います。 次に例を示します。

public static void InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(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);
            }
        }
    }

    context.SaveChanges();
}

TrackGraph

内部的には、Add、Attach、および Update では、追加済み (挿入する)、変更済み (更新する)、変更なし (何もしない)、または削除済み (削除) としてマークする必要があるかどうかに関して各エンティティで行われる判断と共に、グラフ走査を使用します。 このメカニズムは TrackGraph API 経由で公開されています。 たとえば、クライアントがエンティティのグラフを返送するときに、処理方法を示したフラグが各エンティティ上に設定されていると仮定しましょう。 このフラグを処理するために、次のように TrackGraph を使用できます。

public static void 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;
        });

    context.SaveChanges();
}

例を簡単にするために、フラグはエンティティの一部分としてしか表示されていません。 フラグは通常、DTO の一部か、または要求に含まれている他の状態になります。