Django-Multitenant,分佈式多租戶數據庫項目實戰(Python/Django+Postgres+Citus)

  • 2022 年 3 月 19 日
  • 筆記

Python/Django 支持分佈式多租戶數據庫,如 Postgres+Citus

通過將租戶上下文添加到您的查詢來實現輕鬆橫向擴展,使數據庫(例如 Citus)能夠有效地將查詢路由到正確的數據庫節點。

構建多租戶數據庫的架構包括:為每個租戶創建一個數據庫、為每個租戶創建一個 schema 和讓所有租戶共享同一個表。這個庫基於第三種設計,即讓所有租戶共享同一個表,它假設所有租戶相關的模型/表都有一個 tenant_id 列來表示租戶。

以下鏈接更多地討論了何時以及如何為您的多租戶數據庫選擇正確架構的權衡:

關於多租戶的其他有用鏈接:

  1. //www.citusdata.com/blog/2017/03/09/multi-tenant-sharding-tutorial/
  2. //www.citusdata.com/blog/2017/06/02/scaling-complex-sql-transactions/

項目源碼

//github.com/citusdata/django-multitenant

安裝

pip install --no-cache-dir django_multitenant

支持的 Django 版本/前提條件。

Python Django
3.X 2.2
3.X 3.2
3.X 4.0

用法

為了使用這個庫,您可以使用 Mixins 或讓您的模型從我們的自定義模型類繼承。

模型變化

  1. 在要使用庫的任何文件中導入它:
    from django_multitenant.fields import *
    from django_multitenant.models import *
    
  2. 所有模型都應繼承 TenantModel 類。Ex: class Product(TenantModel):
  3. 定義一個名為 tenant_id 的靜態變量,並使用該變量指定租戶列。Ex: tenant_id='store_id'
  4. TenantModel 子類的所有外鍵都應使用 TenantForeignKey 代替 models.ForeignKey
  5. 實現上述 2 個步驟的示例模型:
      class Store(TenantModel):
        tenant_id = 'id'
        name =  models.CharField(max_length=50)
        address = models.CharField(max_length=255)
        email = models.CharField(max_length=50)
    
      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"]
      class Purchase(TenantModel):
        store = models.ForeignKey(Store)
        tenant_id='store_id'
        product_purchased = TenantForeignKey(Product)
    

使用 mixins 更改模型

  1. 在您要使用庫的任何文件中,只需:
    from django_multitenant.mixins import *
    
  2. 所有模型都應使用 TenantModelMixin 和 django models.Model 或您的客戶模型類 Ex: class Product(TenantModelMixin, models.Model):
  3. 定義一個名為 tenant_id 的靜態變量,並使用該變量指定租戶列。Ex: tenant_id='store_id'
  4. TenantModel 子類的所有外鍵都應使用 TenantForeignKey 代替 models.ForeignKey
  5. 實現上述 2 個步驟的示例模型:
      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(object):
          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()
    

db 層自動化複合外鍵:

  1. 使用 TenantForeignKey 在租戶相關模型之間創建外鍵將自動將 tenant_id 添加到引用查詢(例如 product.purchases)和連接查詢(例如 product__name)。如果要確保在 db 層創建複合外鍵(帶有 tenant_id),則應將 settings.py 中的數據庫 ENGINE 更改為 django_multitenant.backends.postgresql
      'default': {
          'ENGINE': 'django_multitenant.backends.postgresql',
          ......
          ......
          ......
        }
    

在哪裡設置租戶?

  1. 使用中間件編寫身份驗證邏輯,該中間件還為每個 session/request 設置/取消設置租戶。 這樣,開發人員不必擔心基於每個視圖設置租戶。只需在身份驗證時設置它,庫將確保其餘部分(將 tenant_id 過濾器添加到查詢中)。上面的示例實現如下:

        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)
    

    在您的設置中,您需要更新 MIDDLEWARE 設置以包含您創建的設置。

       MIDDLEWARE = [
       # ...
       # existing items
       # ...
       'appname.middleware.MultitenantMiddleware'
    ]
    
  2. 在您希望基於租戶範圍的所有視圖中使用 set_current_tenant(t) api 設置租戶。 這將自動(不指定顯式過濾器)將所有 django API 調用範圍限定為單個租戶。如果未設置 current_tenant,則使用沒有租戶範圍的 默認/原生 API。

支持的 API

  1. Model.objects.* 下的大部分 API
  2. Model.save() 為租戶繼承的模型注入 tenant_id
 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);

更多