グローバルクエリフィルターと所有エンティティ
グローバルクエリフィルターでテナント分離と論理削除を適用し、所有エンティティ型で値オブジェクトをマッピングします。
「グローバルクエリフィルターと所有エンティティ」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。
グローバルクエリフィルターとは
グローバルクエリフィルターは、特定のエンティティ型に対するすべてのLINQクエリに自動適用されるWHERE述語です。ソフトデリート、マルチテナント、行レベルセキュリティに最適で、条件をあちこちに繰り返し記述する必要がありません。
グローバルフィルターによるソフトデリート
IsDeletedフラグを追加し、グローバルフィルターを構成すると、フィルターを明示的に無視しない限り、削除済みのレコードが返されなくなります。
// Entity with soft-delete flag
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public bool IsDeleted { get; set; }
}
// Configure in OnModelCreating
modelBuilder.Entity<Post>()
.HasQueryFilter(p => !p.IsDeleted);
// All queries now implicitly add WHERE IsDeleted = 0:
var posts = await _db.Posts.ToListAsync();グローバルフィルターによるマルチテナント
スコープ付きサービスから現在のテナントを読み取るフィルターを使用します。すべてのクエリが自動的に現在のテナントのデータだけを対象にします。
public class AppDbContext : DbContext
{
private readonly ICurrentUserService _user;
public AppDbContext(DbContextOptions<AppDbContext> opt, ICurrentUserService user)
: base(opt) => _user = user;
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Order>().HasQueryFilter(
o => o.TenantId == _user.TenantId);
}
}
// Every Order query automatically filters by TenantIdグローバルフィルターの無視
管理者がすべてのテナントを検索する場合や、ソフトデリートしたレコードを復元する場合など、フィルターを回避する必要が生じることがあります。そのクエリに限ってIgnoreQueryFilters()を使用してください。
// Admin: see ALL posts including deleted
var allPosts = await _db.Posts
.IgnoreQueryFilters()
.ToListAsync();
// Restore a specific deleted post
var deleted = await _db.Posts
.IgnoreQueryFilters()
.FirstOrDefaultAsync(p => p.Id == 42);
if (deleted is not null)
{
deleted.IsDeleted = false;
await _db.SaveChangesAsync();
}ナビゲーションプロパティのフィルター
グローバルフィルターはJOINにも適用されます。Postsにフィルターがある場合、Includeで読み込まれる関連するPostsにも自動的にフィルターが適用されます。
// Blog with soft-delete filter on Posts
var blog = await _db.Blogs
.Include(b => b.Posts) // only non-deleted posts are included
.FirstOrDefaultAsync(b => b.Id == 1);
// blog.Posts contains only active posts — the filter applied in the JOIN所有エンティティ型:概念
所有エンティティ型は、単一の所有元エンティティにのみ属するクラスです。主キーを持たず、デフォルトでは同じテーブルに保存されます。DDDの値オブジェクトに最適です。
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Address BillingAddress { get; set; } = new();
public Address ShippingAddress { get; set; } = new();
}
public class Address
{
public string Street { get; set; } = "";
public string City { get; set; } = "";
public string ZipCode { get; set; } = "";
public string Country { get; set; } = "";
}所有エンティティの構成
OwnsOneを使って、単一の所有エンティティを構成します。EF Coreはデフォルトで、ナビゲーションプロパティ名を列名の接頭辞として付けます。
modelBuilder.Entity<Customer>(entity =>
{
entity.OwnsOne(c => c.BillingAddress, addr =>
{
addr.Property(a => a.Street).HasMaxLength(300);
addr.Property(a => a.Country).HasMaxLength(2);
});
entity.OwnsOne(c => c.ShippingAddress);
});
// Table: Customers with columns:
// BillingAddress_Street, BillingAddress_City, ...
// ShippingAddress_Street, ShippingAddress_City, ...別テーブルの所有エンティティ
OwnsOne内でToTable()を使うと、所有エンティティを独自のテーブルに保存できます。データ量が多い場合やオプションの場合に便利です。
modelBuilder.Entity<Customer>().OwnsOne(
c => c.BillingAddress,
addr => addr.ToTable("CustomerBillingAddresses"));
// Now BillingAddress columns are in a separate table
// with a FK back to CustomerId値オブジェクトのコレクションに対するOwnsMany
OwnsManyは所有エンティティのコレクションを構成します。たとえば、子テーブルに保存するメールアドレスや電話番号のリストに使用できます。
public class Customer
{
public int Id { get; set; }
public IList<PhoneNumber> PhoneNumbers { get; set; } = new List<PhoneNumber>();
}
public class PhoneNumber
{
public string Number { get; set; } = "";
public string Type { get; set; } = "";
}
modelBuilder.Entity<Customer>()
.OwnsMany(c => c.PhoneNumbers,
phone => phone.ToTable("CustomerPhones"));Table-Per-HierarchyとOwnedの比較
グローバルフィルターは、TPH(Table-Per-Hierarchy)継承でも適切に機能します。TPHでは識別子列によってサブタイプを区別します。識別子に基づいてフィルターを適用すると、型固有のクエリを実行できます。
// Base entity
public abstract class Payment { public int Id { get; set; } }
public class CardPayment : Payment { public string CardLast4 { get; set; } = ""; }
public class BankPayment : Payment { public string IBAN { get; set; } = ""; }
// All Payment subtypes in one table with a Discriminator column
// No extra config needed — EF Core handles TPH by default
var cards = await _db.Set<CardPayment>().ToListAsync();
// SQL: SELECT * FROM Payments WHERE Discriminator = 'CardPayment'実践例:SaaSのテナント分離
SaaSのマルチテナント構成では、グローバルクエリフィルターとスコープ付きテナントコンテキストを組み合わせます。クエリごとのコードを記述しなくても、すべてのクエリが自動的に対象テナントだけに限定されます。
// Scoped tenant service
public class TenantContext
{
public Guid TenantId { get; set; }
}
// DbContext uses it
modelBuilder.Entity<Invoice>().HasQueryFilter(
inv => inv.TenantId == _tenantCtx.TenantId);
modelBuilder.Entity<Customer>().HasQueryFilter(
c => c.TenantId == _tenantCtx.TenantId);
// Queries are automatically scoped:
var invoices = await _db.Invoices.ToListAsync();
// WHERE TenantId = 'current-tenant-guid'確認問題
特定のクエリでグローバルクエリフィルターを一時的に回避するには、どのメソッドを呼び出しますか?
まとめ:グローバルフィルターと所有エンティティ
要点:
- グローバルクエリフィルターは、エンティティに対するすべてのクエリにWHERE述語を自動的に追加する
- ソフトデリート、マルチテナント、行レベルセキュリティに最適
- 管理者操作や復元操作では
IgnoreQueryFilters()で回避する - 所有エンティティは値オブジェクトをモデル化する — 主キーを持たず、所有元のテーブルに保存される
OwnsOneとOwnsManyで所有リレーションシップを構成し、ToTable()でテーブルを分割する
よくある質問
「グローバルクエリフィルターと所有エンティティ」レッスンは無料ですか?
はい。「グローバルクエリフィルターと所有エンティティ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。
「グローバルクエリフィルターと所有エンティティ」で何を学びますか?
グローバルクエリフィルターでテナント分離と論理削除を適用し、所有エンティティ型で値オブジェクトをマッピングします。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
C# Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「グローバルクエリフィルターと所有エンティティ」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このC# Academyレッスンでコードを書いて実行できますか?
はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Eager、Lazy、Explicit Loading
- Raw SQL、ストアドプロシージャと補間
- コンパイル済みクエリとパフォーマンス
- グローバルクエリフィルターと所有エンティティ