Multitenancy w Django, czyli jak oddzielić klientów w jednej aplikacji
Utworzono: wrzesień 2023 Zaktualizowano: 1 października 2026
zadanie rekrutacyjne
Python, Django, django-tenants, PostgreSQL
Jedna aplikacja, wielu klientów i żaden nie może zobaczyć danych innego.
Każdy klient ma widzieć tylko swoje dane, pełna separacja. Wystarczy jedno zapytanie bez filtra po kliencie i dane jednej firmy lądują na ekranie drugiej.
Trudne jest to, że separacja musi działać wszędzie, nie tylko w widokach: w panelu admina, w zadaniach w tle, w raportach, w cache, w plikach wrzucanych przez użytkowników. Jak opiera się ją na tym, że każdy pamięta o filter(client=…), to ktoś w końcu zapomni.
Trzy drogi
Osobna baza na klienta daje najmocniejszą izolację i łatwo zrobić backup albo usunąć jednego klienta, tylko że każda baza to osobne połączenia, migracje i koszty, więc ma sens przy kilku dużych klientach. Wspólne tabele z kolumną klienta są najtańsze i skalują się do tysięcy klientów, ale izolacja zależy od kodu, więc filtr trzeba wymusić w jednym miejscu, menedżerem modeli, biblioteką django-multitenant albo Row Level Security w PostgreSQL. Pośrodku jest osobny schemat w PostgreSQL na klienta: jedna baza, każdy klient ma swój schemat z tymi samymi tabelami, a aplikacja przy każdym żądaniu przełącza search_path.
Wybrałem schematy
Osobny schemat na klienta z biblioteką django-tenants. Klient jest rozpoznawany po domenie, a reszta aplikacji pisze zwykłe zapytania, bez filtra po kliencie, więc nie ma czego zapomnieć. Minus jest taki, że migracje idą przez wszystkie schematy i przy bardzo wielu klientach robi się to wolne.
Konfiguracja to głównie podział aplikacji na wspólne i te, które każdy klient ma u siebie:
DATABASES = { "default": {"ENGINE": "django_tenants.postgresql_backend", "NAME": "app"}, } DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",) MIDDLEWARE = [ "django_tenants.middleware.main.TenantMainMiddleware", "django.middleware.security.SecurityMiddleware", # ... ] SHARED_APPS = ["django_tenants", "customers", "django.contrib.contenttypes", "django.contrib.auth"] TENANT_APPS = ["orders", "invoices"] INSTALLED_APPS = SHARED_APPS + [app for app in TENANT_APPS if app not in SHARED_APPS] TENANT_MODEL = "customers.Client" TENANT_DOMAIN_MODEL = "customers.Domain"
Klient i jego domena to zwykłe modele:
from django.db import models from django_tenants.models import DomainMixin, TenantMixin class Client(TenantMixin): name = models.CharField(max_length=100) auto_create_schema = True class Domain(DomainMixin): pass
Najłatwiej przeoczyć zadania w tle, bo tam nie ma żądania, z którego wiadomo, o jakiego klienta chodzi, więc schemat trzeba ustawić samemu:
from django_tenants.utils import tenant_context from customers.models import Client def send_monthly_invoices(): for client in Client.objects.exclude(schema_name="public"): with tenant_context(client): ... # tu zwykłe zapytania, już w schemacie klienta
Migracje puszcza się przez migrate_schemas, osobno dla części wspólnej (--shared) i dla schematów klientów.