EF Core によるマルチテナントアプリケーション
このページは Multi-Tenant Applications With EF Core(Milan Jovanović)の内容をもとにまとめたものです。
現代のソフトウェアアプリケーションの多くはマルチテナントという概念を中心に設計されています。
1 つのアプリケーションが複数のお客様(テナント)にサービスを提供しながら、それぞれのデータを分離します。
マルチテナントの戦略
マルチテナントには大きく 2 つのアプローチがあります。
| 戦略 | 概要 |
|---|---|
| シングルデータベース | 1 つの DB を共有し、TenantId カラムで論理的にデータを分離する |
| マルチデータベース | テナントごとに別々の DB を持ち、物理的にデータを分離する |
どちらを採用するかは要件次第です。医療などの業種では高度なデータ分離が求められるため、テナントごとに DB を用意することが必須になる場合があります。
EF Core Query Filters の基礎
EF Core の Global Query Filters を使うと、特定エンティティへのすべてのクエリに自動的にフィルタを適用できます。
基本的な使い方
modelBuilder
.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted);
HasQueryFilterでエンティティにフィルタを設定する- EF Core がそのエンティティのすべてのクエリに自動適用する
IgnoreQueryFilters()で特定クエリのみ無効化できる- エンティティごとに設定できるフィルタは 1 つのみ
上記の例では、Order テーブルへのすべてのクエリに IsDeleted = FALSE 条件が付与されます。
シングルデータベースによるマルチテナント
1 つの DB でマルチテナントを実現するために必要なものは 2 つです。
- 現在のテナントを識別する手段
- そのテナントのデータのみを取り出すフィルタ
典型的なアプローチは、各テーブルに TenantId カラムを追加し、クエリ時にそのカラムでフィルタリングすることです。
DbContext の実装
public class OrdersDbContext : DbContext
{
private readonly string _tenantId;
public OrdersDbContext(
DbContextOptions<OrdersDbContext> options,
TenantProvider tenantProvider)
: base(options)
{
_tenantId = tenantProvider.TenantId;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _tenantId);
}
}
TenantProvider クラスから現在のテナント値を取得し、Query Filter として設定しています。
TenantProvider の実装
public sealed class TenantProvider
{
private const string TenantIdHeaderName = "X-TenantId";
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string TenantId => _httpContextAccessor
.HttpContext
.Request
.Headers[TenantIdHeaderName];
}
この例では HTTP リクエストヘッダー(X-TenantId)から TenantId を取得しています。
TenantId の取得方法
TenantId を提供する方法はいくつかあります。
| 方法 | 例 | セキュリティ |
|---|---|---|
| HTTP ヘッダー | X-TenantId: tenant-1 | 低 |
| クエリ文字列 | api/orders?tenantId=tenant-1 | 低 |
| JWT クレーム | "tenant_id": "tenant-1" | 高 |
| API キー | Authorization: ApiKey ... | 高 |
より安全な実装を目指すなら JWT クレームまたは API キーを使用してください。
マルチデータベースによるマルチテナント
テナントごとに別々の DB に分離したい場合は、以下の変更が必要です。
- テナントごとに異なる接続文字列を適用する
- 現在のテナントに応じた接続文字列を解決する
この場合は Query Filter は使用しません(別々の DB にアクセスするため)。
テナント情報と接続文字列を何らかの形で管理する必要があります。
appsettings.json での設定例
{
"Tenants": [
{ "Id": "tenant-1", "ConnectionString": "Host=tenant1.db;Database=tenant1" },
{ "Id": "tenant-2", "ConnectionString": "Host=tenant2.db;Database=tenant2" }
]
}
IOptions で Tenant オブジェクトのリストとして登録できます。
TenantProvider(マルチDB版)
public sealed class TenantProvider
{
private const string TenantIdHeaderName = "X-TenantId";
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly TenantSettings _tenantSettings;
public TenantProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<TenantSettings> tenantsOptions)
{
_httpContextAccessor = httpContextAccessor;
_tenantSettings = tenantsOptions.Value;
}
public string TenantId => _httpContextAccessor
.HttpContext
.Request
.Headers[TenantIdHeaderName];
public string GetConnectionString()
{
return _tenantSettings.Tenants
.Single(t => t.Id == TenantId)
.ConnectionString;
}
}
DbContext の動的登録
builder.Services.AddDbContext<OrdersDbContext>((sp, options) =>
{
var tenantProvider = sp.GetRequiredService<TenantProvider>();
var connectionString = tenantProvider.GetConnectionString();
options.UseSqlServer(connectionString);
});
リクエストのたびに新しい OrdersDbContext が生成され、そのテナントに対応する DB に接続します。
テナントの接続文字列は Azure Key Vault などの安全な場所に保管することを強く推奨します。
2 つの戦略の比較
| 比較項目 | シングルDB | マルチDB |
|---|---|---|
| データ分離レベル | 論理的(TenantId カラム) | 物理的(別々の DB) |
| コスト | 低 | 高 |
| スケーラビリティ | テナント数が増えると DB 負荷が集中 | テナントごとに独立してスケール可能 |
| 運用複雑度 | 低 | 高(マイグレーション等が複雑) |
| 規制対応 | 要件によっては不十分 | 医療・金融等の高い分離要件に対応可能 |
| Query Filter | HasQueryFilter で自動適用 | 不要(DB 自体が分離) |
| 接続文字列管理 | 単一 | テナントごとに個別管理 |
バックグラウンドジョブとマルチテナント
バックグラウンドジョブでは HttpContext を使用できないため、TenantId の取得方法に注意が必要です。
- ジョブをスケジュールする際に ジョブデータに
TenantIdを含める - ジョブ内でユーザー ID が利用可能なら、そこから DB を参照して
TenantIdを取得する - メッセージキューを使う場合は メッセージのペイロードに
TenantIdを含める
まとめ
EF Core でマルチテナントを実装するポイントは以下のとおりです。
- シングル DB では Global Query Filters +
TenantIdカラムで実現できる - マルチ DB では動的な接続文字列の切り替えが必要になる
TenantProviderを使って現在のテナントを識別する- セキュリティ要件が高い場面では JWT クレームや API キーで
TenantIdを受け渡す - 接続文字列は Azure Key Vault 等の安全な場所に保管する
基本原則を理解すれば、より堅牢でセキュアなマルチテナントシステムへと発展させることができます。