追跡と追跡なしのクエリ

追跡動作は、Entity Framework Core が、その変更トラッカー内のエンティティ インスタンスに関する情報を保持するかどうかを制御します。 エンティティが追跡されている場合、エンティティ内で検出された変更はすべて、SaveChanges の間にデータベースに対して永続化されます。 EF Core は、追跡クエリの結果内のエンティティ、および変更トラッカーに含まれるエンティティの間のナビゲーション プロパティの修正も行います。

Note

キーなしエンティティ型は追跡されません。 この記事でエンティティ型に関する言及がある場合は、必ずキーが定義されているエンティティ型を参照しています。

ヒント

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

追跡クエリ

既定では、エンティティ型を返すクエリは、追跡を行います。 追跡クエリは、エンティティ インスタンスに対する変更が SaveChanges によって永続化されることを意味します。 次の例では、ブログ評価に対する変更が検出され、SaveChanges の間にデータベースに対して永続化されます。

var blog = context.Blogs.SingleOrDefault(b => b.BlogId == 1);
blog.Rating = 5;
context.SaveChanges();

追跡クエリに結果が返されると、EF Core は、エンティティが既にコンテキスト内に存在するかどうかを確認します。 EF Core が既存のエンティティを見つけると、同じインスタンスが返されます。これにより、使用するメモリが少なくなり、追跡なしのクエリよりも高速になる可能性があります。 EF Core は、エントリ内にあるエンティティのプロパティの現在と元の値をデータベースの値で上書きしません。 エンティティがコンテキスト内に見つからない場合、EF Core は新しいエンティティ インスタンスを作成し、コンテキストにアタッチします。 クエリ結果にはエンティティが含まれず、コンテキストには追加されますが、まだデータベースには保存されていません。

追跡なしのクエリ

追跡なしのクエリは、読み取り専用のシナリオで結果が使用される場合に役立ちます。 変更追跡情報を設定する必要がないため、通常は実行が速くなります。 データベースから取得されたエンティティを更新する必要がない場合は、追跡なしのクエリを使用することをお勧めします。 個々のクエリを追跡なしに設定できます。 追跡なしのクエリは、ローカルの変更や追加されたエンティティとは関係なく、データベースの内容に基づいた結果を返すことも行います。

var blogs = context.Blogs
    .AsNoTracking()
    .ToList();

コンテキスト インスタンスのレベルで、既定の追跡動作を変更できます。

context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

var blogs = context.Blogs.ToList();

次のセクションでは、追跡なしのクエリの効率が追跡クエリよりも低いときについて説明します。

識別子の解決

追跡クエリは変更トラッカーを使用するため、EF Core は追跡クエリで ID 解決を実行します。 エンティティを具体化するとき、それが既に追跡されている場合は、EF Core は変更トラッカーから同じエンティティ インスタンスを返します。 結果に同じエンティティが複数回含まれている場合は、そのたびに同じインスタンスが返されます。 追跡なしのクエリでは、次のようになります。

  • 変更トラッカーを使用せず、ID 解決を実行しません。
  • 同じエンティティが結果に複数回含まれているときにも、エンティティの新しいインスタンスが返されます。

追跡と追跡なしは、同じクエリで組み合わせることができます。 つまり、追跡なしのクエリを使用できます。これにより、結果で ID 解決が行われます。 クエリ可能な AsNoTracking 演算子と同じように、別の AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>) 演算子が追加されました。 QueryTrackingBehavior 列挙型にも、関連するエントリが追加されています。 追跡なしで ID 解決を使用するようにクエリを構成すると、クエリ結果の生成時に、スタンドアロンの変更トラッカーがバックグラウンドで使用されるため、各インスタンスが 1 回だけ具体化されます。 この変更トラッカーはコンテキスト内のものとは異なるため、結果はコンテキストによって追跡されません。 クエリが完全に列挙されると、変更トラッカーはスコープ外になり、必要に応じてガベージ コレクションが実行されます。

var blogs = context.Blogs
    .AsNoTrackingWithIdentityResolution()
    .ToList();

既定の追跡動作の構成

多くのクエリの追跡動作を変更した場合は、代わりに既定値を変更することをお勧めします。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.Tracking;Trusted_Connection=True")
        .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

これにより、既定ではすべてのクエリが追跡されなくなります。 AsTracking を追加して、特定のクエリを追跡することもできます。

追跡とカスタム プロジェクション

クエリの結果の型がエンティティ型ではない場合でも、EF Core では既定で、結果に含まれているエンティティ型が引き続き追跡されます。 次のクエリでは、匿名の型が返され、結果セット内で Blog のインスタンスが追跡されます。

var blog = context.Blogs
    .Select(
        b =>
            new { Blog = b, PostCount = b.Posts.Count() });

結果セットに LINQ の合成に由来するエンティティ型が含まれていた場合は、EF Core によってそれらが追跡されます。

var blog = context.Blogs
    .Select(
        b =>
            new { Blog = b, Post = b.Posts.OrderBy(p => p.Rating).LastOrDefault() });

結果セットにいかなるエンティティ型も含まれていなかった場合は、追跡なしが実行されます。 次のクエリでは、エンティティの値の一部を含む (ただし実際のエンティティ型のインスタンスは含まない) 匿名型が返されます。 クエリから生じた追跡対象のエンティティはありません。

var blog = context.Blogs
    .Select(
        b =>
            new { Id = b.BlogId, b.Url });

EF Core では、最上位レベルのプロジェクションでクライアント評価を行うことができます。 クライアント評価のために、EF Core によりエンティティ インスタンスが具体化された場合、それは追跡されます。 ここでは、blog エンティティがクライアント メソッド StandardizeURL に渡されているため、EF Core によって blog インスタンスも追跡されます。

var blogs = context.Blogs
    .OrderByDescending(blog => blog.Rating)
    .Select(
        blog => new { Id = blog.BlogId, Url = StandardizeUrl(blog) })
    .ToList();
public static string StandardizeUrl(Blog blog)
{
    var url = blog.Url.ToLower();

    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }

    return url;
}

EF Core では、結果に含まれているキーなしのエンティティ インスタンスは追跡されません。 ただし、キーを持つその他のすべてのエンティティ型のインスタンスは、上記のルールに従って、EF Core によって追跡されます。

以前のバージョン

バージョン 3.0 より前の EF Core では、追跡の実行方法にいくつかの違いがありました。 注意すべき違いは次のとおりです。

  • クライアントとサーバーの評価」ページで説明されているように、バージョン 3.0 より前の EF Core では、クエリのあらゆる部分でのクライアント評価がサポートされていました。 クライアント評価によってエンティティの具体化が発生し、それは結果に含まれていませんでした。 そのため、EF Core では、追跡対象を検出するために、その結果が分析されていました。この設計には、次のようないくつかの相違点があります。

    • プロジェクションでのクライアント評価は、具体化を発生させましたが、具体化されたエンティティ インスタンスは返されず、追跡されませんでした。 次の例では、blog エンティティが追跡されませんでした。

      var blogs = context.Blogs
          .OrderByDescending(blog => blog.Rating)
          .Select(
              blog => new { Id = blog.BlogId, Url = StandardizeUrl(blog) })
          .ToList();
      
    • 場合によっては、EF Core では LINQ の合成に由来するオブジェクトが追跡されませんでした。 次の例では、Post が追跡されませんでした。

      var blog = context.Blogs
          .Select(
              b =>
                  new { Blog = b, Post = b.Posts.OrderBy(p => p.Rating).LastOrDefault() });
      
  • クエリ結果にキーなしエンティティ型が含まれていた場合は、常にクエリ全体が追跡なしになりました。 つまり、結果に含まれている、キーを持つエンティティ型も追跡されませんでした。

  • 以前は、EF Core により、追跡なしのクエリで ID 解決が実行されていました。 既に返されているエンティティを追跡するために、弱参照が使用されました。 そのため、結果セットに同じエンティティが複数回含まれていた場合は、そのたびに同じインスタンスが返されました。 同じ ID を持つ前の結果がスコープから外れ、ガベージ コレクションが行われても、EF Core により新しいインスタンスが返されました。