次の方法で共有


その他の変更追跡機能

このドキュメントでは、変更の追跡に関連するその他の機能とシナリオについて説明します。

ヒント

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

ヒント

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

AddAddAsync

Entity Framework Core (EF Core) では、そのメソッドを使用するとデータベースの相互作用が発生する可能性がある場合は常に非同期メソッドが提供されます。 また、高パフォーマンスの非同期アクセスをサポートしていないデータベースを使用する場合のオーバーヘッドを回避するために、同期メソッドも用意されています。

DbContext.Add これらのメソッドは本質的にエンティティの追跡を開始するだけなので、通常、 DbSet<TEntity>.Add はデータベースにアクセスしません。 ただし、キー値を生成するために、一部の形式の値生成がデータベースにアクセス する場合があります 。 これを行い、EF Core に付属する唯一の値ジェネレーターは HiLoValueGenerator<TValue>です。 このジェネレーターの使用は一般的ではありません。既定では構成されません。 つまり、アプリケーションの大部分は、Addではなく、AddAsyncを使用する必要があります。

UpdateAttachRemoveなどの他の同様のメソッドでは、新しいキー値が生成されないため、非同期オーバーロードがないため、データベースにアクセスする必要はありません。

AddRangeUpdateRangeAttachRange、および RemoveRange

DbSet<TEntity> DbContextは、1 回の呼び出しで複数のインスタンスを受け入れるAddUpdateAttach、およびRemoveの代替バージョンを提供します。 これらのメソッドはそれぞれ、 AddRangeUpdateRangeAttachRange、および RemoveRange です。

これらのメソッドは便利な方法として提供されています。 "range" メソッドを使用すると、同等の非範囲メソッドに対する複数の呼び出しと同じ機能が使用されます。 2 つのアプローチの間に大きなパフォーマンスの違いはありません。

これは EF6 とは異なり、 AddRangeAdd はどちらも自動的に DetectChanges呼び出されますが、 Add を複数回呼び出すと、DetectChanges は 1 回ではなく複数回呼び出されます。 これにより、EF6でのAddRangeの効率が向上しました。 EF Core では、どちらのメソッドも自動的に DetectChanges を呼び出しません。

DbContext メソッドと DbSet メソッド

AddUpdateAttachRemoveなど、多くのメソッドには、DbSet<TEntity>DbContextの両方に実装があります。 これらのメソッドは、通常のエンティティ型に対して まったく同じ動作 をします。 これは、エンティティの CLR 型が EF Core モデルの 1 つのエンティティ型にのみマップされるためです。 したがって、CLR 型は、エンティティがモデルに適合する場所を完全に定義するため、使用する DbSet を暗黙的に決定できます。

この規則の例外は、主に多対多結合エンティティに使用される共有型エンティティ型を使用する場合です。 共有型エンティティ型を使用する場合は、使用されている EF Core モデル型に対して DbSet を最初に作成する必要があります。 その後、 AddUpdateAttachRemove などのメソッドは、どの EF Core モデルの種類が使用されているかをあいまいにすることなく、DbSet で使用できます。

共有型エンティティ型は、多対多リレーションシップの結合エンティティに既定で使用されます。 共有型エンティティ型は、多対多リレーションシップで使用するように明示的に構成することもできます。 たとえば、次のコードは、 Dictionary<string, int> を結合エンティティ型として構成します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .SharedTypeEntity<Dictionary<string, int>>(
            "PostTag",
            b =>
            {
                b.IndexerProperty<int>("TagId");
                b.IndexerProperty<int>("PostId");
            });

    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<Dictionary<string, int>>(
            "PostTag",
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany());
}

外部キーとナビゲーションを変更すると、 新しい結合エンティティ インスタンスを追跡して 2 つのエンティティを関連付ける方法が示されます。 次のコードは、結合エンティティに使用 Dictionary<string, int> 共有型エンティティ型に対してこれを行います。

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

DbContext.Set<TEntity>(String)を使用して、PostTag エンティティ型の DbSet を作成していることに注意してください。 この DbSet を使用して、新しい結合エンティティ インスタンスで Add を呼び出すことができます。

Von Bedeutung

規則によってエンティティ型を結合するために使用される CLR 型は、パフォーマンスを向上させるために将来のリリースで変更される可能性があります。 上記のコードにある Dictionary<string, int> のように明示的に構成されている場合を除き、特定の結合エンティティ型に依存しないでください。

プロパティ対フィールドアクセス

エンティティ プロパティへのアクセスでは、プロパティのバッキング フィールドが既定で使用されます。 これは効率的であり、プロパティゲッターとセッターを呼び出すことによる副作用のトリガーを回避します。 たとえば、遅延読み込みが無限ループのトリガーを回避できる方法です。 モデルでの バッキング フィールド の構成の詳細については、バッキング フィールドを参照してください。

EF Core がプロパティ値を変更するときに副作用を生成することが望ましい場合があります。 たとえば、エンティティへのデータ バインディングの場合、プロパティを設定すると、フィールドを直接設定するときに発生しない U.I. への通知が生成される場合があります。 これを実現するには、次の PropertyAccessMode を変更します。

プロパティ アクセス モード FieldPreferField により、EF Core はバッキング フィールドを介してプロパティ値にアクセスします。 同様に、 PropertyPreferProperty により、EF Core は getter と setter を介してプロパティ値にアクセスします。

FieldまたはPropertyが使用され、EF Core がフィールドまたはプロパティの getter/setter を介して値にそれぞれアクセスできない場合、EF Core は例外をスローします。 これにより、EF Core がフィールド/プロパティ アクセスを確実に使用していることを期待通りに保証します。

一方、 PreferField モードと PreferProperty モードは、優先アクセスを使用できない場合は、それぞれプロパティまたはバッキング フィールドの使用にフォールバックします。 PreferField はデフォルト値です。 つまり、EF Core は可能な場合は常にフィールドを使用しますが、代わりに getter または setter を介してプロパティにアクセスする必要がある場合は失敗しません。

FieldDuringConstructionPreferFieldDuringConstructionエンティティ インスタンスの作成時にのみバッキング フィールドを使用するように EF Core を構成します。 これにより、getter と setter の副作用なしでクエリを実行できますが、EF Core による後のプロパティの変更によってこれらの副作用が発生します。

さまざまなプロパティ アクセス モードを次の表にまとめます。

プロパティアクセスモード (PropertyAccessMode) 好み 優先設定を構築するエンティティ フォールバック フォールバックによるエンティティの作成
Field フィールド フィールド 投げ 投げ
Property プロパティ プロパティ 投げ 投げ
PreferField フィールド フィールド プロパティ プロパティ
PreferProperty プロパティ プロパティ フィールド フィールド
FieldDuringConstruction プロパティ フィールド フィールド 投げ
PreferFieldDuringConstruction プロパティ フィールド フィールド プロパティ

一時値

EF Core では、SaveChanges が呼び出されたときにデータベースによって生成される実際のキー値を持つ新しいエンティティを追跡するときに、一時キー値が作成されます。 これらの一時的な値の使用方法の概要については、 EF Core での変更の追跡 に関するページを参照してください。

一時値へのアクセス

一時値は変更トラッカーに格納され、エンティティ インスタンスには直接設定されません。 ただし、追跡対象エンティティにアクセスするためのさまざまなメカニズムを使用すると、これらの一時的な値が公開されます。 たとえば、次のコードは、 EntityEntry.CurrentValuesを使用して一時値にアクセスします。

using var context = new BlogsContext();

var blog = new Blog { Name = ".NET Blog" };

context.Add(blog);

Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");

このコードからの出力は次のとおりです。

Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643

PropertyEntry.IsTemporary を使用して、一時的な値を確認できます。

一時値の操作

一時値を明示的に操作すると便利な場合があります。 たとえば、新しいエンティティのコレクションを Web クライアント上に作成し、サーバーにシリアル化し直す場合があります。 外部キー値は、これらのエンティティ間のリレーションシップを設定する 1 つの方法です。 次のコードでは、このアプローチを使用して、外部キーによって新しいエンティティのグラフを関連付けますが、SaveChanges が呼び出されたときに実際のキー値を生成できます。

var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };

var posts = new List<Post>
{
    new Post
    {
        Id = -1,
        BlogId = -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,
        BlogId = -2,
        Title = "Disassembly improvements for optimized managed debugging",
        Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
    }
};

using var context = new BlogsContext();

foreach (var blog in blogs)
{
    context.Add(blog).Property(e => e.Id).IsTemporary = true;
}

foreach (var post in posts)
{
    context.Add(post).Property(e => e.Id).IsTemporary = true;
}

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

次のことに注意してください。

  • 負の数は一時キー値として使用されます。これは必須ではありませんが、キーの競合を防ぐための一般的な規則です。
  • Post.BlogId FK プロパティには、関連付けられたブログの PK と同じ負の値が割り当てられます。
  • PK 値は、各エンティティが追跡された後 IsTemporary 設定することで一時的な値としてマークされます。 これは、アプリケーションによって提供されるすべてのキー値が実際のキー値であると見なされるために必要です。

SaveChanges を呼び出す前に 変更トラッカーのデバッグ ビュー を見ると、PK 値は一時的なものとしてマークされ、投稿はナビゲーションの修正など、正しいブログに関連付けられていることを示しています。

Blog {Id: -2} Added
  Id: -2 PK Temporary
  Name: 'Visual Studio Blog'
  Posts: [{Id: -2}]
Blog {Id: -1} Added
  Id: -1 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -1}]
Post {Id: -2} Added
  Id: -2 PK Temporary
  BlogId: -2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: -2}
  Tags: []
Post {Id: -1} Added
  Id: -1 PK Temporary
  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}

SaveChangesを呼び出した後、これらの一時的な値は、データベースによって生成された実際の値に置き換えられました。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Posts: [{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}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []

既定値の取り扱い

EF Core を使用すると、プロパティは、 SaveChanges が呼び出されたときにデータベースから既定値を取得できます。 生成されたキー値と同様に、EF Core では、値が明示的に設定されていない場合にのみ、データベースの既定値が使用されます。 たとえば、次のエンティティ型を考えてみましょう。

public class Token
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime ValidFrom { get; set; }
}

ValidFrom プロパティは、データベースから既定値を取得するように構成されています。

modelBuilder
    .Entity<Token>()
    .Property(e => e.ValidFrom)
    .HasDefaultValueSql("CURRENT_TIMESTAMP");

この型のエンティティを挿入すると、EF Core では、明示的な値が設定されていない限り、データベースで値が生成されます。 例えば次が挙げられます。

using var context = new BlogsContext();

context.AddRange(
    new Token { Name = "A" },
    new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

変更トラッカーのデバッグ ビューを見ると、最初のトークンがデータベースによって生成ValidFrom一方で、2 つ目のトークンでは値が明示的に設定されたことを示しています。

Token {Id: 1} Unchanged
  Id: 1 PK
  Name: 'A'
  ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
  Id: 2 PK
  Name: 'B'
  ValidFrom: '11/11/1111 11:11:11 AM'

データベースの既定値を使用するには、データベース列に既定値制約が構成されている必要があります。 これは、 HasDefaultValueSql または HasDefaultValueを使用する場合、EF Core の移行によって自動的に行われます。 EF Core 移行を使用しない場合は、他の方法で列に既定の制約を作成してください。

Null 値を許容するプロパティの使用

EF Core は、プロパティ値をその型の CLR 既定値と比較することで、プロパティが設定されているかどうかを判断できます。 これはほとんどの場合うまく機能しますが、CLR の既定値をデータベースに明示的に挿入できないことを意味します。 たとえば、整数プロパティを持つエンティティを考えてみましょう。

public class Foo1
{
    public int Id { get; set; }
    public int Count { get; set; }
}

データベースの既定値が -1 に設定されるようにそのプロパティが構成されている場合:

modelBuilder
    .Entity<Foo1>()
    .Property(e => e.Count)
    .HasDefaultValue(-1);

この目的は、明示的な値が設定されていない場合は常に、-1 の既定値が使用されることです。 ただし、値を 0 に設定することは (整数の場合の CLR の既定値)、EF Core にとって値が設定されていないことと区別がつかないため、このプロパティには 0 を挿入することができません。 例えば次が挙げられます。

using var context = new BlogsContext();

var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

Countが明示的に 0 に設定されたインスタンスは、引き続きデータベースから既定値を取得していることに注意してください。これは、意図したものではありません。 これに対処する簡単な方法は、 Count プロパティを null 許容にすることです。

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

これにより、CLR の既定値は 0 ではなく null になります。つまり、明示的に設定すると 0 が挿入されます。

using var context = new BlogsContext();

var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

null 許容バッキング フィールドの使用

ドメインモデルにおいて、概念上nullを許容するものではないプロパティをnull許容にすることに関する問題。 したがって、プロパティを null 許容に強制すると、モデルが侵害されます。

このプロパティは null 非許容のままにして、バッキング フィールドのみを null 許容にすることができます。 例えば次が挙げられます。

public class Foo3
{
    public int Id { get; set; }

    private int? _count;

    public int Count
    {
        get => _count ?? -1;
        set => _count = value;
    }
}

これにより、プロパティが明示的に 0 に設定されている場合、CLR の既定値 (0) を挿入できますが、ドメイン モデルでプロパティを null 許容として公開する必要はありません。 例えば次が挙げられます。

using var context = new BlogsContext();

var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Boolプロパティに対するNullableバッキングフィールド

このパターンは、ストアで生成された既定値で bool プロパティを使用する場合に特に便利です。 boolの CLR の既定値は "false" であるため、通常のパターンを使用して明示的に "false" を挿入できないことを意味します。 たとえば、User エンティティ型について考えてみます。

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

    private bool? _isAuthorized;

    public bool IsAuthorized
    {
        get => _isAuthorized ?? true;
        set => _isAuthorized = value;
    }
}

IsAuthorized プロパティは、データベースの既定値 "true" で構成されます。

modelBuilder
    .Entity<User>()
    .Property(e => e.IsAuthorized)
    .HasDefaultValue(true);

IsAuthorizedプロパティは、挿入する前に明示的に "true" または "false" に設定することも、設定を解除したままにすることもできます。その場合、データベースの既定値が使用されます。

using var context = new BlogsContext();

var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!

context.AddRange(userA, userB, userC);

await context.SaveChangesAsync();

SQLite を使用する場合の SaveChanges からの出力は、データベースの既定値が Mac で使用されているのに対し、Alice と Baxter には明示的な値が設定されていることを示しています。

-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

スキーマの既定値のみ

EF Core の移行によって作成されたデータベース スキーマで、EF Core でこれらの値を挿入に使用せずに既定値を使用すると便利な場合があります。 これは、プロパティを PropertyBuilder.ValueGeneratedNever として構成することで実現できます。次に例を示します。

modelBuilder
    .Entity<Bar>()
    .Property(e => e.Count)
    .HasDefaultValue(-1)
    .ValueGeneratedNever();