各 DbContext インスタンスは、エンティティに加えられた変更を追跡します。 これらの追跡対象エンティティは、 SaveChanges が呼び出されたときにデータベースに対する変更を促進します。
Entity Framework Core (EF Core) の変更追跡は、同じ DbContext インスタンスを使用してエンティティのクエリを実行し、 SaveChangesを呼び出して更新する場合に最適です。 これは、EF Core がクエリされたエンティティの状態を自動的に追跡し、SaveChanges が呼び出されたときにこれらのエンティティに加えられた変更を検出するためです。 このアプローチについては、 EF Core の変更追跡に関する記事を参照してください。
ヒント
このドキュメントでは、エンティティの状態と EF Core の変更追跡の基本が理解されていることを前提としています。 これらのトピックの詳細については、 EF Core の変更の追跡 を参照してください。
ヒント
GitHub からサンプル コードをダウンロードすることで、このドキュメントのすべてのコードを実行してデバッグできます。
ヒント
わかりやすくするために、このドキュメントでは、SaveChangesなどの非同期メソッドではなく、SaveChangesAsync などの同期メソッドを使用して参照します。 特に明記されていない限り、非同期メソッドの呼び出しと待機は置き換えることができます。
イントロダクション
エンティティは、コンテキストがそれらのエンティティを追跡するように、 DbContext に明示的に "アタッチ" できます。 これは主に次の場合に役立ちます。
- データベースに挿入される新しいエンティティの作成。
- 別の DbContext インスタンスによって以前に照会された切断されたエンティティを再アタッチする。
これらの 1 つ目は、ほとんどのアプリケーションで必要であり、主に DbContext.Add メソッドによって処理されます。
2 つ目は、エンティティが 追跡されていない間にエンティティまたはそのリレーションシップを変更するアプリケーションでのみ必要です。 たとえば、Web アプリケーションは、ユーザーが変更を加えてエンティティを返送する Web クライアントにエンティティを送信できます。 これらのエンティティは、最初は DbContext から照会されたが、クライアントに送信されたときにそのコンテキストから切断されたため、"切断済み" と呼ばれます。
Web アプリケーションは、これらのエンティティを再び追跡し、 SaveChanges がデータベースに対して適切な更新を行うことができるように行われた変更を示すように、これらのエンティティを再アタッチする必要があります。 これは主に、 DbContext.Attach メソッドと DbContext.Update メソッドによって処理されます。
ヒント
クエリの対象と 同じ DbContext インスタンス にエンティティをアタッチする必要はありません。 追跡なしのクエリを定期的に実行し、返されたエンティティを同じコンテキストにアタッチしないでください。 これは、追跡クエリを使用するよりも遅くなり、シャドウ プロパティの値が見つからないなどの問題が発生し、正しく表示されにくくなることがあります。
生成されたキー値と明示的なキー値
既定では、自動的に生成されるキー値を使用するように整数および GUID キーのプロパティが構成されます。 変更 の追跡には大きな利点があります。未設定のキー値は、エンティティが "新規" であることを示します。 "new" とは、データベースにまだ挿入されていないことを意味します。
次のセクションでは、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; }
}
生成されたキー値の使用は 単純整数キーの既定値であるため、このモデルのキー プロパティには追加の構成は必要ありません。
新しいエンティティの挿入
明示的なキー値
エンティティはAdded
状態で追跡され、SaveChangesによって挿入される必要があります。 エンティティは通常、DbContext.AddでDbContext.AddRange、DbContext.AddAsync、DbContext.AddRangeAsync、DbSet<TEntity>、または同等のメソッドのいずれかを呼び出すことによって、追加された状態になります。
ヒント
これらのメソッドはすべて、変更追跡のコンテキストで同じように動作します。 詳細については、「 その他の変更追跡機能 」を参照してください。
たとえば、新しいブログの追跡を開始するには、次のようにします。
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 クライアントから返される可能性があります)、DbContext.Attach、DbContext.AttachRange、またはDbSet<TEntity>で同等のメソッドを使用して、この状態にすることができます。 たとえば、既存のブログの追跡を開始するには、次のようにします。
context.Attach(
new Blog { Id = 1, Name = ".NET Blog", });
注
ここでは、わかりやすくするために、 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 では、このキー値は 0 と表示されます。これは、整数の CLR の既定値です。 これにより、EF Core は新しいエンティティをAdded
ではなくUnchanged
としてマークします。
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 は常にエンティティを挿入します。
既存のエンティティの更新
明示的なキー値
DbContext.Update、DbContext.UpdateRange、およびDbSet<TEntity>の同等のメソッドは、前述のAttach
メソッドとまったく同じように動作します。ただし、エンティティがModified
状態ではなくUnchanged
に配置される点が異なります。 たとえば、 Modified
として既存のブログの追跡を開始するには、次のようにします。
context.Update(
new Blog { Id = 1, Name = ".NET Blog", });
この呼び出しの後に 変更トラッカーのデバッグ ビュー を調べると、コンテキストがこのエンティティを Modified
状態で追跡していることを示しています。
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: []
Add
やAttach
と同様に、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
状態で追跡する必要があります。 エンティティは通常、Deleted
でDbContext.Remove、DbContext.RemoveRange、または同等のメソッドのいずれかを呼び出すことによって、DbSet<TEntity>状態になります。 たとえば、既存の投稿を 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 からデタッチされます。 したがって、追跡対象のエンティティがないため、デバッグ ビューは空です。
依存エンティティまたは子エンティティの削除
依存エンティティまたは子エンティティをグラフから削除する方が、プリンシパル/親エンティティを削除するよりも簡単です。 詳細については、次のセクションと 外部キーとナビゲーションの変更 を参照してください。
Remove
で作成されたエンティティに対してnew
を呼び出すのは普通ではありません。 さらに、Add
、Attach
、Update
とは異なり、Remove
またはUnchanged
状態ですでに追跡されていないエンティティでModified
を呼び出すことは一般的ではありません。 代わりに、1 つのエンティティまたは関連エンティティのグラフを追跡し、削除する必要があるエンティティに対して Remove
を呼び出すのが一般的です。 追跡対象エンティティのこのグラフは、通常、次のいずれかによって作成されます。
- エンティティのクエリの実行
- 前のセクションで説明したように、切断されたエンティティのグラフで
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]);
Unchanged
が呼び出されたエンティティを除き、すべてのエンティティはRemove
としてマークされます。
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 つのエンティティ型を接続する各関係には、プリンシパルまたは親のエンドと、依存または子のエンドがあります。 依存/子エンティティは、外部キー プロパティを持つエンティティです。 一対多リレーションシップでは、プリンシパル/親は "一" 側にあり、依存/子は "多" 側にあります。 詳細については、「 リレーションシップ」 を参照してください。
前の例では、投稿を削除していました。これは、ブログ投稿の一対多リレーションシップの依存エンティティまたは子エンティティです。 依存エンティティまたは子エンティティの削除は他のエンティティに影響を与えないので、これは比較的簡単です。 一方、プリンシパル/親エンティティを削除すると、依存エンティティや子エンティティにも影響する必要があります。 これを行わないと、存在しなくなった主キー値を参照する外部キー値が残ります。 これは無効なモデル状態であり、ほとんどのデータベースで参照制約エラーが発生します。
この無効なモデルの状態は、次の 2 つの方法で処理できます。
- FK 値を null に設定します。 これは、依存元/子がプリンシパル/親に関連しなくなったことを示します。 これは、外部キーを null 許容にする必要があるオプションのリレーションシップの既定値です。 FK を null に設定することは、外部キーが通常 null 非許容である必要なリレーションシップでは有効ではありません。
- 依存関係/子要素を削除。 これは必須リレーションシップの既定値であり、オプションのリレーションシップにも有効です。
変更の追跡とリレーションシップの詳細については、「 外部キーとナビゲーション の変更」を参照してください。
省略可能なリレーションシップ
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 からデタッチされます。 したがって、デバッグ ビューからの出力は空です。
注
このドキュメントでは、EF Coreでのリレーションシップに関する基本的な部分しか触れていません。 SaveChanges を呼 び出すときの依存エンティティまたは子エンティティの更新/削除の詳細については、リレーションシップのモデリング と外部キーとナビゲーションの変更 に関する詳細を参照してください。
TrackGraph を使用したカスタム追跡
ChangeTracker.TrackGraph は、追跡する前にすべてのエンティティ インスタンスのコールバックを生成する点を除いて、 Add
、 Attach
、および Update
と同様に機能します。 これにより、グラフ内の個々のエンティティを追跡する方法を決定するときにカスタム ロジックを使用できます。
たとえば、生成されたキー値を持つエンティティを追跡するときに 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 async Task 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}");
});
await context.SaveChangesAsync();
}
グラフ内の各エンティティについて、上記のコードは 、エンティティを追跡する前に主キー値を確認します。 設定されていない (ゼロ) キー値の場合、コードは 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
注
わかりやすくするために、このコードでは、各エンティティに Id
と呼ばれる整数の主キー プロパティがあることを前提としています。 これは抽象基底クラスまたはインターフェイスに体系化できます。 または、このコードが任意の種類のエンティティで動作できるように、 IEntityType メタデータから主キーのプロパティを取得することもできます。
TrackGraph には 2 つのオーバーロードがあります。 上で使用した単純なオーバーロードでは、EF Core によってグラフの走査を停止するタイミングが決定されます。 具体的には、そのエンティティが既に追跡されている場合、またはコールバックがエンティティの追跡を開始しない場合は、特定のエンティティからの新しい関連エンティティへのアクセスを停止します。
高度なオーバーロード ( ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>)) には、ブール値を返すコールバックがあります。 コールバックが false を返した場合、グラフトラバーサルは停止し、それ以外の場合は続行されます。 このオーバーロードを使用する場合は、無限ループを避けるために注意する必要があります。
高度なオーバーロードでは、TrackGraph に状態を指定することもでき、この状態は各コールバックに渡されます。
.NET