Django は、会社のアプリケーションを開発するために選択できる人気のあるフレームワークです。しかし、複数のクライアントが使用する SaaS アプリケーションを作成したい場合はどうすればよいでしょうか?どのようなアーキテクチャを選択すればよいでしょうか?このタスクにどのようにアプローチできるかを見てみましょう。 シングルテナントアーキテクチャ 最も簡単なアプローチは、クライアントごとに個別のインスタンスを作成することです。 Django アプリケーションとデータベースがあるとします。次に、クライアントごとに独自のデータベースとアプリケーション インスタンスを実行する必要があります。つまり、各アプリケーション インスタンスにはテナントが 1 つだけ存在します。 このアプローチは実装が簡単です。必要なのは、所有しているすべてのサービスの新しいインスタンスを開始するだけです。しかし同時に、問題が発生する可能性があります。各クライアントのインフラストラクチャの ことになります。クライアントの数がわずかである場合、または各インスタンスが小さい場合は、大した問題ではないかもしれません。 コストが大幅に増加する ただし、100,000 の組織に企業メッセンジャーを提供する大企業を構築していると仮定しましょう。新しいクライアントごとにインフラストラクチャ全体を複製するのにどれだけの費用がかかるかを想像してみてください。また、アプリケーションのバージョンを更新する必要がある場合、クライアントごとに展開する必要があるため、 。 展開も遅くなります マルチテナントアーキテクチャ アプリケーションに多数のクライアントがある場合に役立つもう 1 つのアプローチ、それがマルチテナント アーキテクチャです。これは、 と呼ばれる複数のクライアントが存在しますが、それらはすべてアプリケーションの 1 つのインスタンスのみを使用することを意味します。 テナント このアーキテクチャは、各クライアントの専用インスタンスの高コストの問題を解決しますが、 クライアントのデータが他のクライアントから安全に分離されていることをどのように確認できるかという新しい問題を引き起こします。 次のアプローチについて説明します。 の使用: 各データベース テーブルに追加する必要がある外部キーによって、どのテナントがデータを所有しているかを識別できます。 共有データベースと共有データベース スキーマ を使用する: この方法では、複数のデータベース インスタンスを維持する必要がなく、適切なレベルのテナント データ分離が得られます。 共有データベースを使用するが、別個のデータベース スキーマ の使用 : シングルテナントの例に似ていますが、同じではありません。共有アプリケーション インスタンスを引き続き使用し、テナントを確認して使用するデータベースを選択します。 個別のデータベース これらのアイデアをさらに深く掘り下げて、それらを Django アプリケーションと統合する方法を見てみましょう。 共有スキーマを備えた共有データベース このオプションは、テーブルにForeignKeyを追加し、それを使用して各テナントに適切なデータを選択するために最初に思い浮かぶかもしれません。ただし、これには大きな欠点があります。テナントのデータはまったく分離されていないため、小さなプログラミング エラーでテナントのデータが間違ったクライアントに漏洩する可能性があります。 からデータベース構造の例を見てみましょう。 Django のドキュメント from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) どのレコードがどのテナントによって所有されているかを特定する必要があります。したがって、既存の各テーブルに テーブルと外部キーを追加する必要があります。 Tenant class Tenant(models.Model): name = models.CharField(max_length=200) class Question(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) コードを少し単純化するために、作成する他の各モデルで再利用される抽象的な基本モデルを作成できます。 class Tenant(models.Model): name = models.CharField(max_length=200) class BaseModel(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) class Meta: abstract = True class Question(BaseModel): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(BaseModel): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) ご覧のとおり、ここには少なくとも 2 つの大きなリスクがあります。 、または 開発者が新しいモデルにテナント フィールドを追加することを忘れる可能性があること 開発者がデータをフィルタリングするときにこのフィールドを使用することを忘れる可能性があることです。 この例のソース コードは、GitHub: にあります。 https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema 個別のスキーマを持つ共有データベース 共有スキーマのリスクを念頭に置いて、別のオプションを検討してみましょう。データベースは引き続き共有されますが、テナントごとに専用のスキーマを作成します。実装については、人気のあるライブラリ ( ) を参照してください。 django-tenants ドキュメント 私たちの小さなプロジェクトに を追加しましょう (公式のインストール手順は あります)。 django-tenants ここに 最初のステップは、 によるライブラリのインストールです。 pip pip install django-tenants モデルを変更します。 モデルは別のアプリになり、 モデルと モデルはテナントとの接続を失います。異なるテナントのデータは個別のスキーマに存在するため、個々のレコードをテナントの行にリンクする必要はなくなります。 Tenant Question Choice ファイル tenants/models.py from django.db import models from django_tenants.models import TenantMixin, DomainMixin class Tenant(TenantMixin): name = models.CharField(max_length=200) # default true, schema will be automatically created and synced when it is saved auto_create_schema = True class Domain(DomainMixin): # a required table for django-tenants too ... ファイル polls/models.py from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) Question と Choice には Tenant への外部キーが存在しないことに注意してください。 もう 1 つの変更点は、テナントが別のアプリ内にあることです。これはドメインを分離するためだけでなく、共有スキーマに テーブルを保存する必要があるため、テナントごとに テーブルが作成されるため重要です。スキーマ。 tenants polls 複数のスキーマとテナントをサポートするために ファイルを変更します。 settings.py DATABASES = { 'default': { 'ENGINE': 'django_tenants.postgresql_backend', # .. } } DATABASE_ROUTERS = ( 'django_tenants.routers.TenantSyncRouter', ) MIDDLEWARE = ( 'django_tenants.middleware.main.TenantMainMiddleware', #... ) TEMPLATES = [ { #... 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', #... ], }, }, ] SHARED_APPS = ( 'django_tenants', # mandatory 'tenants', # you must list the app where your tenant model resides in 'django.contrib.contenttypes', # everything below here is optional 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.admin', ) TENANT_APPS = ( # your tenant-specific apps 'polls', ) INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] TENANT_MODEL = "tenants.Tenant" TENANT_DOMAIN_MODEL = "tenants.Domain" 次に、移行を作成して適用しましょう。 python manage.py makemigrations python manage.py migrate_schemas --shared その結果、パブリック スキーマが作成され、共有テーブルのみが含まれることがわかります。 スキーマのデフォルト テナントを作成する必要があります。 public python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant 要求されたら、 を に設定します。 is_primary True 次に、サービスの実際のテナントの作成を開始できます。 python manage.py create_tenant --domain-domain=tenant1.com --schema_name=tenant1 --name=tenant_1 python manage.py create_tenant --domain-domain=tenant2.com --schema_name=tenant2 --name=tenant_2 データベースには、 テーブルを含むスキーマがさらに 2 つあることに注意してください。 polls これで、テナント用に設定したドメインで API を呼び出すときに、さまざまなスキーマから質問と選択肢を取得できるようになり、これですべて完了です。 セットアップはより複雑に見え、既存のアプリを移行する場合はさらに困難になる可能性がありますが、このアプローチ自体にはデータのセキュリティなど多くの利点があります。 サンプルのコードは あります。 ここに 個別のデータベース 今日説明する最後のアプローチは、さらに進んで、テナント用に個別のデータベースを用意することです。 今回は、いくつかのデータベースを用意します。 テナントのデータベース名へのマッピングなどの共有データを に保存し、テナントごとに個別のデータベースを作成します。 default_db 次に、settings.py でデータベース構成を設定する必要があります。 DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, } これで、QuerySet メソッド 呼び出して、各テナントのデータを取得できるようになります。 using Questions.objects.using('tenant_1')… この方法の欠点は、以下を使用して各データベースにすべての移行を適用する必要があることです。 python manage.py migrate --database=tenant_1 また、テナントごとに新しいデータベースを作成することは、 使用する場合や、共有スキーマ アプローチのように外部キーを単に使用する場合に比べて、利便性が劣る可能性があります。 django-tenants 一方、テナントのデータの分離は非常に優れており、データベースを物理的に分離できます。もう 1 つの利点は、 で必要とされる Postgresql のみの使用に制限されず、ニーズに合った任意のエンジンを選択できることです。 django-tenants 複数データベースのトピックの詳細については、Django の を参照してください。 ドキュメント 比較 シングルテナント 共有スキーマを使用した MT 別のスキーマを使用した MT 別個のデータベースを使用した MT データの分離 ✅高い ❌最低 ✅高い ✅高い 誤ってデータが漏洩する危険性 ✅低い ❌高い ✅低い ✅低い インフラコスト ❌テナントごとに高くなる ✅下段 ✅下段 ✅❌ シングルテナントよりも低い 導入速度 ❌テナントごとに値下げ ✅ ✅❌ 移行はスキーマごとに実行する必要があるため、時間がかかります。 ✅❌ 移行はデータベースごとに実行する必要があるため、時間がかかります。 実装が簡単 ✅ ❌ サービスがすでにシングルテナント アプリとして実装されている場合は、多くの変更が必要です ✅ ✅ 結論 上記をすべて要約すると、この問題に対する特効薬はないようです。それぞれのアプローチには長所と短所があるため、どのようなトレードオフがあるかを決定するのは開発者次第です。 個別のデータベースはテナントのデータを最適に分離し、実装が簡単ですが、メンテナンスのコストが高くなります。更新するデータベースが多くなり、データベースの接続数も多くなります。 別のスキーマを持つ共有データベースの実装は少し複雑で、移行時に問題が発生する可能性があります。 シングル テナントは実装が最も簡単ですが、テナントごとにサービスのコピー全体が存在するため、リソースの過剰消費によるコストがかかります。