DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Enterprise AI Trend Report: Gain insights on ethical AI, MLOps, generative AI, large language models, and much more.

2024 Cloud survey: Share your insights on microservices, containers, K8s, CI/CD, and DevOps (+ enter a $750 raffle!) for our Trend Reports.

PostgreSQL: Learn about the open-source RDBMS' advanced capabilities, core components, common commands and functions, and general DBA tasks.

AI Automation Essentials. Check out the latest Refcard on all things AI automation, including model training, data security, and more.

Related

  • Live Database Migration
  • Schema Change Management Tools: A Practical Overview
  • Advanced Maintenance of a Multi-Database Citus Cluster With Flyway
  • Exploring a Paradigm Shift for Relational Database Schema Changes

Trending

  • Some Thoughts on Bad Programming Practices
  • Organizing Knowledge With Knowledge Graphs: Industry Trends
  • Getting Started With NCache Java Edition (Using Docker)
  • Data Processing in GCP With Apache Airflow and BigQuery
  1. DZone
  2. Data Engineering
  3. Databases
  4. Understanding Multi-Tenancy: Core Logic and High-Level Code With Django

Understanding Multi-Tenancy: Core Logic and High-Level Code With Django

Custom implementations of multi-tenancy can allow developers to innovate and tailor solutions according to unique business needs and use cases.

By 
Jatinkumar Patel user avatar
Jatinkumar Patel
·
Feb. 29, 24 · Tutorial
Like (1)
Save
Tweet
Share
2.5K Views

Join the DZone community and get the full member experience.

Join For Free

Multi-tenant applications are crucial for efficiently serving multiple clients from a single shared instance, offering cost savings, scalability, and simplified maintenance. Such applications can allow hospitals and clinics to manage patient records securely, enable financial institutions to provide personalized banking services and help streamline inventory management and customer relationship management across multiple stores. The primary functionality of multi-tenant applications resides in their capacity to serve numerous clients through a single installation of the application. In this architecture, each client, referred to as a tenant, maintains complete data isolation, ensuring data privacy and security.

There are multiple third-party libraries available to implement multi-tenancy in Django. However, custom implementations of multi-tenancy can allow developers to innovate and tailor solutions according to unique business needs and use cases. Therefore, in this blog, we will show the core logic of how multi-tenancy is implemented using Django with a high-level code.

zymr

Approaches to Multi-Tenancy

Multi-tenancy offers to serve diverse requirements that may vary in their constraints. Tenants may have similar data structures and security requirements or might be looking for some flexibility that allows each tenant to have their own schema. Therefore, there are many approaches to achieving multi-tenancy:

  1. Shared database with shared schema
  2. Shared database with isolated schema
  3. Isolated database with a shared app server.

1. Shared Database With Shared Schema

This is the simplest method. It has a shared database and schema. All tenant’s data will be stored in the same DB and schema.

Create a tenant app and create two models: Tenant and Tenant Aware Model to store Tenant base information.

Python
 
class Tenant(models.Model):

	name = models.CharField(max_length=200)

	subdomain_prefix = models.CharField(max_length=200, unique=True)

class TenantAwareModel(models.Model):

	tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)



class Meta:

    abstract = True



#Inherit TenantAwareModel into your all app models:


class User(TenantAwareModel):
...

class Order(TenantAwareModel):
...


Identifying Tenants

One method to identify tenants is to use a subdomain. Let’s say your main domain is www.example.com, and customer subdomains are:

  1. cust1.example.com
  2. cust2.example.com
Write a method to extract the tenant from the request in utils.py file:

Python
 
from .models import Tenant



def hostname_from_request(request):

    # split at `:` to remove port

    return request.get_host().split(':')[0].lower()



def tenant_from_request(request):

    hostname = hostname_from_request(request)

    subdomain_prefix = hostname.split('.')[0]

    return Tenant.objects.filter(subdomain_prefix=subdomain_prefix).first()


Use tenant_from_request method in views:

Python
 
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 super().get_queryset().filter(tenant=tenant)


Also, update ALLOWED_HOSTS your settings.py. Mine looks like this:

Python
 
ALLOWED_HOSTS = ['example.com', '.example.com'].


2. Shared Database With Isolated Schema

In the first option, we used a ForeignKey to separate the tenants. It is simple, but there is no way to limit access to a single tenant’s data at the DB level. Also, getting the tenant from the request and filtering on it is all over your codebase rather than a central location.

One solution to the above problem is to create a separate schema within a shared database to isolate tenant-wise data. Let’s say you have two schemas cust1 and cust2.

Add this code to utils.py file:

Python
 
def get_tenants_map():

    # cust1 and cust2 are your database schema names.

    return {

        "cust1.example.com": "cust1",

        "cust2.example.com": "cust2",

    }

def hostname_from_request(request):

    return request.get_host().split(':')[0].lower()



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 will set the schema in the middleware before any view code comes into the picture, so any ORM code will pull and write the data from the tenant’s schema.

Create a new middleware like this:

Python
 
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

Python
 
MIDDLEWARE = [

    # ...

    'tenants.middlewares.TenantMiddleware',

]


3. Isolated Database With a Shared App Server

In this third option, We will use a separate database for all tenants. We will use a thread local feature to store the DB value during the life cycle of the thread.

Add multiple databases in the setting file:

Python
 
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:

Python
 
def tenant_db_from_request(request):

    return request.get_host().split(':')[0].lower()



Create a TenantMiddleware like this:

Python
 
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.local()



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 name. This TenantRouter will be assigned to DATABASE_ROUTERS in settings.py file.

Python
 
from tenants.middleware import get_current_db_name

class CustomDBRouter:



    def db_for_read(self, model, **hints):

        return get_current_db_name()



    def db_for_write(self, model, **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:

Python
 
MIDDLEWARE = [

    # ...

    "tenants.middlewares.TenantMiddleware",

]

DATABASE_ROUTERS = ["tenants.router.TenantRouter"]


Conclusion

You can choose a way to implement multi-tenancy per requirement and complexity level. However, an isolated DB and schema is the best way to keep data isolated when you do not want to mix all tenant's data in a single database due to security concerns.

Database Django (web framework) Schema

Published at DZone with permission of Jatinkumar Patel. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Live Database Migration
  • Schema Change Management Tools: A Practical Overview
  • Advanced Maintenance of a Multi-Database Citus Cluster With Flyway
  • Exploring a Paradigm Shift for Relational Database Schema Changes

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: