Skip to content

Part 2: Integration with the library

Please make sure that the library has been installed. If not, head over to the installation section and then come back here.

Pre-requisites

This tutorial uses Docker to install and run Redis. We use Redis as the backend for storing tenant-specific metadata.

Install Docker from its official website - there are official runtimes for Mac OS, Windows and Linux that make it easy to use across all platforms.

Note

Although you could manually install and run Redis specific to your platform, we recommend using Docker as it is easier to run/manage such components without having any platform dependencies.

Step 1: Setup

Add the application to the list of INSTALLED_APPS as follows:

# mt_site/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tenant_router.apps.TenantRouterConfig', # <-- added here
    'mt_site.apps.MtAppConfig'
]
...

Warning

Make sure to add this at the top of any user defined app. If not, the entire application could potentially malfunction.

The library also ships with a middleware that is responsible for setting the correct tenant context based on the request headers' for a particular request/response cycle. It must be added to the list of MIDDLEWARES as follows:

# mt_site/settings.py
...
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'tenant_router.middleware.TenantContextMiddleware', # <-- added here
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
...

Important

Make sure to add this at the top of any middleware which depends on the Django ORM. If not, the tenant context would be unavailable and any corresponding queries would fail.

Step 2: Configure

Add the following configuration options to the settings.py file as follows:

# mt_site/settings.py
...
DATABASE_ROUTERS = [
    'tenant_router.orm_backends.django_orm.router.DjangoOrmRouter'
]

TENANT_ROUTER_ORM_SETTINGS = {
    'django_orm': {
        'SETTINGS_KEY': 'DATABASES',
    }
}

TENANT_ROUTER_SERVICE_NAME = 'mt_site'

TENANT_ROUTER_PUBSUB_SETTINGS = {
    'BACKEND': 'tenant_router.pubsub.backends.redis.RedisPubSub',
    'LOCATION': {
        'HOST': '0.0.0.0',
        'PORT': '6379'
    }
}

TENANT_ROUTER_PUBSUB_ENABLED = True
...

Note

To know more about the list of configuration options available and what each of them mean, refer to the
settings page.

Next, we'll have to setup the config store as part of the CACHES dictionary. Since Redis will be used, we'll need to install a Django compatible redis cache backend.

Although any Django compatible redis backend can be used, for the purposes of this tutorial, we'll be using django-redis.

$ pip install -U django-redis

Now, add an entry to the CACHES dictionary in settings.py as follows:

# mt_site/settings.py
...
CACHES = {
    "tenant_router_config_store": {
        "BACKEND": "django_redis.cache.RedisCache",
        "TIMEOUT": None,
        "LOCATION": "redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}".format(
            REDIS_HOST='0.0.0.0',
            REDIS_PORT='6379',
            REDIS_DB='5'
        ),
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "SERIALIZER": "django_redis.serializers.json.JSONSerializer"
        }
    }
}
...

Also, the DATABASES dictionary which was configured in Part 1 of this tutorial will have to be refactored as follows:

# mt_site/settings.py
...
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql"
    }
}
...

Also, the HospitalView has to be refactored as follows:

# mt_demo/views.py

import json

from django.http import HttpResponse
from django.views import View

from tenant_router.threadlocal import tls_tenant_manager
from tests.dummy_app.models import Hospital

class HospitalView(View):
    def get(self, request):
        data = list(Hospital.objects.all().values())
        return HttpResponse(
            content=json.dumps(
                {
                    "data": data,
                    "tenant_id": tls_tenant_manager.current_tenant_context.id                
                }   
            ),
            status=200
        )

Step 3: Creating the tenant_config.json file

Create a file called tenant_config.json in the directory path ~/mt_site/. Now the directory layout would look like this:

mt_site/
    manage.py
    tenant_config.json <-- created here
    mt_site/
        __init__.py
        asgi.py
        settings.py
        urls.py
        wsgi.py

Add the following content within this newly created file:

{
    "mapping_metadata": {
        "POSTGRES_URL": "orm_config_django_orm_default",
    },
    "tenant_config": {
        "tenant-1.test.com": {
            "mt_site": {
                "orm_config": {
                    "django_orm": {
                        "default": {
                            "HOST": "127.0.0.1",
                            "PORT": "5432",
                            "USER": "your_db_username",
                            "PASSWORD": "your_db_password",
                            "NAME": "tenant_1"
                        }
                    }
                }
            }
        }
    }
}

Note

For more info about the schema of this file, refer to this link.

To start Redis, run the follwoing command:

$ docker run -p 6379:6379 -d redis:latest

In order to load contents of the file into Redis, run the following command:

Important

Before running the command below, make sure that settings.DEBUG is True.

$ python manage.py load_tenant_config

Step 4: Running the server

Having completed the integration, the next step is ensure that the server starts properly without any errors and its behaviour is the same as before.

Start the server using

$ python manage.py runserver

Now make a HTTP GET call to the endpoint http://127.0.0.1:8000/hospital/ as follows:

$ python manage.py shell
>>> import requests
>>> r = requests.get(
...     'http://127.0.0.1:8000/hospital/', 
...     headers={"x-tenant-id": "tenant-1.test.com"}
... )
>>> print(r.json())

The expected response is as follows:

{
    "data": [{
        "id": 1,
        "name": "Test hospital",
        "address": "1st street"
    }],
    "tenant_id": "tenant-1.test.com"
}

As you can see, not much has changed from before except that a new header named
x-tenant-id with value as tenant-1.test.com is sent as part of the request.

Behind the scenes, the library parses this value from the header to route ORM queries made from within the view to the appropriate DB.

As a result, the app has suddenly become capable of routing DB queries to the appropriate databases by providing the tenant id as part of the HTTP request.

Info

In order to verify the above statement, make the same HTTP request as above with a different value for the x-tenant-id header. The server will return a 500: Internal server error with a traceback containing the TenantContextNotFound exception.

However, we see that there is currently only one DB configuration pertaining to
tenant-1.test.com. So the golden question is:

What's the procedure to add a new tenant and how to verify it ?

This and a lot more would be answered in the next part of the tutorial.