Shared database with shared schema¶
In this chapter, we will rebuild a slightly modified Django polls app to be multi-tenant. You can download the code from Github.
The base single-tenant app¶
Our base project has one app called polls
. The models look something like this.
from django.db import models
from django.contrib.auth.models import User
class Poll(models.Model):
question = models.CharField(max_length=100)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
pub_date = models.DateTimeField(auto_now=True)
def __str__(self):
return self.question
class Choice(models.Model):
poll = models.ForeignKey(Poll, related_name='choices',on_delete=models.CASCADE)
choice_text = models.CharField(max_length=100)
def __str__(self):
return self.choice_text
class Vote(models.Model):
choice = models.ForeignKey(Choice, related_name='votes', on_delete=models.CASCADE)
poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
voted_by = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
unique_together = ("poll", "voted_by")
There are a number of other files which we will look at later.
Adding multi tenancy to models¶
We will add another app called tenants
python manage.py startapp tenants
Create a model for storing Tenant
data.
class Tenant(models.Model):
name = models.CharField(max_length=100)
subdomain_prefix = models.CharField(max_length=100, unique=True)
And then create a class TenantAwareModel
class which other models will subclass from.
class TenantAwareModel(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
class Meta:
abstract = True
Change the polls.models
to subclass from TenantAwareModel
.
# ...
class Poll(TenantAwareModel):
# ...
class Choice(TenantAwareModel):
# ...
class Vote(TenantAwareModel):
# ...
Identifying tenants¶
There are many approaches to identify the tenant. One common method is to give each tenant their own subdomain. So if you main website is
www.example.com
And each of the following will be a separate tenant.
- thor.example.com
- loki.example.com
- potter.example.com
We will use the same method in the rest of the book. Our Tenant
model has subdomain_prefix
which will identify the tenant.
We will use polls.local
as the main domain and <xxx>.polls.local
as tenant subdomain.
Extracting tenant from request¶
Django views always have a request
which has the Host
header. This will contain the full subdomain the tenant is using.
We will add some utility methods to do this. Create a utils.py
and add this code.
from .models import Tenant
def hostname_from_request(request):
# split on `:` 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()
Now wherever you have a request
, you can use tenant_from_request
to get the tenant.
A detour to /etc/hosts¶
To ensure that the <xxx>.polls.local
hits your development machine, make sure you add a few entries to your /etc/hosts
(If you are on windows, use C:\Windows\System32\Drivers\etc\hosts
). My file looks like this.
# ...
127.0.0.1 polls.local
127.0.0.1 thor.polls.local
127.0.0.1 potter.polls.local
Also update ALLOWED_HOSTS
your settings.py. Mine looks like this: ALLOWED_HOSTS = ['polls.local', '.polls.local']
.
Using tenant_from_request
in the views¶
Views, whether they are Django function based, class based or a Django Rest Framework view have access to the request.
Lets take the example of polls.views.PollViewSet
to limit the endpoints to tenant specific Poll
objects.
from tenants.utils import tenant_from_request
class PollViewSet(viewsets.ModelViewSet):
queryset = Poll.objects.all()
serializer_class = PollSerializer
def get_queryset(self):
tenant = tenant_from_request(self.request)
return super().get_queryset().filter(tenant=tenant)
Isolating the admin¶
Like the views we need to enforce tenant isolation on the admin. We will need to override two methods.
get_queryset
: So that only the current tenant’s objects show up.save_model
: So that tenant gets set on the object when the object is saved.
With the changes, your admin.py
looks something like this.
@admin.register(Poll)
class PollAdmin(admin.ModelAdmin):
fields = ["question", "created_by", "pub_date"]
readonly_fields = ["pub_date"]
def get_queryset(self, request, *args, **kwargs):
queryset = super().get_queryset(request, *args, **kwargs)
tenant = tenant_from_request(request)
queryset = queryset.filter(tenant=tenant)
return queryset
def save_model(self, request, obj, form, change):
tenant = tenant_from_request(request)
obj.tenant = tenant
super().save_model(request, obj, form, change)
With these changes, you have a basic multi-tenant app. But there is a lot more to do as we will see in the following chapters.
The code for this chapter is available at https://github.com/agiliq/building-multi-tenant-applications-with-django/tree/master/shared-db