探索 Python/Django 支持分布式多租户数据库,如 Postgres+Citus

在 确定分布策略 中,我们讨论了在多租户用例中使用 Citus 所需的与框架无关的数据库更改。在这里,我们专门研究如何借助 django-multitenant 库将多租户 Django 应 用程序迁移到 Citus 存储后端。

  • Django
  • https://www.djangoproject.com/
  • 确定分布策略
  • http://citus.hackerlinner.com/develop/migration_mt_schema.html#mt-schema-migration
  • django-multitenant
  • https://github.com/citusdata/django-multitenant

此过程将分为 5 个步骤:

  • 将租户列介绍给我们想要分发的缺少它的模型
  • 更改分布式表的主键以包含租户列
  • 更新模型以使用 TenantModelMixin
  • 分发数据
  • 将 Django 应用程序更新为范围查询

准备横向扩展多租户应用程序

最初,您将从放置在单个数据库节点上的所有租户开始。为了能够扩展 django,必须对模型进行一些简单的更改。

让我们考虑这个简化的模型:

from django.utils import timezone
from django.db import 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')

这种模式的棘手之处在于,为了找到一个帐户的所有任务,您必须首先查询一个帐户的所有项目。一旦您开始分片数据,这就会成为一个问题,特别是当您对嵌套模型(如本例中的任务)运行 UPDATE 或 DELETE 查询时。

1. 将租户列引入属于帐户的模型

1.1 向属于某个帐户的模型引入该列

为了扩展多租户模型,查询必须快速定位属于一个帐户的所有记录。考虑一个 ORM 调用,例如:

Project.objects.filter(account_id=1).prefetch_related('tasks')

它生成这些底层 SQL 查询:

SELECT*
FROM myapp_project
WHERE account_id =1;

SELECT*
FROM myapp_task
WHERE project_id IN(1,2,3);

但是,使用额外的过滤器,第二个查询会更快:

-- the AND clause identifies the tenant
SELECT*
FROM myapp_task
WHERE project_id IN(1,2,3)
AND account_id =1;

这样您就可以轻松查询属于一个帐户的任务。实现这一点的最简单方法是在属于帐户的每个对象上简单地添加一个 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)

创建迁移以反映更改:python manage.py makemigrations。

1.2 在属于一个帐户的每个ManyToMany 模型上为 account_id 引入一个列

目标与之前相同。我们希望能够将 ORM 调用和查询路由到一个帐户。我们还希望能够在 account_id 上分发与帐户相关的多对多关系。

所以产生的调用:

Project.objects.filter(account_id=1).prefetch_related('managers')

可以在他们的 WHERE 子句中包含这样的 account_id:

SELECT*
FROM"myapp_project"WHERE"myapp_project"."account_id"=1;

SELECT*
FROM myapp_manager manager
INNER JOIN myapp_projectmanager projectmanager
ON(manager.id= projectmanager.manager_id
AND projectmanager.account_id= manager.account_id)
WHERE projectmanager.project_idIN(1,2,3)
AND manager.account_id=1;

为此,我们需要引入 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)

创建迁移以反映更改: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.db import migrations

class Migration(migrations.Migration):

dependencies =[
# leave this as it 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);
"""),
]

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')]

对于这些约束,您可以简单地在模型中更改约束:

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')]

然后使用以下命令生成迁移:

python manage.py makemigrations

一些 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)

在迁移中删除此约束:

from django.db import migrations

class Migration(migrations.Migration):

dependencies =[
# leave this as it was generated
]

operations =[
migrations.RunSQL("""
DROP CONSTRAINT myapp_projectman_project_id_manager_id_bc477b48_uniq;
"""),

然后改变你的模型有一个 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'))

最后通过创建新迁移来应用更改以生成这些约束:

python manage.py makemigrations

3. 更新模型以使用 TenantModelMixin 和 TenantForeignKey

接下来,我们将使用 django-multitenant 库将 account_id 添加到外键中,以便以后更轻松地查询应用程序。

在 Django 应用程序的 requirements.txt 中,添加:

django_multitenant>=2.0.0,<3

运行 pip install -r requirements.txt。

在 settings.py 中,将数据库引擎改为 django-multitenant 提供的自定义引擎:

'ENGINE':'django_multitenant.backends.postgresql'

3.1 介绍 TenantModelMixin 和 TenantManager

模型现在不仅继承自 models.Model,还继承自 TenantModelMixin。

要在你的 models.py 文件中做到这一点,你需要执行以下导入:

from django_multitenant.mixins import *

以前我们的示例模型仅继承自 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()

3.2 处理外键约束

对于 ForeignKey 和 OneToOneField 约束,我们有几种不同的情况:

  • 分布式表之间的外键(或一对一),您应该使用 TenantForeignKey (或 TenantOneToOneField)。
  • 分布式表和引用表之间的外键不需要更改。
  • 分布式表和本地表之间的外键,需要使用 models.ForeignKey(MyModel, on_delete=models.CASCADE, db_constraint=False) 来删除约束。

最后你的模型应该是这样的:

from django.db import models
from django_multitenant.fields import TenantForeignKey
from django_multitenant.mixins import *

class Country(models.Model): # This tableis 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()

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()

安装库、更改引擎和更新模型后,运行 python manage.py makemigrations。这将产生一个迁移,以便在必要时合成外键。

4. 在 Citus 中分发数据

我们需要最后一次迁移来告诉 Citus 标记要分发的表。创建一个新的迁移 python manage.py makemigrations appname –empty –name Distribute_tables。编辑结果如下所示:

from django.db import migrations
from django_multitenant.db import migrations as tenant_migrations

class Migration(migrations.Migration):
dependencies =[
# leave this as it 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'),
]

从到目前为止的步骤中创建的所有迁移,使用 python manage.py migrate 将它们应用到数据库。

此时,Django 应用程序模型已准备好与 Citus 后端一起工作。您可以继续将数据导入新系统并根据需要修改视图以处理模型更改。

将 Django 应用程序更新为范围查询

django-multitenant 库不仅对迁移有用,而且对简化应用程序查询也很有用。该库允许应用程序代码轻松地将查询范围限定为单个租户。它会自动将正确的 SQL 过滤器添加到所有语句中,包括通过关系获取对象。

例如,在一个视图中只需 set_current_tenant,之后的所有查询或连接都将包含一个过滤器,以将结果范围限定为单个租户。

# set the current tenant to the first account
s = Account.objects.first()
set_current_tenant(s)

# now this count query applies only to Project for that account
Project.objects.count()

# Find tasks for very important projects in the current account
Task.objects.filter(project__name='Very important project')

在应用程序视图的上下文中,当前租户对象可以在用户登录时存储为 SESSION 变量, 并且视图操作可以 set_current_tenant 到该值。有关更多示例,请参阅 django-multitenant 中的 README。

set_current_tenant 函数也可以接受一个对象数组,比如:

set_current_tenant([s1, s2, s3])

它使用类似于 tenant_id IN (a,b,c) 的过滤器更新内部 SQL 查询。

使用中间件自动化

而不是在每个视图中调用 set_current_tenant(), 您可以在 Django 应用程序中创建并安装一个新的 middleware 类来自动完成。

# src/appname/middleware.py

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.userandnot request.user.is_anonymous:
set_current_tenant(request.user.employee.company)
response = self.get_response(request)
return response

通过更新 src/appname/settings/base.py 中的 MIDDLEWARE 数组来启用中间件:

MIDDLEWARE =[
# ...
# existing items
# ...

'appname.middleware.MultitenantMiddleware'
]

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/269139.html<

(0)
运维的头像运维
上一篇2025-05-07 05:18
下一篇 2025-05-07 05:20

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注