フォーム読み込み中
ソフトバンク アドベントカレンダー 2024 11日目の記事を担当する高山です。普段は、TASUKIというAIの教師データ作成ツール、RAG用データ作成ツールの開発を行っております。
TASUKIの開発チームは少人数体制ですが、効率的なワークフローを構築し、お客様からの要望に迅速に応えることを重視しています。その結果として、週1回・月4回の機能アップデートを継続的に実現しています。この高いリリース頻度は、開発チームの実装力や機動力だけでなく、日々の試行錯誤と継続的なカイゼン活動によるものです。
さらに、リリース頻度が高いだけでなく、必ずリリース前にはE2E(エンドツーエンド)試験を含む確認試験の実施を徹底しています。これにより、機能の動作とUXにおいて重大な問題が発生しないことを保証しています。品質とスピードを両立させた開発プロセスがTASUKIの開発チームの強みです。
TASUKIはマルチテナントで開発が必須となります。マルチテナントは、1つのシステムやアプリケーションを複数のユーザーやテナントが共有します。その場合、一つのシステムで複数のユーザーの情報をデータベースで管理しないといけません。これらのデータは適切に隔離し、他のユーザーの情報を第三者のデータと混同することのないようにすることが特に重要です。
本稿では、Entity Framework Coreで、マルチテナント/マルチユーザ環境におけるデータベースの論理分割方法について紹介します。
マルチテナント環境において、異なるユーザやテナントのデータを効果的に分離し、セキュリティを確保しつつ、アプリケーションのパフォーマンスを維持するためには、グローバルクエリフィルターを利用することが効果的です。
グローバルクエリフィルターはEntity Framework Coreの機能であり、これにより特定の条件を常に適用することで、データアクセスの安全性と効率性を高めることができます。
グローバルフィルターの実装目的は、RLS(Row Level Security/行レベルセキュリティ)と似ていますが、大きな違いがあります。RLSはデータベースレベルでセキュリティポリシーを強制しますが、グローバルクエリフィルターはアプリケーションレベルで設定されます。これにより、開発者がより柔軟にフィルタリングロジックを管理できます。
また、Entity Framework Coreは、Entity Frameworkデータアクセス技術の軽量版であり、拡張性に優れたO/Rマッパーとして機能します。
アプリケーションレベルで実装すると柔軟な開発が行えるのがメリットで、下記のような活用例が考えられます。
データベースに複数のテナント、又はユーザが所属
論理削除の実装
公開、非公開データのフィルタリング
期限切れデータのフィルタリング
アクセス権に基づくフィルタリング
バージョン | 内容 |
---|---|
.NET SDK 8.0.101 | .NETフレームワーク |
Entity Framework Core 8.0.2 | ORM(オブジェクトリレーショナルマッピング)フレームワーク |
PostgreSQL 14 | リレーショナルデータベース |
下記サンプルをカスタマイズして実行します
web-db:
image: postgres:14
container_name: web_db
environment:
POSTGRES_USER: tasuki
POSTGRES_PASSWORD: tasuki
POSTGRES_DB: tasuki
ports:
- "5432:5432"
アクセスした company_id でフィルタを適用するため、company_id が 1 のユーザーがアクセスした場合、そのユーザーは他のレコードを参照できません。
company_id | company_name |
---|---|
1 | テストカンパニー1 |
2 | テストカンパニー2 |
public partial class Company
{
public long CompanyId { get; set; }
public string CompanyName { get; set; }
public virtual ICollection<User> Users { get; set; } = new List<CustomerUser>();
public virtual ICollection<Project> Projects { get; set; } = new List<ProjectInfo>();
}
プロジェクトにアサインされたメンバーに基づいてプロジェクトをフィルタリングし、アサインされていないメンバーにはプロジェクトを参照不可とします。
例えば、user_id が 100 のユーザーがアクセスした場合、そのユーザーがアサインされているプロジェクト(プロジェクトIDが 1 と 2)以外のプロジェクトは参照できません。このようにすることで、アサインされていないメンバーが不要なプロジェクト情報を参照するのを制限します。
project_id | project_name |
---|---|
1 | テストプロジェクト1 |
2 | テストプロジェクト2 |
3 | テストプロジェクト3 |
public partial class Project
{
public string ProjectId { get; set; }
public long UserId { get; set; }
public string ProjectName { get; set; }
public long CompanyId { get; set; }
public virtual Company Company { get; set; }
public virtual User User { get; set; }
public virtual ICollection<ProjectMember> ProjectMembers { get; set; } = new List<ProjectMember>();
}
mamber_id | project_id | user_id |
---|---|---|
1 | 1 | 100 |
2 | 1 | 200 |
3 | 2 | 100 |
4 | 3 | 200 |
public partial class Member
{
public string MemberId { get; set; }
public string ProjectId { get; set; }
public long UserId { get; set; }
public virtual User User { get; set; }
public virtual Project Project { get; set; }
}
データベースコンテキストを継承したクラスで、OnModelCreating メソッドをオーバーライドしています。
ここでは、前述したテナントのフィルタリングパターンと、アサインプロジェクトのフィルタリングパターンを定義しています。
また、EF.Property は、動的プロパティアクセスを可能にします。この方法により、コンパイル時には存在が確定していないプロパティに基づくフィルタリングも可能になります。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
)
// テナントでフィルタ
modelBuilder.Entity<Company>().HasQueryFilter(x => EF.Property<long>(x, "CompanyId") == _companyId);
// アサインプロジェクトでフィルタ
modelBuilder.Entity<Project>().HasQueryFilter(x => x.Members.Any(y => EF.Property<long>(y, "UserId") == _userId));
}
この記事では、マルチテナント/マルチユーザ環境におけるグローバルフィルタについて紹介しました。
グローバルフィルタを使用することで、安全かつ柔軟にデータフィルタリングが行える点が非常に驚きであり、私たちのアプリケーション開発で必須機能となってます!
私たちは常にセキュリティを意識したアプリケーション開発を行っております。さらなる改善のために、より良い機能や方法を模索しております。もし推奨される機能や評判の良い手法がありましたら、ぜひ紹介して下さい!
TASUKIでは、学習データアノテーションの代行サービスに加え、RAG向けのデータ構造化サービスを提供しています。
TASUKIチームの技術やChatGPTやRAGの精度向上にご興味がある方は、ぜひご相談ください。
データ構造化を代行し、検索拡張生成(RAG)の検索精度の向上をご支援。さらに構造化代行により リソース不足を解決し、企業のLLM活用を促進。
条件に該当するページがございません