接続解除エンティティDisconnected entities

DbContext インスタンスでは、データベースから返されるエンティティを自動的に追跡します。A DbContext instance will automatically track entities returned from the database. 必要に応じて SaveChanges が呼び出され、データベースが更新されたときに、これらのエンティティに対して行われた変更が検出されます。Changes made to these entities will then be detected when SaveChanges is called and the database will be updated as needed. 詳細については、「Basic Save」(基本の保存) および「Related Data」(関連データ) を参照してください。See Basic Save and Related Data for details.

しかし、エンティティが 1 つのコンテキスト インスタンスを使って照会され、別のインスタンスを使って保存される場合があります。However, sometimes entities are queried using one context instance and then saved using a different instance. これは、エンティティの照会、クライアントへの送信、変更、要求内でのサーバーへの返送、および保存が行われる Web アプリケーションなどの "接続解除" シナリオで、頻繁に発生します。This often happens in "disconnected" scenarios such as a web application where the entities are queried, sent to the client, modified, sent back to the server in a request, and then saved. この場合、2 番目のコンテキスト インスタンスでは、エンティティが新しいか (挿入する必要がある) または既存か (更新する必要があるか) を把握する必要があります。In this case, the second context instance needs to know whether the entities are new (should be inserted) or existing (should be updated).

ヒント

この記事のサンプルは GitHub で確認できます。You can view this article's sample on GitHub.

ヒント

EF Core では、指定されたプライマリ キー値を持つ任意のエンティティの 1 つのインスタンスしか追跡できません。EF Core can only track one instance of any entity with a given primary key value. これを回避する最善の方法は、各作業単位に一時的なコンテキストを使用して、最初は空のコンテキストにエンティティをアタッチし、それらのエンティティを保存してから、コンテキストが消去および破棄されるようにすることです。The best way to avoid this being an issue is to use a short-lived context for each unit-of-work such that the context starts empty, has entities attached to it, saves those entities, and then the context is disposed and discarded.

新しいエンティティの識別Identifying new entities

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

最も処理しやすいのは、エンティティが新規か既存かをクライアントからサーバーに知らせるタイミングです。The simplest case to deal with is when the client informs the server whether the entity is new or existing. たとえば、新しいエンティティを挿入するための要求は、多くの場合、既存のエンティティを更新するための要求とは異なります。For example, often the request to insert a new entity is different from the request to update an existing entity.

このセクションの以降の説明では、別の方法で挿入か更新かを判断する必要があるケースを取り上げます。The remainder of this section covers the cases where it necessary to determine in some other way whether to insert or update.

自動生成キーを利用するWith auto-generated keys

自動生成されたキーの値は、エンティティを挿入する必要があるか、または更新する必要があるかを判断するために使用されることが、よくあります。The value of an automatically generated key can often be used to determine whether an entity needs to be inserted or updated. キーが設定済みでない (つまり、null、ゼロなどの CLR 既定値をまだ保持している) 場合、エンティティは常に新規となり、挿入する必要があります。If the key has not been set (i.e. it still has the CLR default value of null, zero, etc.), then the entity must be new and needs inserting. 一方、キー値が設定済みの場合、エンティティは常に既に事前保存済みであり、更新する必要があります。On the other hand, if the key value has been set, then it must have already been previously saved and now needs updating. つまり、キーが値を保持している場合、エンティティは照会され、クライアントに送信されて、更新されるようになります。In other words, if the key has a value, then entity was queried, sent to the client, and has now come back to be updated.

エンティティ型がわかっている場合、設定されていないキーのチェックは簡単です。It is easy to check for an unset key when the entity type is known:

public static bool IsItNew(Blog blog) 
    => blog.BlogId == 0;

しかし、EF でもエンティティ型やキーの種類をチェックするための組み込みの方法を備えています。However, EF also has a built-in way to do this for any entity type and key type:

public static bool IsItNew(DbContext context, object entity) 
    => !context.Entry(entity).IsKeySet;

ヒント

エンティティがコンテキストによって追跡されると、エンティティが追加済みの状態になっていても、すぐにキーが設定されます。Keys are set as soon as entities are tracked by the context, even if the entity is in the Added state. これは、TrackGraph API を使用している場合など、エンティティのグラフを走査して各グラフで行う操作を決定する際に役立ちます。This helps when traversing a graph of entities and deciding what to do with each, such as when using the TrackGraph API. キー値は、エンティティを追跡するために何らかの呼び出しが行われる "" に、ここに示された方法でのみ使用する必要があります。The key value should only be used in the way shown here before any call is made to track the entity.

他のキーを利用するWith other keys

キー値が自動生成されない場合、他のいくつかのメカニズムが、新しいエンティティの識別に利用されます。Some other mechanism is needed to identify new entities when key values are not generated automatically. これを行うための一般的な方法として、次の 2 つがあります。There are two general approaches to this:

  • エンティティに対してクエリを実行するQuery for the entity
  • クライアントからフラグを渡すPass a flag from the client

エンティティに対してクエリを実行するには、単純に Find メソッドを使用します。To query for the entity, just use the Find method:

public static bool IsItNew(BloggingContext context, Blog blog)
    => context.Blogs.Find(blog.BlogId) == null;

クライアントからフラグを渡す完全なコードの提示は、このドキュメントでは行いません。It is beyond the scope of this document to show the full code for passing a flag from a client. Web アプリでは、多くの場合、さまざまなアクションに対して異なる要求を行うか、または要求内で何らかの状態を渡して、コントローラーでその状態を抽出することになります。In a web app, it usually means making different requests for different actions, or passing some state in the request then extracting it in the controller.

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

挿入または更新のどちらが必要かがわかったら、次のように Add または Update を適切に使用できます。If it is known whether or not an insert or update is needed, then either Add or Update can be used appropriately:

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 メソッドを使用できます。However, if the entity uses auto-generated key values, then the Update method can be used for both cases:

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

Update メソッドは通常、挿入ではなく、更新用のエンティティをマークします。The Update method normally marks the entity for update, not insert. ただし、エンティティが自動生成されたキーを保持しており、設定済みのキー値がない場合、そのエンティティは更新用ではなく、自動的に挿入用にマークされます。However, if the entity has a auto-generated key, and no key value has been set, then the entity is instead automatically marked for insert.

ヒント

この動作は、EF Core 2.0 で導入されました。This behavior was introduced in EF Core 2.0. それ以前のリリースでは、Add または Update のどちらかを常に明示的に選択する必要があります。For earlier releases it is always necessary to explicitly choose either Add or Update.

エンティティが自動生成されたキーを使用していない場合、アプリケーションでは、エンティティの挿入または更新のどちらが必要かを、次の例のように判断する必要があります。If the entity is not using auto-generated keys, then the application must decide whether the entity should be inserted or updated: For example:

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();
}

この場合の手順は、次のようになります。The steps here are:

  • Find で null が返された場合、データベースがこの ID のブログを既に含んでいるわけではないため、Add を呼び出して挿入用にマークします。If Find returns null, then the database doesn't already contain the blog with this ID, so we call Add mark it for insertion.
  • Find がエンティティを返した場合は、エンティティがデータベースに存在しており、コンテキストは既存のエンティティを追跡するようになります。If Find returns an entity, then it exists in the database and the context is now tracking the existing entity
    • SetValues を使用して、このエンティティ上のすべてのプロパティの値を、クライアントから受信したプロパティに設定します。We then use SetValues to set the values for all properties on this entity to those that came from the client.
    • SetValues の呼び出しでは、必要に応じて更新されるエンティティをマークします。The SetValues call will mark the entity to be updated as needed.

ヒント

SetValues は、追跡されたエンティティのプロパティに別の値を保持しているプロパティを、変更済みとしてマークすることしか行いません。SetValues will only mark as modified the properties that have different values to those in the tracked entity. これは、更新が送信されると、実際に変更されたそれらの列のみが更新されることを意味します This means that when the update is sent, only those columns that have actually changed will be updated. (また、何も変更されなかった場合は、更新もまったく送信されません)。(And if nothing has changed, then no update will be sent at all.)

グラフを操作するWorking with graphs

識別子の解決Identity resolution

上述したように、EF Core では、指定されたプライマリ キー値を持つ任意のエンティティの 1 つのインスタンスしか追跡できません。As noted above, EF Core can only track one instance of any entity with a given primary key value. グラフを操作するとき、この不変の条件が維持されるようにグラフが理想的に作成される必要があり、コンテキストは 1 つの作業単位のみに対して使用される必要があります。When working with graphs the graph should ideally be created such that this invariant is maintained, and the context should be used for only one unit-of-work. グラフに重複値が含まれている場合、EF に送信して複数のインスタンスを 1 つに統合する前に、そのグラフを処理する必要があります。If the graph does contain duplicates, then it will be necessary to process the graph before sending it to EF to consolidate multiple instances into one. インスタンスに競合する値やリレーションシップがある場合、この処理が複雑になるので、アプリケーションのパイプラインで競合の解決が発生しないように、できるだけすぐに複数の値の統合を実行する必要があります。This may not be trivial where instances have conflicting values and relationships, so consolidating duplicates should be done as soon as possible in your application pipeline to avoid conflict resolution.

すべての新規/既存のエンティティAll new/all existing entities

グラフの操作の例では、関連する投稿のコレクションと共にブログを挿入または更新しています。An example of working with graphs is inserting or updating a blog together with its collection of associated posts. グラフ内のすべてのエンティティが挿入される必要がある場合、またはすべてが更新される必要がある場合、プロセスは上述した単一のエンティティの場合と同じです。If all the entities in the graph should be inserted, or all should be updated, then the process is the same as described above for single entities. たとえば、作成されたブログと投稿のグラフは次のよになります。For example, a graph of blogs and posts created like this:

var blog = new Blog
{
    Url = "http://sample.com",
    Posts = new List<Post>
    {
        new Post {Title = "Post 1"},
        new Post {Title = "Post 2"},
    }
};

また、次のように挿入できます。can be inserted like this:

public static void InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    context.SaveChanges();
}

Add の呼び出しでは、ブログとすべての投稿が挿入されるようにマークします。The call to Add will mark the blog and all the posts to be inserted.

同様に、グラフ内のすべてのエンティティが更新される必要がある場合は、Update を使用できます。Likewise, if all the entities in a graph need to be updated, then Update can be used:

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

ブログとそのすべての投稿が、更新されるようにマークされます。The blog and all its posts will be marked to be updated.

新規と既存のエンティティの混合Mix of new and existing entities

自動生成されたキーを使用する場合、挿入を必要とするエンティティと更新を必要とするエンティティがグラフ内に混在していても、挿入と更新の両方に Update を再使用できます。With auto-generated keys, Update can again be used for both inserts and updates, even if the graph contains a mix of entities that require inserting and those that require updating:

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

Update は、設定されたキー値がない場合は、グラフ、ブログ、または投稿内のエンティティを挿入用にマークし、その他のエンティティはすべて更新用にマークします。Update will mark any entity in the graph, blog or post, for insertion if it does not have a key value set, while all other entities are marked for update.

前述のように、自動生成キーを使用しない場合は、クエリおよびいくつかの処理を使用できます。As before, when not using auto-generated keys, a query and some processing can be used:

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();
}

削除の処理Handling deletes

エンティティの不在は、エンティティが削除されていることを意味することがよくあるため、削除は慎重に扱う必要があります。Delete can be tricky to handle since often the absence of an entity means that it should be deleted. これに対処する方法の 1 つは、エンティティが実際に削除されるのではなく、削除としてマークされるように、"論理的な削除" を使用することです。One way to deal with this is to use "soft deletes" such that the entity is marked as deleted rather than actually being deleted. これで、削除は更新と同様になります。Deletes then becomes the same as updates. 論理的な削除は、クエリ フィルターを使用して実装できます。Soft deletes can be implemented in using query filters.

実際の削除では、一般的なパターンとしてクエリ パターンの拡張機能を使用して、本質的なグラフの差分特定を行います。例:For true deletes, a common pattern is to use an extension of the query pattern to perform what is essentially a graph diff. For example:

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();
}

TrackGraphTrackGraph

内部的には、Add、Attach、および Update では、追加済み (挿入する)、変更済み (更新する)、変更なし (何もしない)、または削除済み (削除) としてマークする必要があるかどうかに関して各エンティティで行われる判断と共に、グラフ走査を使用します。Internally, Add, Attach, and Update use graph-traversal with a determination made for each entity as to whether it should be marked as Added (to insert), Modified (to update), Unchanged (do nothing), or Deleted (to delete). このメカニズムは TrackGraph API 経由で公開されています。This mechanism is exposed via the TrackGraph API. たとえば、クライアントがエンティティのグラフを返送するときに、処理方法を示したフラグが各エンティティ上に設定されていると仮定しましょう。For example, let's assume that when the client sends back a graph of entities it sets some flag on each entity indicating how it should be handled. このフラグを処理するために、次のように TrackGraph を使用できます。TrackGraph can then be used to process this flag:

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();
}

例を簡単にするために、フラグはエンティティの一部分としてしか表示されていません。The flags are only shown as part of the entity for simplicity of the example. フラグは通常、DTO の一部か、または要求に含まれている他の状態になります。Typically the flags would be part of a DTO or some other state included in the request.