概要
Entity Framework Core (EF Core) でデータを表示するだけの(更新を行わない)場合、AsNoTracking メソッドを使用することでパフォーマンスを劇的に向上させることができます。 これにより、DbContextが行う「変更追跡(Change Tracking)」のスナップショット作成処理がスキップされ、メモリ消費量の削減とクエリ実行速度の向上が見込めます。
仕様(入出力)
- 入力: 取得したい記事の最大件数。
- 出力: 取得した記事データのタイトルと著者名をコンソール表示。
- 前提: .NET 6.0以上、Microsoft.EntityFrameworkCore.InMemory(動作確認用)。
基本の使い方
クエリのメソッドチェーンの中に .AsNoTracking() を追加するだけです。通常は ToListAsync などを呼ぶ直前に記述します。
// 変更追跡を行わない(高速・省メモリ)
var products = await context.Products
.AsNoTracking() // ★ここに追加
.Where(p => p.Price > 1000)
.ToListAsync();
コード全文
ここでは「新着記事リストを表示する(編集機能はない)」というシナリオでの実装例です。 大量のデータを取得するバッチ処理や、Web APIのGETリクエスト処理などで特に有効です。
外部ライブラリとして Microsoft.EntityFrameworkCore.InMemory が必要です。
dotnet add package Microsoft.EntityFrameworkCore.InMemory
using System;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.EntityFrameworkCore;
public class Program
{
public static async Task Main()
{
var options = new DbContextOptionsBuilder<BlogContext>()
.UseInMemoryDatabase("BlogDb_NoTracking")
.Options;
// --- 1. 初期データの準備 ---
using (var context = new BlogContext(options))
{
if (!await context.Posts.AnyAsync())
{
var author = new User { Name = "TechUser" };
context.Users.Add(author);
// 大量のデータを想定
for (int i = 1; i <= 1000; i++)
{
context.Posts.Add(new Post
{
Title = $"C# Tips Vol.{i}",
Content = "Content...",
SentTime = DateTime.Now.AddHours(-i),
User = author
});
}
await context.SaveChangesAsync();
}
}
// --- 2. 読み取り専用クエリの実行 ---
using (var context = new BlogContext(options))
{
var worker = new BlogReader(context);
await worker.ShowRecentPostsAsync(10);
}
}
}
// 業務ロジッククラス
public class BlogReader
{
private readonly BlogContext _context;
public BlogReader(BlogContext context)
{
_context = context;
}
public async Task ShowRecentPostsAsync(int count)
{
Console.WriteLine($"--- 最新 {count} 件の記事を取得します ---");
// AsNoTrackingを使用
// これにより、EF Coreは取得したエンティティのコピーを追跡マネージャーに保存しません
var posts = await _context.Posts
.Include(p => p.User) // 関連データも含める
.OrderByDescending(p => p.SentTime)
.Take(count)
.AsNoTracking() // ★パフォーマンス向上の鍵
.ToArrayAsync();
foreach (var post in posts)
{
// Nullチェック (Includeしていてもデータ不整合でnullの可能性があるため安全策)
var authorName = post.User?.Name ?? "Unknown";
Console.WriteLine($"[{post.SentTime:MM/dd HH:mm}] {authorName}: {post.Title}");
}
// 注意: ここで post.Title = "変更"; としても、SaveChangesAsyncでDBは更新されません
}
}
// エンティティ定義
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime SentTime { get; set; }
public int UserId { get; set; }
public User? User { get; set; }
}
// DbContext定義
public class BlogContext : DbContext
{
public BlogContext(DbContextOptions<BlogContext> options)
: base(options) { }
public DbSet<Post> Posts => Set<Post>();
public DbSet<User> Users => Set<User>();
}
カスタムポイント
- デフォルト設定の変更: 個別のクエリではなく、そのDbContext全体で追跡を無効にしたい場合は、
DbContextの設定でQueryTrackingBehavior.NoTrackingを指定できます。C#optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - ID解決付きNoTracking: 結合によって同じエンティティ(例:同じ
User)が何度も結果に含まれる場合、通常のAsNoTrackingだと別々のインスタンスとして生成されメモリを浪費します。EF Core 5.0以降ではAsNoTrackingWithIdentityResolution()を使うことで、同一キーのインスタンスを共有化しメモリ効率を高められます。
注意点
- 更新不可:
AsNoTrackingで取得したエンティティのプロパティを変更してSaveChangesAsyncを呼んでも、データベースには反映されません。更新が必要な場合は使用しないでください。 - 遅延読み込み(Lazy Loading)無効: 追跡されていないエンティティは、ナビゲーションプロパティの遅延読み込み機能が働きません。必要なデータは必ず
Includeで明示的にロードする必要があります。 - Findメソッド:
DbSet.Find(id)は追跡されているデータをキャッシュから返す機能がありますが、AsNoTrackingは常にデータベースへのクエリを発行します。用途に応じて使い分けてください。
応用
Selectによる射影(さらなる高速化)
エンティティ全体を取得する必要がない場合、Select を使って必要な項目だけの匿名クラス(またはDTO)を取得するのが最速です。これは自動的に NoTracking として扱われます。
// エンティティ化せず、必要なカラムだけ取得(最軽量)
var dtos = await _context.Posts
.OrderByDescending(p => p.SentTime)
.Take(10)
.Select(p => new
{
p.Title,
AuthorName = p.User.Name // Include不要で結合データにアクセス可
})
.ToListAsync();
まとめ
さらに最適化したい場合は、Select を使って取得する列を絞り込んでください。
データの表示、レポート出力、Web APIの参照系エンドポイントなど、保存を伴わない処理では常に AsNoTracking を付ける癖をつけてください。
メモリ使用量とCPU負荷の両方を削減でき、高負荷時のスケーラビリティが向上します。
