• Stars
    star
    722
  • Rank 62,738 (Top 2 %)
  • Language
    Python
  • License
    MIT License
  • Created over 7 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Python/Django support for distributed multi-tenant databases like Postgres+Citus

django-multitenant
Build Status Latest Documentation Status Coverage Status PyPI Version

Python/Django support for distributed multi-tenant databases like Postgres+Citus

Enables 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 tenant and Have all tenants share the same table(s). This library is based on the 3rd design i.e Have 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-tenat 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-multitenant https://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:

  1. https://www.citusdata.com/blog/2017/03/09/multi-tenant-sharding-tutorial/
  2. https://www.citusdata.com/blog/2017/06/02/scaling-complex-sql-transactions/
  3. https://www.youtube.com/watch?v=RKSwjaZKXL0

Installation:

  1. pip install --no-cache-dir django_multitenant

Supported Django versions/Pre-requisites.

Python Django Citus
3.8 3.9 3.10 3.11 4.1 11
3.8 3.9 3.10 3.11 4.0 10 11
3.7 3.2 10 11

Usage:

In order to use this library you can either use Mixins or have your models inherit from our custom model class.

Changes in Models:

  1. In whichever files you want to use the library import it:

    from django_multitenant.fields import *
    from django_multitenant.models import *
  2. All models should inherit the TenantModel class. Ex: class Product(TenantModel):

  3. 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 acceptable

    • Using TenantMeta.tenant_field_name variable
    • Using TenantMeta.tenant_id variable
    • Using tenant_id field

    Warning Using tenant_id field directly in the class is not suggested since it may cause collision if class has a field named with 'tenant'


  4. All foreign keys to TenantModel subclasses should use TenantForeignKey in place of models.ForeignKey

  5. A sample model implementing the above 2 steps:

    class Store(TenantModel):
      name =  models.CharField(max_length=50)
      address = models.CharField(max_length=255)
      email = models.CharField(max_length=50)
      class TenantMeta:
        tenant_field_name = "id"
    
    class Product(TenantModel):
      store = models.ForeignKey(Store)
      name = models.CharField(max_length=255)
      description = models.TextField()
      class Meta:
        unique_together = ["id", "store"]
      class TenantMeta:
        tenant_field_name = "store_id"
    class Purchase(TenantModel):
      store = models.ForeignKey(Store)
      product_purchased = TenantForeignKey(Product)
      class TenantMeta:
        tenant_field_name = "store_id"

Changes in Models using mixins:

  1. In whichever files you want to use the library import it by just saying
    from django_multitenant.mixins import *
  2. All models should use the TenantModelMixin and the django models.Model or your customer Model class Ex: class Product(TenantModelMixin, models.Model):
  3. Define a static variable named tenant_id and specify the tenant column using this variable. Ex: tenant_id='store_id'
  4. All foreign keys to TenantModel subclasses should use TenantForeignKey in place of models.ForeignKey
  5. Referenced table in TenenatForeignKey should include a unique key including tenant_id and primary key
    Ex:       
    class Meta:
         unique_together = ["id", "store"]
    
  6. A sample model implementing the above 3 steps:
    class ProductManager(TenantManagerMixin, models.Manager):
      pass
    
    class Product(TenantModelMixin, models.Model):
      store = models.ForeignKey(Store)
      tenant_id='store_id'
      name = models.CharField(max_length=255)
      description = models.TextField()
    
      objects = ProductManager()
    
      class Meta:
        unique_together = ["id", "store"]
    
    class PurchaseManager(TenantManagerMixin, models.Manager):
      pass
    
    class Purchase(TenantModelMixin, models.Model):
      store = models.ForeignKey(Store)
      tenant_id='store_id'
      product_purchased = TenantForeignKey(Product)
    
      objects = PurchaseManager()

Automating composite foreign keys at db layer:

  1. 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 to django_multitenant.backends.postgresql.
  'default': {
      'ENGINE': 'django_multitenant.backends.postgresql',
      ......
      ......
      ......
}

Where to Set the Tenant?

  1. 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:

        from django_multitenant.utils import set_current_tenant
        
        class MultitenantMiddleware:
            def __init__(self, get_response):
                self.get_response = get_response
    
            def __call__(self, request):
                if request.user and not request.user.is_anonymous:
                    set_current_tenant(request.user.employee.company)
                return self.get_response(request)

    In your settings, you will need to update the MIDDLEWARE setting to include the one you created.

       MIDDLEWARE = [
           # ...
           # existing items
           # ...
           'appname.middleware.MultitenantMiddleware'
       ]
  2. 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.

     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;
       #Command 3;
       #Command 4;
       #Command 5;

Supported APIs:

  1. Most of the APIs under Model.objects.*.
  2. 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 join
    Purchase.objects.filter(id=1).filter(store__name='The Awesome Store').filter(product__description='All products are awesome')
    
    #Update
    Purchase.objects.filter(id=1).update(id=1)
    
    #Save
    p=Product(8,1,'Awesome Shoe','These shoes are awesome')
    p.save()
    
    #Simple 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);

Credits

This library uses similar logic of setting/getting tenant object as in django-simple-multitenant. We thank the authors for their efforts.

License

Copyright (C) 2023, Citus Data Licensed under the MIT license, see LICENSE file for details.

More Repositories

1

citus

Distributed PostgreSQL as an extension
C
10,512
star
2

pg_cron

Run periodic jobs in PostgreSQL
C
2,846
star
3

cstore_fdw

Columnar storage extension for Postgres built as a foreign data wrapper. Check out https://github.com/citusdata/citus for a modernized columnar storage implementation built as a table access method.
C
1,758
star
4

postgresql-hll

PostgreSQL extension adding HyperLogLog data structures as a native data type
C
1,116
star
5

pg_shard

ATTENTION: pg_shard is superseded by Citus, its more powerful replacement
C
1,058
star
6

pg_auto_failover

Postgres extension and service for automated failover and high-availability
C
852
star
7

activerecord-multi-tenant

Rails/ActiveRecord support for distributed multi-tenant databases like Postgres+Citus
Ruby
717
star
8

postgres_vectorization_test

Vectorized executor to speed up PostgreSQL
C
331
star
9

docker

🚢 Docker images and configuration for Citus
Dockerfile
241
star
10

postgresql-topn

TopN is an open source PostgreSQL extension that returns the top values in a database according to some criteria
C
238
star
11

mongo_fdw

DEPRECATED, moved to
C
154
star
12

podyn

DynamoDB to PostgreSQL & Citus continuous replication tool
Java
85
star
13

citus-example-ad-analytics

Reference App for Ad Analytics, using Ruby on Rails.
CSS
75
star
14

citus_docs

Documentation for Citus. Distributed PostgreSQL as an extension.
CSS
58
star
15

pgconfsv-tutorial

Files for the PGConf SV tutorial on real-time analytics
Python
54
star
16

citus-benchmark

Tools for running benchmarks against Citus
Python
38
star
17

postgres-analytics-tutorial

Exercises for the Architecting Real-Time Analytics for your Customers tutorial
PLpgSQL
28
star
18

membership-manager

🚢 Docker image for managing Citus membership via docker-py
Python
21
star
19

packaging

Packaging scripts for Citus
Dockerfile
19
star
20

test-automation

Tools for making our tests easier to run
C
14
star
21

tools

Tools and config used in Citus Data projects
Python
14
star
22

pgconfus-tutorial-multi-tenant

Files for the tutorial on Citus & Multi-Tenant Models @ PGConf US 2017
Ruby
10
star
23

pg_octopus

A health checker for PostgreSQL
C
9
star
24

python-citus-rebalancer

A python command line tool to recommend shard moves to customers with unbalanced clusters
Python
9
star
25

pgmasq

Transparently forward transactions from a hot standby to a primary in PostgreSQL (failed experiment)
C
8
star
26

citus-django-example-ad-analytics

Reference App for Ad Analytics, using Django
CSS
8
star
27

pg_intpair

C
7
star
28

the-process

Trust it.
Dockerfile
6
star
29

interactive-tutorials

Shell
5
star
30

realtime-dashboards-resources

Some useful scripts
PLpgSQL
5
star
31

pg_shard-old

Repo used for pg_shard development before release.
C
4
star
32

PathToCitusCon

This repo stores show notes for our PathToCitusCon series of events on Discord
3
star
33

citus-example-microservices

Python
3
star
34

workerlist-gen

🚢 Docker image for generating Citus workerlist file using docker-gen
Shell
3
star
35

real-time-analytics-Hands-On-Lab-Hyperscale-Citus

2
star
36

Nationwide-Hands-On-Session

1
star