0Pricing
C# Academy · レッスン

グローバルクエリフィルターと所有エンティティ

グローバルクエリフィルターでテナント分離と論理削除を適用し、所有エンティティ型で値オブジェクトをマッピングします。

「グローバルクエリフィルターと所有エンティティ」は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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Eager、Lazy、Explicit Loading
  2. Raw SQL、ストアドプロシージャと補間
  3. コンパイル済みクエリとパフォーマンス
  4. グローバルクエリフィルターと所有エンティティ
← C# Academyに戻る