多租户的租户是动态增加的,django初始化的时候会加载settings里面的DATABASES变量,用来初始数据连接池,但是在项目运营过程中,租户都是动态增加或者删除的,总不能每次发生租户的增加或者删除我们修改DATABASES变量,然后重启整个项目吧,因此数据库连接池都需要支持动态增加或者删除
django认证完成之后,request.user是一个django.contrib.auth.models.User对象而不是我们的GlobalUser对象,因此我们必须替换request.user对象
很多人选择django作为Python框架的原因是因为django一些内置的App十分好用,因此如果保证租户业务逻辑中能完整的使用django一些内置App,例如Auth模块(User、Group和Permission),Admin模块、migration模块、contenttypes模块等
DjangoContentType作为django内置的通用外键模型,在很多地方被广泛使用,该模型自带缓存,可以在一定程度上提升ContentType的使用效率,这特性通常没有任何问题,但是在多租户场景下,因为项目的迭代开发,不同的租户加入的时间不一致,contentType内容每个租户可能不一致,因为带有缓存,默认会以第一个ContentType数据作为缓存,这样可能会导致其他使用租户使用这个模型时数据异常
按照我们对多租户数据划分的原则,如果想使用Djangoadmin模块,超级用户只能访问公共app信息,租户用户只能访问租户相关数据,因此Adamin 模块也必须进行对应适配
django中通常我们使用djangomigration做数据库的迁移,因为租户是动态新增或者减少的,通常我们需要动态的对新租户进行数据迁移操作
rest_framework作为django领域最流行的rest框架,我们在对应的认证、权限方面也需要进行适配
数据库模块适配在项目新部署的时候,默认DATABASES里面只配置公共数据库,用来保存公共app相关数据,当有租户加入的时候,要求租户必须提供数据库配置信息,我们根据数据库配置信息,动态创建数据库、数据迁移、动态为django加载数据连接。
动态创建数据库连接我们来看一段django源码
# django.utils.connection class BaseConnectionHandler: ... def __getitem__(self, alias): try: return getattr(self._connections, alias) except AttributeError: if alias not in self.settings: raise self.exception_class(f"The connection \'{alias}\' doesn\'t exist.") conn = self.create_connection(alias) setattr(self._connections, alias, conn) return connBaseConnectionHandler作为django数据库连接基类,实现了__getitem__魔法函数,意味着django 在多数据库连接的情况采取类似字典取值的方式方式返回具体的数据库连接,根据代码可知,如果数据库连接不存在的话,会抛出一个The connection \'{alias}\' doesn\'t exist.的异常,因为我们租户的数据库配置是在项目运行起来,之后动态增加了,因此数据库连接池里面肯定没有我们新加入的数据库连接,因此我们需要在ConnectionHandler找不到对应的数据库连接的时候去创建对应的数据库连接
import logging from django.db.utils import ConnectionHandler from multi_tenant.tenant import get_tenant_db logger = logging.getLogger(\'django.db.backends\') def __connection_handler__getitem__(self, alias: str) -> ConnectionHandler: if isinstance(alias, str): try: return getattr(self._connections, alias) except AttributeError: if alias not in self.settings: tenant_db = get_tenant_db(alias) if tenant_db: self.settings[alias] = tenant_db else: logger.error(f"The connection \'{alias}\' doesn\'t exist.") raise self.exception_class(f"The connection \'{alias}\' doesn\'t exist.") conn = self.create_connection(alias) setattr(self._connections, alias, conn) return conn else: logger.error(f\'The connection alias [{alias}] must be string\') raise Exception(f\'The connection alias [{alias}] must be string\') ConnectionHandler.__getitem__ = __connection_handler__getitem__