Multi-tenant purposes are essential for effectively serving a number of shoppers from a single shared occasion, providing price financial savings, scalability, and simplified upkeep. Such purposes can permit hospitals and clinics to handle affected person information securely, allow monetary establishments to offer personalised banking providers and assist streamline stock administration and buyer relationship administration throughout a number of shops. The first performance of multi-tenant purposes resides of their capability to serve quite a few shoppers by a single set up of the appliance. On this structure, every consumer, known as a tenant, maintains full information isolation, guaranteeing information privateness and safety.
There are a number of third-party libraries out there to implement multi-tenancy in Django. Nevertheless, customized implementations of multi-tenancy can permit builders to innovate and tailor options in keeping with distinctive enterprise wants and use circumstances. Due to this fact, on this weblog, we’ll present the core logic of how multi-tenancy is carried out utilizing Django with a high-level code.
Approaches to Multi-Tenancy
Multi-tenancy provides to serve numerous necessities which will differ of their constraints. Tenants could have comparable information buildings and safety necessities or may be in search of some flexibility that permits every tenant to have their very own schema. Due to this fact, there are numerous approaches to attaining multi-tenancy:
- Shared database with shared schema
- Shared database with remoted schema
- Remoted database with a shared app server.
1. Shared Database With Shared Schema
That is the only methodology. It has a shared database and schema. All tenant’s information will probably be saved in the identical DB and schema.
Create a tenant app and create two fashions: Tenant and Tenant Conscious Mannequin to retailer Tenant base info.
class Tenant(fashions.Mannequin):
title = fashions.CharField(max_length=200)
subdomain_prefix = fashions.CharField(max_length=200, distinctive=True)
class TenantAwareModel(fashions.Mannequin):
tenant = fashions.ForeignKey(Tenant, on_delete=fashions.CASCADE)
class Meta:
  summary = True
#Inherit TenantAwareModel into your all app fashions:
class Consumer(TenantAwareModel):
...
class Order(TenantAwareModel):
...
Figuring out Tenants
One methodology to establish tenants is to make use of a subdomain. Let’s say your essential area is www.instance.com, and buyer subdomains are:
- cust1.instance.com
- cust2.instance.com
Write a way to extract the tenant from the request in utils.py file:
from .fashions import Tenant
def hostname_from_request(request):
  # cut up at `:` to take away port
  return request.get_host().cut up(':')[0].decrease()
def tenant_from_request(request):
  hostname = hostname_from_request(request)
  subdomain_prefix = hostname.cut up('.')[0]
  return Tenant.objects.filter(subdomain_prefix=subdomain_prefix).first()
Use tenant_from_request
methodology in views:
from tenants.utils import tenant_from_request
class OrderViewSet(viewsets.ModelViewSet):
  queryset = Order.objects.all()
  serializer_class = OrderSerializer
  def get_queryset(self):
    tenant = tenant_from_request(self.request)
    return tremendous().get_queryset().filter(tenant=tenant)
Additionally, replace ALLOWED_HOSTS
your settings.py. Mine seems like this:
ALLOWED_HOSTS = ['example.com', '.example.com'].
2. Shared Database With Remoted Schema
Within the first possibility, we used a ForeignKey to separate the tenants. It’s easy, however there isn’t any strategy to restrict entry to a single tenant’s information on the DB degree. Additionally, getting the tenant from the request and filtering on it’s all over your codebase moderately than a central location.
One resolution to the above drawback is to create a separate schema inside a shared database to isolate tenant-wise information. Let’s say you’ve gotten two schemas cust1 and cust2.
Add this code to utils.py file:
def get_tenants_map():
  # cust1 and cust2 are your database schema names.
  return {
    "cust1.instance.com": "cust1",
    "cust2.instance.com": "cust2",
  }
def hostname_from_request(request):
  return request.get_host().cut up(':')[0].decrease()
def tenant_schema_from_request(request):
  hostname = hostname_from_request(request)
  tenants_map = get_tenants_map()
  return tenants_map.get(hostname)
def set_tenant_schema_for_request(request):
  schema = tenant_schema_from_request(request)
  with connection.cursor() as cursor:
    cursor.execute(f"SET search_path to {schema}")
We are going to set the schema within the middleware earlier than any view code comes into the image, so any ORM code will pull and write the information from the tenant’s schema.
Create a brand new middleware like this:
from tenants.utils import set_tenant_schema_for_request
class TenantMiddleware:
  def __init__(self, get_response):
    self.get_response = get_response
  def __call__(self, request):
    set_tenant_schema_for_request(request)
    response = self.get_response(request)
    return response
And add it to your settings.MIDDLEWARES
MIDDLEWARE = [
  # ...
  'tenants.middlewares.TenantMiddleware',
]
3. Remoted Database With a Shared App Server
On this third possibility, We are going to use a separate database for all tenants. We are going to use a thread native function to retailer the DB worth throughout the life cycle of the thread.
Add a number of databases within the setting file:
DATABASES = {
  "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "default.db"},
  "cust1": {"ENGINE": "django.db.backends.sqlite3", "NAME": "cust1.db"},
  "cust2": {"ENGINE": "django.db.backends.sqlite3", "NAME": "cust2.db"},
}
Add this code to utils.py file:
def tenant_db_from_request(request):
  return request.get_host().cut up(':')[0].decrease()
Create a TenantMiddleware
like this:
import threading
from django.db import connections
from .utils import tenant_db_from_request
import threading
from django.db import connections
from .utils import tenant_db_from_request
THREAD_LOCAL = threading.native()
class TenantMiddleware:
  def __init__(self, get_response):
    self.get_response = get_response
  def __call__(self, request):
    db = tenant_db_from_request(request)
    setattr(THREAD_LOCAL, "DB", db)
    response = self.get_response(request)
    return response
def get_current_db_name():
  return getattr(THREAD_LOCAL, "DB", None)
def set_db_for_router(db):
  setattr(THREAD_LOCAL, "DB", db)
Now, write a TenantRouter
class to get a database title. This TenantRouter will probably be assigned to DATABASE_ROUTERS
in settings.py file.
from tenants.middleware import get_current_db_name
class CustomDBRouter:
  def db_for_read(self, mannequin, **hints):
    return get_current_db_name()
  def db_for_write(self, mannequin, **hints):
    return get_current_db_name()
  def allow_relation(self, obj1, obj2, **hints):
    return get_current_db_name()
  def allow_migrate(self, db, app_label, model_name=None, **hints):
    return get_current_db_name()
Add TenantMiddleware
and CustomDBRouter
to settings.py file:
MIDDLEWARE = [
  # ...
  "tenants.middlewares.TenantMiddleware",
]
DATABASE_ROUTERS = ["tenants.router.TenantRouter"]
Conclusion
You may select a strategy to implement multi-tenancy per requirement and complexity degree. Nevertheless, an remoted DB and schema is the easiest way to maintain information remoted when you don’t want to combine all tenant’s information in a single database as a consequence of safety considerations.