多租户Django应用程序:根据请求改变数据库连接?

时间:2013-05-23 18:13:24

标签: mysql django multi-tenant

我正在寻找其他尝试使用数据库级别隔离构建多租户Django应用程序的代码和想法。

更新/解决方案:我在一个新的开源项目中解决了这个问题:请参阅django-db-multitenant

目标

我的目标是在请求进入单个应用服务器(WSGI前端,如gunicorn)时根据请求主机名或请求路径多路复用请求(例如,foo.example.com/设置Django连接以使用数据库{ {1}},foo使用数据库bar.example.com/)。

判例

我知道Django中有一些现有的多租户解决方案:

  1. django-tenant-schemas:这与我想要的非常接近:以最高优先级安装其中间件,并向db发送bar命令。不幸的是,这是Postgres特有的,我坚持使用MySQL。
  2. django-simple-multitenant:此处的策略是添加一个"租户"所有模型的外键,并调整所有应用程序业务逻辑以关键。基本上每行都会被SET search_path而不是(id, tenant_id)编入索引。我尝试过,并且不喜欢这种方法,原因有很多:它使应用程序更复杂,可能导致难以发现的错误,并且它不提供数据库级别的隔离。
  3. 每个租户一个{app server,django设置文件,包含适当的db}。 Aka穷人的多租户(实际上是富人,考虑到它所涉及的资源)。我不想为每个租户启动一个新的应用服务器,并且为了可扩展性,我希望任何应用服务器都能够为任何客户端分派请求。
  4. 到目前为止,我最好的想法是做(id)之类的事情:在第一个中间件中,抓住django-tenant-schemas并摆弄数据库选择而不是模式。我还没有想过在汇集/持久连接方面意味着什么

    我追求的另一个死胡同是特定于租户的表前缀:抛开我需要它们是动态的,即使是Django中也不容易实现全局表前缀(参见rejected ticket 5000等等) )。

    最后,Django multiple database support允许您定义多个命名数据库,并根据实例类型和读/写模式在它们之间进行多路复用。没有帮助,因为无法根据请求选择数据库。

    问题

    有没有人管理类似的东西?如果是这样,你是如何实现它的?

3 个答案:

答案 0 :(得分:14)

我做了类似于最接近第1点的类似事情,但不使用中间件来设置默认连接,而是使用Django数据库路由器。这允许应用程序逻辑在每个请求需要时使用多个数据库。应用程序逻辑可以为每个查询选择合适的数据库,这是这种方法的一大缺点。

通过此设置,settings.DATABASES中列出了所有数据库,包括可在客户之间共享的数据库。每个客户特定的模型都放在具有特定应用标签的Django应用中。

例如。以下类定义了存在于所有客户数据库中的模型。

class MyModel(Model):
    ....
    class Meta:
        app_label = 'customer_records'
        managed = False

数据库路由器放在settings.DATABASE_ROUTERS链中以app_label路由数据库请求,类似这样(不是完整示例):

class AppLabelRouter(object):
    def get_customer_db(self, model):
        # Route models belonging to 'myapp' to the 'shared_db' database, irrespective
        # of customer.
        if model._meta.app_label == 'myapp':
            return 'shared_db'
        if model._meta.app_label == 'customer_records':
            customer_db = thread_local_data.current_customer_db()
            if customer_db is not None:
                return customer_db

            raise Exception("No customer database selected")
        return None

    def db_for_read(self, model, **hints):
        return self.get_customer_db(model, **hints)

    def db_for_write(self, model, **hints):
        return self.get_customer_db(model, **hints)

此路由器的特殊部分是thread_local_data.current_customer_db()电话。在执行路由器之前,调用者/应用程序必须在thread_local_data中设置当前客户数据库。可以使用Python上下文管理器来推送/弹出当前的客户数据库。

通过配置所有这些,应用程序代码看起来像这样,其中UseCustomerDatabase是一个上下文管理器,用于将当前客户数据库名称推送/弹出thread_local_data,以便thread_local_data.current_customer_db()将在最终命中路由器时返回正确的数据库名称:

class MyView(DetailView):
    def get_object(self):
        db_name = determine_customer_db_to_use(self.request) 
        with UseCustomerDatabase(db_name):
            return MyModel.object.get(pk=1)

这已经非常复杂了。它有效,但我会试着总结一下我看到的优点和缺点:

<强>优点

  • 数据库选择非常灵活。它允许在单个查询中使用多个数据库,客户特定数据库和共享数据库都可以在请求中使用。
  • 数据库选择是明确的(不确定这是优势还是劣势)。如果您尝试运行命中客户数据库的查询,但应用程序未选择一个,则会发生异常,指示编程错误。
  • 使用数据库路由器允许不同的主机上存在不同的数据库,而不是依赖于猜测所有数据库都可通过单个连接访问的USE db;语句。

<强>缺点

  • 设置很复杂,并且涉及很多层以使其正常运行。
  • 线程本地数据的需求和使用是模糊的。
  • 视图中充斥着数据库选择代码。这可以使用基于类的视图进行抽象,以基于请求参数自动选择数据库,方式与中间件选择默认数据库的方式相同。
  • 选择数据库的上下文管理器必须围绕查询集进行处理,以便在评估查询时上下文管理器仍处于活动状态。

<强>建议

如果您想要灵活的数据库访问,我建议使用Django的数据库路由器。使用中间件或视图Mixin,它根据请求参数自动设置用于连接的默认数据库。您可能不得不求助于线程本地数据来存储要使用的默认数据库,以便在命中路由器时,它知道要路由到哪个数据库。这允许Django使用其与数据库的现有持久连接(如果需要,可以驻留在不同的主机上),并根据请求中设置的路由选择要使用的数据库。

此方法还具有以下优点:如果需要,可以使用QuerySet using()函数选择非默认数据库来覆盖查询数据库。

答案 1 :(得分:2)

您可以创建一个自己的简单中间件来确定子域中的数据库名称,然后在数据库游标上为每个请求执行USE语句。看看django-tenants-schema代码,这基本上就是它正在做的事情。它是对psycopg2进行子类化并发出相当于USE的postgres,“set search_path XXX”。您也可以创建一个模型来管理和创建您的租户,但之后您将重写大量的django-tenants-schema。

在MySQL中切换模式(db名称)不应该有性能或资源损失。它只是为连接设置会话参数。

答案 2 :(得分:2)

为了记录,我选择实现我的第一个想法的变体:在早期请求中间件中发出USE <dbname>。我也以同样的方式设置CACHE前缀。

我在一个小型生产站点上使用它,根据请求主机从Redis数据库中查找租户名称。到目前为止,我对结果非常满意。

我把它变成了一个(希望可以恢复的)github项目:https://github.com/mik3y/django-db-multitenant