Citus 12.1 is out! Now with PG16 Support. Read all about it in Naisila’s 12.1 blog post. 💥
There are a number of data architectures you could use when building a multi-tenant app. Some, such as using one database per customer or one schema per customer, have trade-offs when it comes to larger scale. The other option is to build the notion of tenancy directly into the logic of your SaaS application. With
django-multitenant and Citus, built-in tenancy becomes much easier to put in place for your application without having to re-invent the wheel yourself.
Our django-multitenant Python library, enables easy scale out of applications that are built on top of Django and follow a multi tenant data model. This Python library has evolved from our experience working with SaaS customers, scaling out their multi-tenant apps.
You can get started at the application level setup with:
pip install -r requirements.txt,
Once you've done the installation. You'll want to import that package and begin updating your models to leverage the package:
class Product(TenantModel): store = models.ForeignKey(Store) tenant_id='store_id' name = models.CharField(max_length=255) description = models.TextField() class Meta(object): unique_together = ["id", "store"]
In the above there are three changes to your app which you'd make to each model that relates to a tenant:
TenantModelwhen creating your model
tenant_idon each model.
Do the above and you should be set to prep your multi-tenant application for scaling when you need it. But there's more.
What happens when you don't want to have to think about scoping the tenant? With
django-multitenant you can easily put in place your own logic that hooks into the Django authentication infrastructure and sets the appropriate tenant as a request comes in.
To do this we'll write some middleware which sets/unsets a tenant for each session/request. This way you don't have to worry about setting a tenant on a per view basis. Let’s set it at authentication and the library would ensure the rest (adding tenant_id filters to the queries). This way you do not need to worry about setting a tenant on a per view basis.
Here's a sample of what such would look like:
class SetCurrentTenantFromUser(object): def process_request(self, request): if not hasattr(self, 'authenticator'): from rest_framework_jwt.authentication import JSONWebTokenAuthentication self.authenticator = JSONWebTokenAuthentication() try: user, _ = self.authenticator.authenticate(request) except: # TODO: handle failure return try: # Assuming your app has a function to get the tenant associated for a user current_tenant = get_tenant_for_user(user) except: # TODO: handle failure return set_current_tenant(current_tenant) def process_response(self, request, response): set_current_tenant(None) return response MIDDLEWARE_CLASSES = ( 'our_app.utils.multitenancy.SetCurrentTenantFromUser', )
Middleware works great and would suffice to scope your application, another approach is to set the tenant using setcurrenttenant(t) api in all the views which you want to be scoped based on tenant. This allows you to scope django API calls automatically (without specifying explicit filters) to a single tenant. If the current_tenant is not set, then the default/native API without tenant scoping is used.
Let’s take a look at how that works:
def application_function: # current_tenant can be stored as a SESSION variable when a user logs in. # This should be done by the app t = current_tenant #set the tenant set_current_tenant(t); #Django ORM API calls; #Command 1; #Command 2;
Most of API calls are supported by this library, some of the examples are listed below
# Most of the APIs under Model.objects.* except select_related(). # Examples: s=Store.objects.all() set_current_tenant(s) # All the below API calls would add suitable tenant filters. # Simple get_queryset() Product.objects.get_queryset() # Joins Purchase.objects.filter(id=1).filter(store__name='The Awesome Store').filter(product__description='All products are awesome') # Updates Purchase.objects.filter(id=1).update(id=1) # Saves p=Product(8,1,'Awesome Shoe','These shoes are awesome') p.save() # Aggregates Product.objects.count() Product.objects.filter(store__name='The Awesome Store').count() # Subqueries Product.objects.filter(name='Awesome Shoe'); Purchase.objects.filter(product__in=p);
You can find the django-multitenant library from Citus on GitHub. If you need any help in getting your multi-tenant application prepared to scale out feel free to join us in our Slack channel and we’d be happy to help where needed.