【问题标题】:Django running a migration on multi-tenant DBDjango 在多租户数据库上运行迁移
【发布时间】:2012-11-26 20:29:54
【问题描述】:
在具有一些自制(但接近可用的插件方法)多租户实现的 Django 应用程序上,我想使用 South 运行可以应用于所有模式的迁移(这次是一个简单的 add_column)。我的配置非常接近this one。
如果可能,我想跳过任何纯 SQL 查询。我可以从 ORM 中正确获取模式名称列表,但是我想知道我是否有可能以某种适当的方式从各种模式中访问表。
我有一个钩子可以在某种程度上通过参数更改 DB_HOST 和 DB_SCHEMA,但我认为在 South 的前向迁移方法中不能以这种方式干净地循环。
这个问题相当高级,但我主要想知道是否有人必须面对同样的问题,我很想知道是否有一些巧妙的方法来处理它!
问候,
马特
【问题讨论】:
标签:
django
django-south
multi-tenant
database-migration
【解决方案1】:
这是一个解决方案的大纲,发布在 South 邮件列表中。措辞的问题与列表中发布的问题略有不同:在那里,还提到了在单独的模式中在所有租户之间共享的“公共”表。 Rmatt 自己的回答将此称为 public 架构。
我的解决方案的基本思想:在架构中保存每个数据库(架构)的迁移历史。为此,我们需要使用一些数据库和 Django 技巧。
这意味着公共架构上的应用迁移历史记录保存在公共架构中,而租户应用迁移的历史记录保存在租户架构中——有效地对迁移历史表进行分片。 Django 并不真正支持这种分片;按实例内容设置写入很容易,但无法设置读取。
所以我建议为每个租户创建一个“租户助手”模式,其中包含一个名为 south_migrationhistory 的视图,它是来自公共模式和租户模式的 south_migrationhistory 表的联合。然后,为 South MigrationHistory 模型设置一个数据库路由器,指示它:
- syncdb 到公共和租户模式
- 始终从租户助手架构中读取
- 根据迁移所属的应用写入公共或租户架构
结果允许正确处理从租户应用迁移到公共应用迁移的依赖关系;这意味着向前迁移所需要做的就是循环migrate --all(或syncdb --migrate)命令——无需伪造向后迁移。公共架构的迁移将与循环中第一个租户的迁移一起运行,所有其他租户将“看到”它们。
作为事后的想法,可能也可以在没有辅助模式的情况下执行此操作 - 通过在租户模式中重命名 south_migrationhistory 表,并在模式中安装具有该名称的视图,该视图在以下情况下返回上述联合查询,并有一个“代替插入”触发器写入重命名的表。
【解决方案2】:
好吧,似乎没有多少人有经验或关心这个非常具体的问题。我在这里和那里尝试了一些东西,我还得到了南方邮件列表的一些支持,帮助我理解了一些要点。
基本上,我实现的解决方案如下:
我有一个通过 South 的 schemamigration 自动生成的非常正常的迁移文件。但是我把add_column和delete_column的表名改成了schema.table_name。通过导入多租户中间件来提供模式。
仅当架构未针对 public 架构运行时,才会应用迁移。它实际上并不意味着独立运行,或者仅与数据库和模式 kwargs 一起运行,而是来自一个新的 django 命令的迁移运行程序。
不幸的是,跑步者不得不在外部调用迁移,以便每次都再次通过中间件。另一个技巧是,我们必须获取之前的迁移状态,以便在每次租户迁移后伪造回到之前的南方状态。
这是我的 sn-p:
from subprocess import call
import os
from django.core.management.base import BaseCommand
from south.models import MigrationHistory
from myapp.models import MyModel
class Command(BaseCommand):
def handle(self, *args, **options):
#the only allowed arg is the prefix version and it should have a length of 4 (i.e. 0002)
applied = MigrationHistory.objects.filter(app_name='myapp').latest('applied')
current_version = applied.migration[:4]
call_args = ['python', os.path.join('bin', 'manage.py'), 'migrate', 'myorderbird.app.backups']
if len(args) == 1 and len(args[0]) == 4:
call_args.append(args[0])
obje_call_args = None
for obje in MyModel.objects.all():
if obje.schema_exists:
# fake the migration of the previous venue back to the current version
if obje_call_args:
obje_call_args = obje_call_args[:4] + [current_version, '--fake'] + obje_call_args[len(obje_call_args)-3:]
call(obje_call_args)
# migrate the venue in the loop
obje_call_args = list(call_args)
obje_call_args.extend(['--database={}'.format(obje.db), '--schema={}'.format(obje.schema)])
call(venue_call_args)