跳到主要内容

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 つです。

  1. 現在のテナントを識別する手段
  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" }
]
}

IOptionsTenant オブジェクトのリストとして登録できます。

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 FilterHasQueryFilter で自動適用不要(DB 自体が分離)
接続文字列管理単一テナントごとに個別管理

バックグラウンドジョブとマルチテナント

バックグラウンドジョブでは HttpContext を使用できないため、TenantId の取得方法に注意が必要です。

  • ジョブをスケジュールする際に ジョブデータに TenantId を含める
  • ジョブ内でユーザー ID が利用可能なら、そこから DB を参照して TenantId を取得する
  • メッセージキューを使う場合は メッセージのペイロードに TenantId を含める

まとめ

EF Core でマルチテナントを実装するポイントは以下のとおりです。

  • シングル DB では Global Query Filters + TenantId カラムで実現できる
  • マルチ DB では動的な接続文字列の切り替えが必要になる
  • TenantProvider を使って現在のテナントを識別する
  • セキュリティ要件が高い場面では JWT クレームや API キーで TenantId を受け渡す
  • 接続文字列は Azure Key Vault 等の安全な場所に保管する

基本原則を理解すれば、より堅牢でセキュアなマルチテナントシステムへと発展させることができます。