Designing a multi-tenant Django app
Setting and getting the tenant Link to heading
Thread-Local Link to heading
Thread-Local
using asgiref that is a drop-in replacement for
threading.local with support to threads and asyncio.
from asgiref.local import Local
local = Local()
def set_tenant(id):
local.id = id
def get_tenant():
return getattr(local, "id", None)
- Pros
- Globally available
- Cons
Marking models as tenant-aware Link to heading
Inheritance Link to heading
django-multitenant approach.
# models.py
class TenantModel(models.Model):
objects = TenantManager()
class Meta:
abstract = True
class MyModel(TenantModel):
pass
- Pros
- more flexibility
- Cons
- doesn’t work well for third party apps
- the flaws of inheritance
Settings by apps Link to heading
django-tenants approach: https://django-tenants.readthedocs.io/en/latest/install.html#configure-tenant-and-shared-applications
# settings.py
SHARED_APPS = (
...
)
TENANT_APPS = (
# tenant-specific apps
...
)
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
- Pros
- work for third party apps
- Cons
- less flexibility
Settings by models (idea) Link to heading
Inspired by Settings by apps.
# settings.py
TENANT_MODELS = (
'core.MyModel',
...
)
Decorator (idea) Link to heading
Inspired by attrs.
@tenant_aware
class MyModel(models.Model):
objects = models.Manager()
- Pros
- more flexibility
- no inheritance
- Cons
- doesn’t work well for third party apps
Applying the tenant Link to heading
Manager setting tenant in all queries Link to heading
Example:
# middleware.py
class TenantMiddleware:
...
def __call__(self, request):
set_tenant(extract_tenant(request))
response = self.get_response(request)
return response
# models.py
class TenantManager(models.Manager):
def get_queryset(self):
assert_tenant()
return super().get_queryset().filter(tenant_id=get_tenant()})
class TenantModel(models.Model):
objects = TenantManager()
unsafe_objects = models.Manager()
- Pros
- No need to set the tenant ID in each query.
- Cons
- Admin doesn’t work because it will use
objectswithout setting the tenant. - Tests will need more setup.
- Admin doesn’t work because it will use
Manager setting tenant when available Link to heading
# middleware.py
class TenantMiddleware:
...
def __call__(self, request):
set_tenant(extract_tenant(request))
response = self.get_response(request)
return response
# models.py
class TenantManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
if tenant := get_tenant():
assert_tenant()
qs = qs.filter(tenant_id=tenant)
return qs
class TenantModel(models.Model):
objects = TenantManager()
- Pros
- Works with Django Admin
- Tests better with tests
- Cons
- Less secure because some bug could let the query without the tenant.