次の方法で共有


エンティティの明示的な追跡

DbContext インスタンスによって、エンティティに加えられる変更が追跡されます。 さらに、これらの追跡対象エンティティによって、SaveChanges が呼び出されたときにデータベースへの変更が実行されます。

Entity Framework Core (EF Core) の変更の追跡が最も効果を発揮するのは、同じ DbContext インスタンスを使用してエンティティのクエリを実行し、SaveChanges を呼び出して更新するときです。 これは、EF Core によって、クエリされたエンティティの状態が自動的に追跡され、SaveChanges が呼び出されたときにこれらのエンティティに加えられた変更が検出されるためです。 このアプローチについては、「EF Core での変更の追跡」で取り上げられています。

ヒント

このドキュメントは、エンティティの状態と、EF Core での変更の追跡に関する基本を理解していることが前提となっています。 これらのトピックの詳細については、「EF Core での変更の追跡」を参照してください。

ヒント

このドキュメントに含まれているすべてのコードは、GitHub からサンプル コードをダウンロードすることで実行およびデバッグできます。

ヒント

わかりやすくするために、このドキュメントでは SaveChanges などの同期メソッドを使用および参照しています。その非同期バージョンである SaveChangesAsync などは使用していません。 特に明記されていない限り、非同期メソッドの呼び出しと待機は置き換えることができます。

はじめに

エンティティは、後からコンテキストがそれらのエンティティを追跡するように、明示的に DbContext に "アタッチ" できます。 これが有用であるのは主に以下の場合です。

  1. データベースに挿入される新しいエンティティの作成。
  2. 以前に "異なる" DbContext インスタンスによってクエリされた接続解除エンティティの再アタッチ。

これらのうち、1 つめはほとんどのアプリケーションで必要になり、主に DbContext.Add メソッドによって処理されます。

2 つ目は、"エンティティが追跡されていないとき" に、エンティティまたはそれらのリレーションシップを変更するアプリケーションでのみ必要とされます。 たとえば Web アプリケーションは、ユーザーが変更を加えてエンティティを送り返す Web クライアントにエンティティを送信することがあります。 これらのエンティティは、最初は DbContext からクエリが実行されたのに、その後クライアントに送信されたときに、そのコンテキストから切断されたので、"接続解除" と呼ばれます。

ここで Web アプリケーションは、これらのエンティティを再アタッチして、それらが再度追跡され、加えられた変更を示すようにし、SaveChanges がデータベースに対して適切な更新を加えることができるようにする必要があります。 これは主に、DbContext.AttachDbContext.Update メソッドによって処理されます。

ヒント

エンティティを、クエリの発行元と "同じ DbContext インスタンス" にアタッチすることは、通常は不要であるはずです。 追跡なしのクエリを定期的に実行した後、返されたエンティティを同じコンテキストにアタッチしないでください。 これは追跡クエリを使用するよりも遅くなります。また、シャドウ プロパティ値が見つからないなどの問題が発生して、値を明確にするのがより困難になる可能性もあります。

生成されたキー値と明示的なキー値の違い

既定では、整数と GUID であるキー プロパティは、自動的に生成されたキー値を使用するように構成されます。 これには、変更の追跡に関して大きな利点があります。未設定のキー値は、そのエンティティが "新しい" ことを示すからです。 "新しい" とは、それがまだデータベースに挿入されたことがないことを意味します。

2 つのモデルは以下のセクションで使用されます。 1 つ目は、生成されたキー値を使用しないように構成されています。

public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

各例では、すべてが非常に明確で従いやすいため、生成されていない (つまり、明示的に設定された) キー値が最初に示されています。 この次に、生成されたキー値が使用される例が続きます。

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

単純な整数キーでの既定は生成されたキー値を使用することなので、ここでは、このモデルのキー プロパティに追加の構成は不要であることに注目してください。

新しいエンティティの挿入

明示的なキー値

エンティティは、SaveChanges によって挿入される Added 状態で追跡する必要があります。 エンティティは、通常、DbSet<TEntity> に対して DbContext.AddDbContext.AddRangeDbContext.AddAsyncDbContext.AddRangeAsync、または同等のメソッドのいずれかを呼び出すことによって、Added 状態になります。

ヒント

これらのメソッドはすべて、変更の追跡のコンテキストでは同様に動作します。 詳細については、「その他の変更の追跡機能」を参照してください。

たとえば、新しいブログの追跡を開始するには、次のようにします。

context.Add(
    new Blog { Id = 1, Name = ".NET Blog", });

この呼び出しに続く変更トラッカーのデバッグ ビューを調べると、コンテキストにより、Added 状態になっている新しいエンティティが追跡中であることが示されます。

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

ただし Add メソッドは、個々のエンティティに対して機能するだけではありません。 これらは実際には、関連するエンティティのグラフ全体の追跡を開始し、それらすべてを Added 状態にします。 たとえば、新しいブログと、関連付けられている新しい投稿を挿入するには、次のようにします。

context.Add(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

コンテキストではこれで、Added としてのこれらのエンティティすべてが追跡中です。

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
  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}
Post {Id: 2} Added
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

上の例では、Id キー プロパティに明示的な値が設定されていることに注目してください。 これは、ここに示したモデルが、自動的に生成されるキー値ではなく明示的に設定されたキー値を使用するように構成されているためです。 生成されたキーを使用しない場合は、Add を呼び出す "前に" キー プロパティを明示的に設定する必要があります。 これらのキー値は次に、SaveChanges が呼び出されたときに挿入されます。 たとえば、SQLite を使用する場合は、次のようになります。

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);

これらのエンティティはすべて、SaveChanges が完了した後の Unchanged 状態で追跡されます。これらのエンティティはこの時点で、データベース内に存在しているためです。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

生成されたキー値

前述したように、整数と GUID であるキー プロパティは、既定では自動的に生成されたキー値を使用するように構成されます。 これは、アプリケーションでは、"どのキー値も明示的に設定してはいけない" ことを意味します。 たとえば、新しいブログと投稿すべてを、生成されたキー値を使用して挿入するには、次のようにします。

context.Add(
    new Blog
    {
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                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
            {
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

明示的なキー値と同様に、コンテキストではこれで、Added としてのこれらのエンティティすべてが追跡中です。

Blog {Id: -2147482644} Added
  Id: -2147482644 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
  Id: -2147482637 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: -2147482644}

この場合、エンティティごとに一時キー値が生成されていることに注目してください。 これらの値は、SaveChanges が呼び出される、つまりデータベースから実際のキー値が読み取られて戻される時点まで、EF Core によって使用されます。 たとえば、SQLite を使用する場合は、次のようになります。

-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

SaveChanges が完了すると、すべてのエンティティが実際のキー値で更新されて、Unchanged 状態で追跡されます。それらがデータベース内での状態と一致するようになっているためです。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

これは、明示的なキー値を使用していた前の例とまったく同じ最終状態です。

ヒント

生成されたキー値の使用時であっても、明示的なキー値を引き続き設定できます。 その後 EF Core により、このキー値を使用した挿入が試みられます。 ID 列を使用する SQL Server を含めて、一部のデータベース構成ではそのような挿入がサポートされておらず、スローされます (回避策についてはこちらのドキュメントを参照)。

既存のエンティティのアタッチ

明示的なキー値

クエリから返されたエンティティは、Unchanged 状態で追跡されます。 Unchanged 状態は、そのエンティティが、クエリされた後に変更されていないことを意味します。 おそらく HTTP 要求で Web クライアントから返された接続解除エンティティは、DbSet<TEntity> に対して DbContext.AttachDbContext.AttachRange、または同等のメソッドのいずれかを使用して、この状態にできます。 たとえば、既存のブログの追跡を開始するには、次のようにします。

context.Attach(
    new Blog { Id = 1, Name = ".NET Blog", });

Note

この記事の例では、わかりやすくするために、new を使用して明示的にエンティティを作成しています。 通常、エンティティ インスタンスは、クライアントから逆シリアル化されたり、HTTP Post でデータから作成されたりなど、別のソースから来ることになります。

この呼び出しに続く変更トラッカーのデバッグ ビューを調べると、エンティティは Unchanged 状態で追跡されていることが示されます。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Add と同様に、Attach では実際には、接続されたエンティティのグラフ全体が Unchanged 状態に設定されます。 たとえば、既存のブログと、関連付けられている既存の投稿を添付するには、次のようにします。

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

コンテキストではこれで、Unchanged としてのこれらのエンティティすべてが追跡中です。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

この時点で SaveChanges を呼び出しても影響はありません。 すべてのエンティティは Unchanged としてマークされているので、データベース内に更新するものは何もありません。

生成されたキー値

前述したように、整数と GUID であるキー プロパティは、既定では自動的に生成されたキー値を使用するように構成されます。 これには、接続解除エンティティを操作する場合に大きな利点があります。未設定のキー値は、そのエンティティがまだデータベースに挿入されていないことを示すからです。 これにより、変更トラッカーでは新しいエンティティを自動的に検出して、それらを Added 状態にできます。 たとえば、ブログと投稿から成るこのグラフを添付することを考えてみます。

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

ブログにはキー値 1 があり、それが既にデータベース内に存在することを示しています。 2 つの投稿にもキー値が設定されていますが、3 つ目には設定されていません。 EF Core からは、このキー値は、整数に対する CLR の既定値である 0 と認識されます。 これにより、新しいエンティティは EF Core によって、Unchanged ではなく Added としてマークされる結果になります。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
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}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'

この時点で SaveChanges を呼び出しても、Unchanged エンティティには何の影響もありませんが、新しいエンティティがデータベースに挿入されます。 たとえば、SQLite を使用する場合は、次のようになります。

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

ここで注意すべき重要なことは、生成されたキー値を使用すると、EF Core では新しいものを、切断されたグラフ内の既存のエンティティと自動的に区別できるという点です。 一言で言えば、生成されたキーの使用時には、エンティティにキー値が設定されていなければ常に、EF Core によってエンティティが挿入されます。

既存のエンティティの更新

明示的なキー値

DbSet<TEntity> に対する DbContext.UpdateDbContext.UpdateRange、および同等のメソッドは、エンティティが Unchanged 状態ではなく Modified にされること以外、上で説明した Attach メソッドとまったく同様に動作します。 たとえば、Modified としての既存のブログの追跡を開始するには、次のようにします。

context.Update(
    new Blog { Id = 1, Name = ".NET Blog", });

この呼び出しに続く変更トラッカーのデバッグ ビューを調べると、コンテキストにより、Modified 状態になっているこのエンティティが追跡中であることが示されます。

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: []

AddAttach と同様に、Update では実際には、関連するエンティティの "グラフ全体" が Modified としてマークされます。 たとえば、既存のブログと、関連付けられている Modified としての既存の投稿を添付するには、次のようにします。

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

コンテキストではこれで、Modified としてのこれらのエンティティすべてが追跡中です。

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

この時点で SaveChanges を呼び出すと、これらのエンティティすべてに対する更新がデータベースに送信されます。 たとえば、SQLite を使用する場合は、次のようになります。

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

生成されたキー値

Attach と同様に、生成されたキー値には、Update と同じ大きな利点があります。未設定のキー値は、エンティティが新しく、まだデータベースに挿入されていないことを示すからです。 Attach と同様、これにより DbContext では、新しいエンティティを自動的に検出して、それらを Added 状態にできます。 たとえば、ブログと投稿から成るこのグラフが指定された Update 呼び出しを考えてみます。

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

Attach の例と同様に、キー値がない投稿は新しい投稿として検出され、Added 状態に設定されます。 その他のエンティティは Modified としてマークされます。

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
  Id: -2147482633 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

この時点で SaveChanges を呼び出すと、既存のエンティティすべてについての更新がデータベースに送信される一方で、新しいエンティティが挿入されます。 たとえば、SQLite を使用する場合は、次のようになります。

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

これは、切断されたグラフから更新と挿入を生成する、非常に簡単な方法です。 ただし、一部のプロパティ値は変更されていない可能性がある場合でも、追跡対象の全エンティティのプロパティすべてについて更新または挿入がデータベースに送信されることになります。 これはあまり気にしないでください。小さなグラフを使用するアプリケーションの多くで、これは更新を生成するうえで簡単かつ実用的な方法になります。 そうは言っても、他のより複雑なパターンでは、「EF Core での ID 解決」で説明しているように、より効率的な更新が発生する場合があります。

既存のエンティティの削除

エンティティが SaveChanges によって削除されるようにするには、エンティティを Deleted 状態で追跡する必要があります。 エンティティは、通常、DbSet<TEntity> に対して DbContext.RemoveDbContext.RemoveRange、または同等のメソッドのいずれかを呼び出すことによって、Deleted 状態になります。 たとえば、既存の投稿を Deleted としてマークするには、次のようにします。

context.Remove(
    new Post { Id = 2 });

この呼び出しに続く変更トラッカーのデバッグ ビューを調べると、コンテキストにより、Deleted 状態になっているエンティティが追跡中であることが示されます。

Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: <null> FK
  Content: <null>
  Title: <null>
  Blog: <null>

このエンティティは、SaveChanges が呼び出されると削除されます。 たとえば、SQLite を使用する場合は、次のようになります。

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

SaveChanges の完了後、削除されたエンティティはデータベースに存在しなくなったので、DbContext からデタッチされます。 その結果、追跡中のエンティティが存在しないためにデバッグ ビューは空です。

依存または子エンティティの削除

グラフから依存または子エンティティを削除する方が、プリンシパルまたは親エンティティを削除するよりも簡単です。 詳細については、次のセクションと、「外部キーとナビゲーションの変更」を参照してください。

new によって作成されたエンティティに対して Remove を呼び出すのは普通ではありません。 さらに、AddAttachUpdate とは異なり、UnchangedModified の状態でまだ追跡されていないエンティティに対して Remove を呼び出すのは一般的ではありません。 それよりも、関連するエンティティのうち、1 つのエンティティまたはグラフを追跡してから、削除する必要があるエンティティに対して Remove を呼び出すのが一般的です。 追跡されるエンティティから成るこのグラフは、一般に、次のいずれかによって作成されます。

  1. エンティティに対するクエリの実行
  2. 前のセクションで説明したような、接続解除エンティティのグラフに対する Attach または Update メソッドの使用。

たとえば前のセクションのコードでは、高い確率で、クライアントから投稿を取得してから、次のようなことを行います。

context.Attach(post);
context.Remove(post);

これは、追跡されていないエンティティに対して Remove を呼び出すと、それは最初にアタッチされてから Deleted としてマークされるので、前の例とまったく同様に動作します。

より現実的な例では、エンティティのグラフが最初にアタッチされてから、それらのエンティティの一部が削除済みとしてマークされます。 次に例を示します。

// Attach a blog and associated posts
context.Attach(blog);

// Mark one post as Deleted
context.Remove(blog.Posts[1]);

すべてのエンティティは、Remove の呼び出し対象であるものを除き、 Unchanged としてマークされます。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

このエンティティは、SaveChanges が呼び出されると削除されます。 たとえば、SQLite を使用する場合は、次のようになります。

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

SaveChanges の完了後、削除されたエンティティはデータベースに存在しなくなったので、DbContext からデタッチされます。 その他のエンティティは Unchanged 状態のままになります。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
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}

プリンシパルまたは親エンティティの削除

2 つのエンティティ型を接続する各リレーションシップには、プリンシパルまたは親の側と、依存または子の側があります。 依存または子エンティティは、外部キー プロパティがあるものです。 1 対多のリレーションシップでは、プリンシパルまたは親は "1" の側に、依存または子は "多" の側にあります。 詳細については、「リレーションシップ」を参照してください。

前の例では、ブログ投稿の 1 対多リレーションシップにおける依存または子エンティティである投稿を削除していました。 依存または子エンティティを削除しても他のエンティティに一切影響はないため、これは比較的簡単です。 他方では、プリンシパルまたは親エンティティを削除すると、依存または子エンティティにも必ず影響します。 そうでないと、存在しなくなった主キー値を参照する外部キー値が残されることになります。 これは無効なモデル状態であり、ほとんどのデータベースで参照制約エラーが発生する結果となります。

この無効なモデル状態は、次の 2 つの方法で処理できます。

  1. FK 値の null への設定。 これは、依存または子が、プリンシパルまたは親にもはや関連付けられていないことを示します。 外部キーが Null 許容である必要がある省略可能なリレーションシップについては、これが既定値です。 FK の null への設定は、外部キーは通常 null 非許容である必須リレーションシップに対しては無効です。
  2. 依存または子の削除。 これは必須リレーションシップでの既定値であり、オプションのリレーションシップにも有効です。

変更の追跡とリレーションシップの詳細については、「外部キーとナビゲーションの変更」を参照してください。

省略可能なリレーションシップ

Post.BlogId 外部キー プロパティは、ここで使用してきたモデルで Null 許容となっています。 これは、リレーションシップは省略可能であるため、EF Core の既定の動作では、ブログが削除されたときに BlogId 外部キー プロパティが null に設定されることを意味します。 次に例を示します。

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

Remove の呼び出しに続く変更トラッカーのデバッグ ビューを調べると、予期したとおり、ブログが Deleted としてマークされるようになっていることが示されます。

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

さらに興味深いことに、関連する投稿すべてが Modified としてマークされるようになっています。 これは、各エンティティの外部キー プロパティが null に設定されているためです。 SaveChanges を呼び出して、データベースで各投稿の外部キー値を null に更新してから、ブログを削除します。

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p2;
SELECT changes();

SaveChanges の完了後、削除されたエンティティはデータベースに存在しなくなったので、DbContext からデタッチされます。 他のエンティティは、データベースの状態と一致する null 外部キー値で Unchanged としてマークされるようになっています。

Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: <null> FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: <null> FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

必須リレーションシップ

Post.BlogId 外部キー プロパティが null 非許容の場合、ブログと投稿との間のリレーションシップは "必須" になります。 この状況では、プリンシパルまたは親が削除されると、既定で、EF Core によって依存または子エンティティが削除されます。 たとえば、前の例のように関連する投稿が含まれるブログを削除します。

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

Remove の呼び出しに続く変更トラッカーのデバッグ ビューを調べると、予期したとおり、この場合もブログが Deleted としてマークされるようになっていることが示されます。

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
  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}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

この場合は興味深いことに、関連するすべての投稿も、Deleted としてマークされています。 SaveChanges を呼び出すと、ブログとすべての関連する投稿が、データベースから削除されます。

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;

SaveChanges の完了後、削除されたすべてのエンティティは、データベースに存在しなくなっているため、DbContext からデタッチされます。 したがって、デバッグ ビューの出力は空です。

Note

このドキュメントでは、EF Core でのリレーションシップの操作について基礎的な内容のみを説明しています。 リレーションシップのモデリングの詳細については「リレーションシップ」を、SaveChanges を呼び出す際の依存または子エンティティの更新と削除の詳細については、「外部キーとナビゲーションの変更」を参照してください。

TrackGraph を使用したカスタム追跡

ChangeTracker.TrackGraph は、追跡の前にすべてのエンティティ インスタンスのコールバックを生成することを除き、AddAttachUpdate と同様に機能します。 これを利用すると、グラフ内の個々のエンティティを追跡する方法を決定するときにカスタム ロジックを使用できます。

たとえば、生成されたキー値があるエンティティを追跡するときに EF Core で使用される規則について考えます。キー値が 0 の場合、エンティティは新規であり、その挿入が必要です。 この規則を拡張して、キー値が負の値の場合はエンティティを削除する必要があるとしましょう。 こうすると、切断されたグラフのエンティティの主キー値を変更して、削除されたエンティティをマークできます。

blog.Posts.Add(
    new Post
    {
        Title = "Announcing .NET 5.0",
        Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
    }
);

var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;

この切断されたグラフは、その後、TrackGraph を使用して追跡できます。

public static void UpdateBlog(Blog blog)
{
    using var context = new BlogsContext();

    context.ChangeTracker.TrackGraph(
        blog, node =>
        {
            var propertyEntry = node.Entry.Property("Id");
            var keyValue = (int)propertyEntry.CurrentValue;

            if (keyValue == 0)
            {
                node.Entry.State = EntityState.Added;
            }
            else if (keyValue < 0)
            {
                propertyEntry.CurrentValue = -keyValue;
                node.Entry.State = EntityState.Deleted;
            }
            else
            {
                node.Entry.State = EntityState.Modified;
            }

            Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
        });

    context.SaveChanges();
}

上記のコードでは、グラフ内のエンティティごとに、エンティティを追跡する前に主キー値がチェックされます。 未設定 (0) のキー値であれば、コードにより、EF Core によって通常実行されることが行われます。 つまり、キーが設定されていない場合、エンティティは Added としてマークされます。 キーが設定されていて、値が負でない場合、エンティティは Modified とマークされます。 ただし、負のキー値が見つかった場合は、負でないその実数値が復元され、エンティティは Deleted として追跡されます。

このコードを実行すると、出力は次のようになります。

Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added

Note

わかりやすくするために、このコードでは、各エンティティに Id という名前の、整数の主キー プロパティがあることが前提となっています。 これは、抽象基本クラスやインターフェイスにコード化できます。 別の方法として、このコードがどの型のエンティティでも機能するように、IEntityType メタデータから 1 つまたは複数の主キー プロパティを取得できます。

TrackGraph には 2 つのオーバーロードがあります。 上で使用した単純なオーバーロードでは、EF Core によって、グラフの走査をいつ停止するかが決定されます。 具体的には、エンティティが既に追跡されている場合や、コールバックによってエンティティの追跡を開始されない場合に、特定のエンティティから新しい関連エンティティへのアクセスが停止されます。

高度なオーバーロードである ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>) には、ブール値を返すコールバックが用意されています。 このコールバックから false が返された場合はグラフ走査が停止され、それ以外の場合は続行されます。 このオーバーロードを使用する場合は、注意深く無限ループを避ける必要があります。

高度なオーバーロードでは、TrackGraph に状態を指定することもできます。この状態は次に、各コールバックに渡されます。