Package: django-multitenant
django-multitenantPython/Django support for distributed multi-tenant databases like Postgres+CitusEnables easy scale-out by adding the tenant context to your queries, enabling the database (e.g. Citus) to efficiently route queries to the right database node.There are architecures for building multi-tenant databases viz.Create one database per tenant,Create one schema per tenantandHave all tenants share the same table(s). This library is based on the 3rd design i.eHave all tenants share the same table(s), it assumes that all the tenant relates models/tables have a tenant_id column for representing a tenant.The following link talks more about the trade-offs on when and how to choose the right architecture for your multi-tenant database:https://www.citusdata.com/blog/2016/10/03/designing-your-saas-database-for-high-scalability/The following blogpost is a good starting point to start to use django-multitenanthttps://www.citusdata.com/blog/2023/05/09/evolving-django-multitenant-to-build-scalable-saas-apps-on-postgres-and-citus/Other useful links on multi-tenancy:https://www.citusdata.com/blog/2017/03/09/multi-tenant-sharding-tutorial/https://www.citusdata.com/blog/2017/06/02/scaling-complex-sql-transactions/https://www.youtube.com/watch?v=RKSwjaZKXL0Installation:pip install  --no-cache-dir django_multitenantSupported Django versions/Pre-requisites.PythonDjangoCitus3.8 3.9 3.10 3.114.211  123.8 3.9 3.10 3.114.111  123.8 3.9 3.10 3.114.010  11  123.73.210  11  12Usage:In order to use this library you can either use Mixins or have your models inherit from our custom model class.Changes in Models:In whichever files you want to use the library import it:fromdjango_multitenant.fieldsimport*fromdjango_multitenant.modelsimport*All models should inherit the TenantModel class.Ex: class Product(TenantModel):Define a static variable named tenant_id and specify the tenant column using this variable.You can define tenant_id in three ways. Any of them is acceptableUsing TenantMeta.tenant_field_name variableUsing TenantMeta.tenant_id variableUsing tenant_id fieldWarningUsing  tenant_id field directly in the class is not suggested since it may cause collision if class has a field named with 'tenant'All foreign keys to TenantModel subclasses should use TenantForeignKey in place of
models.ForeignKeyA sample model implementing the above 2 steps:classStore(TenantModel):name=models.CharField(max_length=50)address=models.CharField(max_length=255)email=models.CharField(max_length=50)classTenantMeta:tenant_field_name="id"classProduct(TenantModel):store=models.ForeignKey(Store)name=models.CharField(max_length=255)description=models.TextField()classMeta:unique_together=["id","store"]classTenantMeta:tenant_field_name="store_id"classPurchase(TenantModel):store=models.ForeignKey(Store)product_purchased=TenantForeignKey(Product)classTenantMeta:tenant_field_name="store_id"Changes in Models using mixins:In whichever files you want to use the library import it by just sayingfromdjango_multitenant.mixinsimport*All models should use theTenantModelMixinand the djangomodels.Modelor your customer Model classEx: class Product(TenantModelMixin, models.Model):Define a static variable named tenant_id and specify the tenant column using this variable.Ex: tenant_id='store_id'All foreign keys to TenantModel subclasses should use TenantForeignKey in place of
models.ForeignKeyReferenced table in TenantForeignKey should include a unique key including tenant_id and primary keyEx:       
class Meta:
     unique_together = ["id", "store"]A sample model implementing the above 3 steps:classProductManager(TenantManagerMixin,models.Manager):passclassProduct(TenantModelMixin,models.Model):store=models.ForeignKey(Store)tenant_id='store_id'name=models.CharField(max_length=255)description=models.TextField()objects=ProductManager()classMeta:unique_together=["id","store"]classPurchaseManager(TenantManagerMixin,models.Manager):passclassPurchase(TenantModelMixin,models.Model):store=models.ForeignKey(Store)tenant_id='store_id'product_purchased=TenantForeignKey(Product)objects=PurchaseManager()Automating composite foreign keys at db layer:Creating foreign keys between tenant related models using TenantForeignKey would automate adding tenant_id to reference queries (ex. product.purchases) and join queries (ex. product__name). If you want to ensure to create composite foreign keys (with tenant_id) at the db layer, you should change the database ENGINE in the settings.py todjango_multitenant.backends.postgresql.'default':{'ENGINE':'django_multitenant.backends.postgresql',..................}Where to Set the Tenant?Write authentication logic using a middleware which also sets/unsets a tenant for each session/request. This way developers need not worry about setting a tenant on a per view basis. Just set it while authentication and the library would ensure the rest (adding tenant_id filters to the queries). A sample implementation of the above is as follows:fromdjango_multitenant.utilsimportset_current_tenantclassMultitenantMiddleware:def__init__(self,get_response):self.get_response=get_responsedef__call__(self,request):ifrequest.userandnotrequest.user.is_anonymous:set_current_tenant(request.user.employee.company)returnself.get_response(request)In your settings, you will need to update theMIDDLEWAREsetting to include the one you created.MIDDLEWARE=[# ...# existing items# ...'appname.middleware.MultitenantMiddleware']Set the tenant using set_current_tenant(t) api in all the views which you want to be scoped based on tenant. This would scope all the 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.defapplication_function:# current_tenant can be stored as a SESSION variable when a user logs in.# This should be done by the appt=current_tenant#set the tenantset_current_tenant(t);#Django ORM API calls;#Command 1;#Command 2;#Command 3;#Command 4;#Command 5;Supported APIs:Most of the APIs under Model.objects.*.Model.save() injects tenant_id for tenant inherited models.s=Store.objects.all()[0]set_current_tenant(s)#All the below API calls would add suitable tenant filters.#Simple get_queryset()Product.objects.get_queryset()#Simple joinPurchase.objects.filter(id=1).filter(store__name='The Awesome Store').filter(product__description='All products are awesome')#UpdatePurchase.objects.filter(id=1).update(id=1)#Savep=Product(8,1,'Awesome Shoe','These shoes are awesome')p.save()#Simple aggregatesProduct.objects.count()Product.objects.filter(store__name='The Awesome Store').count()#SubqueriesProduct.objects.filter(name='Awesome Shoe');Purchase.objects.filter(product__in=p);CreditsThis library uses similar logic of setting/getting tenant object as indjango-simple-multitenant. We thank the authors for their efforts.LicenseCopyright (C) 2023, Citus Data
Licensed under the MIT license, see LICENSE file for details.