entity-framework-core について
entity-framework-coreは、EF Coreのデータアクセスを構築・レビューする開発スキルです。DbContext、クエリ、マイグレーション、性能に関する判断を、データベースアプリケーション向けのパターンとアンチパターンで支援します。
主な機能
- 01DbContextのライフサイクルと設定の指針
- 02クエリの追跡・読み込み・射影パターン
- 03マイグレーションとスキーマ変更の実践
- 04性能・インデックス・同時実行の指針
- 05生成SQLとN+1クエリの確認
活用シーン
- 01EF Coreモデルとクエリの設計
- 02マイグレーションとDbContext利用のレビュー
- 03遅いクエリやN+1の調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill entity-framework-coreインストール後、Claude Code で/entity-framework-coreを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
manifest.jsonreferences/anti-patterns.mdpatterns.md詳細
Entity Framework Core
発動条件
DbContext、マイグレーション、モデル構成、または EF クエリを扱う場合- 追跡、読み込み、パフォーマンス、またはトランザクションの動作をレビューする場合
- EF6 またはカスタムリポジトリから EF Core にデータアクセスを移行する場合
- 遅いデータベースクエリを最適化する場合
ドキュメント
リファレンス
- patterns.md - クエリパターン、追跡戦略、読み込み戦略、プロジェクション、コンパイル済みクエリ、ページネーション、テンポラルテーブル
- anti-patterns.md - N+1 クエリ、大きなコンテキスト、汎用リポジトリ、インデックス不足など、EF Core でよくある間違い
ワークフロー
- 新規開発では EF Core を優先します。文書化された制約があり、Dapper または生 SQL が必要な場合を除きます
DbContextの有効期間はスコープにします — 作業単位に合わせます- クエリの変換を確認します — 生成された SQL を確認し、N+1 を回避します
- マイグレーションを重要な成果物として扱います — 使い捨てにせず、レビュー可能な状態にします
- プロバイダーの動作を慎重に考慮します — プロバイダー間で共通点はありますが、同一ではありません
- クエリを調査して検証します — 頭の中だけのデータモデルに頼りません
現在の上流プロジェクトの注記
- EF Core
v10.0.11はサービスリリースです。Azure SQL の互換性レベル 170 における JSON 変換を修正し、ネストされたOPENJSONプロジェクションでAS JSONが維持されるようになりました。また、.NET 11 で追加されたMemoryExtensions.Min/Maxオーバーロードとの互換性も保たれます。アップグレード後は、プロバイダーを使用した JSON クエリを再実行し、リポジトリで選択されているターゲットフレームワークに対してコンパイルしてください。 - 2026 年 8 月時点の EF Core と EF6 の比較は、移行を判断する際の最初の参照先です。EF Core は現在も開発が続くクロスプラットフォームのスタックですが、EF6 専用の EDMX/ObjectContext を多用するコードは、機能を洗い出し、データベースを使用した同等性テストを行わずに移行しないでください。
DbContext のパターン
基本的な構成
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
// Entity Configuration (Fluent API)
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Name).HasMaxLength(200).IsRequired();
builder.HasIndex(p => p.Sku).IsUnique();
builder.HasMany(p => p.OrderItems).WithOne(oi => oi.Product);
}
}
DI への登録
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.EnableSensitiveDataLogging() // Dev only
.EnableDetailedErrors()); // Dev only
// Or with pooling (better performance)
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(connectionString));
クエリパターン
読み取り専用には AsNoTracking を使用する
// Bad - tracks entities unnecessarily
var products = await db.Products.ToListAsync();
// Good - no tracking overhead
var products = await db.Products
.AsNoTracking()
.ToListAsync();
DTO にプロジェクションする
// Bad - loads entire entity graph
var orders = await db.Orders
.Include(o => o.Items)
.Include(o => o.Customer)
.ToListAsync();
// Good - loads only needed data
var orders = await db.Orders
.Select(o => new OrderDto
{
Id = o.Id,
CustomerName = o.Customer.Name,
ItemCount = o.Items.Count,
Total = o.Items.Sum(i => i.Price)
})
.ToListAsync();
N+1 クエリを回避する
// Bad - N+1 problem
foreach (var order in orders)
{
var items = await db.OrderItems
.Where(i => i.OrderId == order.Id)
.ToListAsync();
}
// Good - eager loading
var orders = await db.Orders
.Include(o => o.Items)
.ToListAsync();
// Good - split query for large graphs
var orders = await db.Orders
.Include(o => o.Items)
.AsSplitQuery()
.ToListAsync();
コンパイル済みクエリ(EF Core 9)
// Pre-compiled for frequently used queries
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
// Usage
var product = await GetProductById(db, productId);
マイグレーションのパターン
マイグレーションの作成
# Add migration
dotnet ef migrations add AddProductIndex
# Apply to database
dotnet ef database update
# Generate SQL script
dotnet ef migrations script --idempotent -o migrate.sql
データマイグレーション
public partial class AddProductIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_Products_Sku",
table: "Products",
column: "Sku",
unique: true);
// Data migration (if needed)
migrationBuilder.Sql(@"
UPDATE Products
SET NormalizedName = UPPER(Name)
WHERE NormalizedName IS NULL");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Products_Sku",
table: "Products");
}
}
回避すべきアンチパターン
| アンチパターン | 問題点 | より良い方法 |
|---|---|---|
ToList() の後にフィルターする | すべてのデータをメモリに読み込む | クエリ内でフィルターする |
| リクエストごとに複数の DbContext を使う | トランザクションの問題が発生する | スコープの有効期間にする |
| あらゆる場所で遅延読み込みを使う | N+1 クエリが発生する | 明示的に Include する |
| 汎用リポジトリのラッパー | クエリの柔軟性が失われる | DbContext を直接使う |
| 生成された SQL を無視する | パフォーマンスの問題が見えなくなる | ログに記録してレビューする |
ループ内で SaveChanges() を呼ぶ | 往復回数が増える | バッチ処理してから保存する |
パフォーマンスのベストプラクティス
-
頻繁にクエリされる列にインデックスを付けます:
builder.HasIndex(p => p.CreatedAt); builder.HasIndex(p => new { p.Category, p.Status }); -
ページネーションを使います:
var page = await db.Products .OrderBy(p => p.Id) .Skip(pageSize * pageNumber) .Take(pageSize) .ToListAsync(); -
更新をバッチ処理します(EF Core 7 以降):
await db.Products .Where(p => p.Category == "Obsolete") .ExecuteDeleteAsync(); await db.Products .Where(p => p.Category == "Sale") .ExecuteUpdateAsync(p => p.SetProperty(x => x.Price, x => x.Price * 0.9m)); -
ネットワークの往復を最小限に抑えます:
// Bad - 3 roundtrips var product = await db.Products.FindAsync(id); var reviews = await db.Reviews.Where(r => r.ProductId == id).ToListAsync(); var related = await db.Products.Where(p => p.Category == product.Category).ToListAsync(); // Good - 1 roundtrip var data = await db.Products .Where(p => p.Id == id) .Select(p => new { Product = p, Reviews = p.Reviews, Related = db.Products.Where(r => r.Category == p.Category).Take(5) }) .FirstOrDefaultAsync();
同時実行のパターン
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
[ConcurrencyCheck]
public int Version { get; set; }
// Or use RowVersion
[Timestamp]
public byte[] RowVersion { get; set; }
}
// Handle concurrency conflicts
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// Resolve conflict...
}
提供するもの
- ドメインに合致する EF Core のモデルとクエリ
- より安全なマイグレーションと有効期間の管理
- パフォーマンスを考慮したデータアクセスの判断
- 適切なインデックス設定とクエリの最適化
検証項目
- クエリの動作が意図どおりである(SQL ログを確認する)
- マイグレーションがレビュー可能で、正しい
- 一般的な処理経路に N+1 クエリがない
- フィルターや並べ替えに使う列にインデックスがある
- DbContext の有効期間が適切にスコープされている
- 重要なエンティティの同時実行が適切に処理されている
コメント
Markdown 対応