在 确定分布策略 中,探索我们讨论了在多租户用例中使用 Citus 所需的支持租户与框架无关的数据库更改。在这里,分布我们专门研究如何借助 django-multitenant 库将多租户 Django 应 用程序迁移到 Citus 存储后端。式多数据Djangohttps://www.djangoproject.com/确定分布策略http://citus.hackerlinner.com/develop/migration_mt_schema.html#mt-schema-migrationdjango-multitenanthttps://github.com/citusdata/django-multitenant
此过程将分为 5 个步骤:

准备横向扩展多租户应用程序
最初,您将从放置在单个数据库节点上的支持租户所有租户开始。为了能够扩展 django,分布必须对模型进行一些简单的式多数据更改。
让我们考虑这个简化的探索模型:
复制from django.utilsimport timezone
from django.dbimport models
class Country(models.Model): name = models.CharField(max_length=255)class Account(models.Model): name = models.CharField(max_length=255) domain = models.CharField(max_length=255) subdomain = models.CharField(max_length=255) country = models.ForeignKey(Country, on_delete=models.SET_NULL)class Manager(models.Model): name = models.CharField(max_length=255) account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name=managers)class Project(models.Model): name = models.CharField(max_length=255) account = models.ForeignKey(Account, related_name=projects, on_delete=models.CASCADE) managers = models.ManyToManyField(Manager)class Task(models.Model): name = models.CharField(max_length=255) project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name=tasks)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.这种模式的棘手之处在于,为了找到一个帐户的支持租户所有任务,您必须首先查询一个帐户的分布所有项目。一旦您开始分片数据,式多数据这就会成为一个问题,探索特别是支持租户当您对嵌套模型(如本例中的任务)运行 UPDATE 或 DELETE 查询时。
1. 将租户列引入属于帐户的分布模型
1.1 向属于某个帐户的模型引入该列为了扩展多租户模型,查询必须快速定位属于一个帐户的所有记录。考虑一个 ORM 调用,例如:
复制Project.objects.filter(account_id=1).prefetch_related(tasks)1.它生成这些底层 SQL 查询:
复制SELECT *FROMmyapp_project
WHERE account_id = 1;SELECT *FROMmyapp_task
WHERE project_id IN (1, 2, 3);1.2.3.4.5.6.7.但是云服务器提供商,使用额外的过滤器,第二个查询会更快:
复制-- the AND clause identifies the tenantSELECT *FROMmyapp_task
WHERE project_id IN (1, 2, 3) AND account_id = 1;1.2.3.4.5.这样您就可以轻松查询属于一个帐户的任务。实现这一点的最简单方法是在属于帐户的每个对象上简单地添加一个 account_id 列。
在我们的例子中:
复制class Task(models.Model): name = models.CharField(max_length=255) project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name=tasks) account = models.ForeignKey(Account, related_name=tasks, on_delete=models.CASCADE)1.2.3.4.5.6.创建迁移以反映更改:python manage.py makemigrations。
1.2 在属于一个帐户的每个ManyToMany 模型上为 account_id 引入一个列目标与之前相同。我们希望能够将 ORM 调用和查询路由到一个帐户。我们还希望能够在 account_id 上分发与帐户相关的多对多关系。
所以产生的调用:
复制Project.objects.filter(account_id=1).prefetch_related(managers)1.可以在他们的 WHERE 子句中包含这样的 account_id:
复制SELECT *FROM "myapp_project" WHERE "myapp_project"."account_id" = 1;SELECT *FROMmyapp_manager manager
INNER JOINmyapp_projectmanager projectmanager
ON (manager.id = projectmanager.manager_idAND projectmanager.account_id = manager.account_id)WHERE projectmanager.project_id IN (1, 2, 3)AND manager.account_id = 1;1.2.3.4.5.6.7.8.9.10.为此,我们需要引入 through 模型。在我们的例子中:
复制class Project(models.Model): name = models.CharField(max_length=255) account = models.ForeignKey(Account, related_name=projects, on_delete=models.CASCADE) managers = models.ManyToManyField(Manager, through=ProjectManager)class ProjectManager(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) manager = models.ForeignKey(Manager, on_delete=models.CASCADE) account = models.ForeignKey(Account, on_delete=models.CASCADE)1.2.3.4.5.6.7.8.9.10.创建迁移以反映更改:python manage.py makemigrations。
2. 在所有主键和唯一约束中包含 account_id
2.1 将 account_id 包含到主键中Django 会自动在模型上创建一个简单的 “id” 主键,因此我们需要通过自己的自定义迁移来规避这种行为。运行 python manage.py makemigrations appname --empty --name remove_simple_pk, 并将结果编辑为如下所示:
复制from django.dbimport migrations
class Migration(migrations.Migration): dependencies = [ # leave this asit was generated
] operations = [ # Django considers "id" the primary key of these tables,but
# we want the primary key to be (account_id, id) migrations.RunSQL(""" ALTER TABLE myapp_manager DROP CONSTRAINT myapp_manager_pkey CASCADE; ALTER TABLE myapp_manager ADD CONSTRAINT myapp_manager_pkey PRIMARY KEY (account_id, id); """), migrations.RunSQL(""" ALTER TABLE myapp_project DROP CONSTRAINT myapp_project_pkey CASCADE; ALTER TABLE myapp_project ADD CONSTRAINT myapp_product_pkey PRIMARY KEY (account_id, id); """), migrations.RunSQL(""" ALTER TABLE myapp_task DROP CONSTRAINT myapp_task_pkey CASCADE; ALTER TABLE myapp_task ADD CONSTRAINT myapp_task_pkey PRIMARY KEY (account_id, id); """), migrations.RunSQL(""" ALTER TABLE myapp_projectmanager DROP CONSTRAINT myapp_projectmanager_pkey CASCADE; ALTER TABLE myapp_projectmanager ADD CONSTRAINT myapp_projectmanager_pkey PRIMARY KEY (account_id, id); """), ]1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46. 2.2 将 account_id 包含到唯一约束中对 UNIQUE 约束也需要做同样的事情。您可以使用 unique=True 或 unique_together 在模型中设置显式约束,例如:
复制class Project(models.Model): name = models.CharField(max_length=255, unique=True) account = models.ForeignKey(Account, related_name=projects, on_delete=models.CASCADE) managers = models.ManyToManyField(Manager, through=ProjectManager)class Task(models.Model): name = models.CharField(max_length=255) project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name=tasks) account = models.ForeignKey(Account, related_name=tasks, on_delete=models.CASCADE) class Meta: unique_together = [(name, project)]1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.对于这些约束,您可以简单地在模型中更改约束:
复制class Project(models.Model): name = models.CharField(max_length=255) account = models.ForeignKey(Account, related_name=projects, on_delete=models.CASCADE) managers = models.ManyToManyField(Manager, through=ProjectManager) class Meta: unique_together = [(account, name)]class Task(models.Model): name = models.CharField(max_length=255) project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name=tasks) account = models.ForeignKey(Account, related_name=tasks, on_delete=models.CASCADE) class Meta: unique_together = [(account, name, project)]1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.然后使用以下命令生成迁移:
复制python manage.py makemigrations1.一些 UNIQUE 约束是由 ORM 创建的高防服务器,您需要显式删除它们。 OneToOneField 和 ManyToMany 字段就是这种情况。
对于这些情况,您需要:1. 找到约束 2. 进行迁移以删除它们 3. 重新创建约束,包括 account_id 字段。
要查找约束,请使用 psql 连接到您的数据库并运行 \d+ myapp_projectmanager 你将看到 ManyToMany (或 OneToOneField )约束:
复制"myapp_projectmanager" UNIQUE CONSTRAINT myapp_projectman_project_id_manager_id_bc477b48_uniq,btree (project_id, manager_id)1.2.在迁移中删除此约束:
复制from django.dbimport migrations
class Migration(migrations.Migration): dependencies = [ # leave this asit was generated
] operations = [ migrations.RunSQL(""" DROP CONSTRAINT myapp_projectman_project_id_manager_id_bc477b48_uniq; """),1.2.3.4.5.6.7.8.9.10.11.12.然后改变你的模型有一个 unique_together 包括 account_id:
复制class ProjectManager(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) manager = models.ForeignKey(Manager, on_delete=models.CASCADE) account = models.ForeignKey(Account, on_delete=models.CASCADE) class Meta: unique_together=((account, project, manager))1.2.3.4.5.6.7.最后通过创建新迁移来应用更改以生成这些约束:
复制python manage.py makemigrations1.3. 更新模型以使用 TenantModelMixin 和 TenantForeignKey
接下来,我们将使用 django-multitenant 库将 account_id 添加到外键中,以便以后更轻松地查询应用程序。
在 Django 应用程序的 requirements.txt 中,添加:
复制django_multitenant>=2.0.0, <31.运行 pip install -r requirements.txt。
在 settings.py 中,将数据库引擎改为 django-multitenant 提供的自定义引擎:
复制ENGINE: django_multitenant.backends.postgresql1.3.1 介绍 TenantModelMixin 和 TenantManager
模型现在不仅继承自 models.Model,还继承自 TenantModelMixin。
要在你的 models.py 文件中做到这一点,你需要执行以下导入:
复制from django_multitenant.mixins import *1.以前我们的示例模型仅继承自 models.Model,但现在我们需要将它们更改为也继承自 TenantModelMixin。实际项目中的模型也可能继承自其他 mixin,例如 django.contrib.gis.db,这很好。
此时,您还将引入 tenant_id 来定义哪一列是分布列。
复制class TenantManager(TenantManagerMixin, models.Manager):pass
class Account(TenantModelMixin, models.Model):...
tenant_id = id objects = TenantManager()class Manager(TenantModelMixin, models.Model):...
tenant_id = account_id objects = TenantManager()class Project(TenantModelMixin, models.Model):...
tenant_id = account_id objects = TenantManager()class Task(TenantModelMixin, models.Model):...
tenant_id = account_id objects = TenantManager()class ProjectManager(TenantModelMixin, models.Model):...
tenant_id = account_id objects = TenantManager()1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.3.2 处理外键约束
对于 ForeignKey 和 OneToOneField 约束,云服务器我们有几种不同的情况:
分布式表之间的外键(或一对一),您应该使用 TenantForeignKey (或 TenantOneToOneField)。分布式表和引用表之间的外键不需要更改。分布式表和本地表之间的外键,需要使用 models.ForeignKey(MyModel, on_delete=models.CASCADE, db_constraint=False) 来删除约束。最后你的模型应该是这样的:
复制from django.dbimport models
from django_multitenant.fieldsimport TenantForeignKey
from django_multitenant.mixins import *class Country(models.Model): # This table is a reference table name = models.CharField(max_length=255)class TenantManager(TenantManagerMixin, models.Manager):pass
class Account(TenantModelMixin, models.Model): name = models.CharField(max_length=255) domain = models.CharField(max_length=255) subdomain = models.CharField(max_length=255) country = models.ForeignKey(Country, on_delete=models.SET_NULL)# No changes needed
tenant_id = id objects = TenantManager()class Manager(TenantModelMixin, models.Model): name = models.CharField(max_length=255) account = models.ForeignKey(Account, related_name=managers, on_delete=models.CASCADE) tenant_id = account_id objects = TenantManager()class Project(TenantModelMixin, models.Model): account = models.ForeignKey(Account, related_name=projects, on_delete=models.CASCADE) managers = models.ManyToManyField(Manager, through=ProjectManager) tenant_id = account_id objects = TenantManager()class Task(TenantModelMixin, models.Model): name = models.CharField(max_length=255) project = TenantForeignKey(Project, on_delete=models.CASCADE, related_name=tasks) account = models.ForeignKey(Account, on_delete=models.CASCADE) tenant_id = account_id objects = TenantManager()class ProjectManager(TenantModelMixin, models.Model): project = TenantForeignKey(Project, on_delete=models.CASCADE) manager = TenantForeignKey(Manager, on_delete=models.CASCADE) account = models.ForeignKey(Account, on_delete=models.CASCADE) tenant_id = account_id objects = TenantManager()1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.3.3 处理多对多约束
在本文的第二部分,我们介绍了在 citus 中, ManyToMany 关系需要一个带有租户列的 through 模型。这就是为什么我们有这个模型:
复制class ProjectManager(TenantModelMixin, models.Model): project = TenantForeignKey(Project, on_delete=models.CASCADE) manager = TenantForeignKey(Manager, on_delete=models.CASCADE) account = models.ForeignKey(Account, on_delete=models.CASCADE) tenant_id = account_id objects = TenantManager()1.2.3.4.5.6.7.安装库、更改引擎和更新模型后,运行 python manage.py makemigrations。这将产生一个迁移,以便在必要时合成外键。
4. 在 Citus 中分发数据
我们需要最后一次迁移来告诉 Citus 标记要分发的表。创建一个新的迁移 python manage.py makemigrations appname --empty --name Distribute_tables。编辑结果如下所示:
复制from django.dbimport migrations
from django_multitenant.db import migrations astenant_migrations
class Migration(migrations.Migration): dependencies = [ # leave this asit was generated
] operations = [ tenant_migrations.Distribute(Country, reference=True), tenant_migrations.Distribute(Account), tenant_migrations.Distribute(Manager), tenant_migrations.Distribute(Project), tenant_migrations.Distribute(ProjectManager), tenant_migrations.Distribute(Task), ]1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.从到目前为止的步骤中创建的所有迁移,使用 python manage.py migrate 将它们应用到数据库。
此时,Django 应用程序模型已准备好与 Citus 后端一起工作。您可以继续将数据导入新系统并根据需要修改视图以处理模型更改。
将 Django 应用程序更新为范围查询
django-multitenant 库不仅对迁移有用,而且对简化应用程序查询也很有用。该库允许应用程序代码轻松地将查询范围限定为单个租户。它会自动将正确的 SQL 过滤器添加到所有语句中,包括通过关系获取对象。
例如,在一个视图中只需 set_current_tenant,之后的所有查询或连接都将包含一个过滤器,以将结果范围限定为单个租户。
复制# setthe current tenant to the first account
s = Account.objects.first()set_current_tenant(s)# now this countquery applies only to Project for that account
Project.objects.count()# Find tasks for very important projects inthe current account
Task.objects.filter(project__name=Very important project)1.2.3.4.5.6.7.8.9.在应用程序视图的上下文中,当前租户对象可以在用户登录时存储为 SESSION 变量, 并且视图操作可以 set_current_tenant 到该值。有关更多示例,请参阅 django-multitenant 中的 README。
set_current_tenant 函数也可以接受一个对象数组,比如:
复制set_current_tenant([s1, s2, s3])1.它使用类似于 tenant_id IN (a,b,c) 的过滤器更新内部 SQL 查询。
使用中间件自动化
而不是在每个视图中调用 set_current_tenant(), 您可以在 Django 应用程序中创建并安装一个新的 middleware 类来自动完成。
复制# src/appname/middleware.pyfrom django_multitenant.utilsimport 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) response = self.get_response(request) return response1.2.3.4.5.6.7.8.9.10.11.12.13.通过更新 src/appname/settings/base.py 中的 MIDDLEWARE 数组来启用中间件:
复制MIDDLEWARE = [# ...
# existing items
# ...
appname.middleware.MultitenantMiddleware]1.2.3.4.5.6.7.

