Django多租户,可为每个租户自动创建数据库

时间:2019-04-04 14:01:38

标签: django python-3.x multi-tenant

所以我正在处理具有以下要求的Django应用程序:

1-应用程序应该能够处理多租户(在我们的应用程序中称为组织),并为每个组织/租户使用单独的数据库。

2-应用程序可以具有一个共享的数据库,用于处理每个组织的用户模型,因此,通过此共享的db进行身份验证和登录。

3-新的组织管理员在我们的应用程序中注册了新帐户后,他/她应该能够(通过表格)为他/她的组织初始化新数据库的创建,当然迁移该数据库并开始对其进行CRUD操作。

由于Django supports具有多个数据库,因此在将django指向使用相应数据库的同时对任何模型进行CRUD操作都没有问题。 而且由于将所有用户放在共享数据库中的一个表中并从那里进行身份验证符合我们的项目要求,所以对于共享数据库的tightly coupled身份验证后端,我没有遇到问题。

我面临的问题是需要为每个组织管理员自动创建一个新数据库,我的意思是自动的是,组织管理员是触发数据库创建/迁移过程的管理员,不需要来自我们的开发人员或系统管理员的干预。 在此之上,应该使Django对创建的每个新数据库都“了解”,并在不重新启动django服务器的情况下开始与它的连接。

因此,我想出了一个解决问题的方法,但是该解决方案未在文档中提及,也没有在Web或SO的任何位置实现。 因此,我担心此解决方案是否可行,否则可能会引入错误或任何安全漏洞,所以这就是我寻求帮助的原因。

我的解决方案

应自定义默认的用户模型,以将组织名称作为字段,以后将在视图中使用该模型将django指向正确的数据库:

from django.contrib.auth.models import AbstractUser
...
class CustomUser(AbstractUser):
    organization = models.CharField(max_length=255)

当然还有settings.py

AUTH_USER_MODEL = 'base.CustomUser'

Databases字典将照常保存默认数据库:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'db_name',
        'USER': 'db_user',
        'PASSWORD': 'db_pass',
        'HOST': 'localhost',
        'PORT': 5432,
    }
}

到目前为止,没有什么异常,我们现在必须在共享数据库中创建另一个表,以保存组织数据库及其详细信息的列表:

class Organization(models.Model):
    org_name = models.CharField(max_length=255)
    django_db_name = models.CharField(max_length=255)
    postgres_db_name = models.CharField(max_length=255)

因此,现在我们需要使用django来了解Organization模型中存储的新数据库,无论它何时启动其服务器并尝试像开始时一样“消化” settings.py文件,因此在Databases字典下方,我们添加了以下内容:

from django.db.utils import ProgrammingError
from base.models import Organization
...
DATABASES = {
    'default': {
...
    }
}
try:
    organizations = Organization.objects.all()
    for organization in organizations:
        # Dynamically adding the new databases instead of hard coding them in the settings file.
        DATABASES[organization.django_db_name] =  {
            'ENGINE': 'django.db.backends.postgresql_psycopg2',
            'NAME': organization.postgres_db_name,
            'USER': 'db_user',
            'PASSWORD': 'db_pass', # In production this should be encrypted and stored and retrieved from the shared db Organization table.
            'HOST': 'localhost',
            'PORT': 5432,
    }
except ProgrammingError:
    pass # The Organization table was not found in the db, i.e. hasn't been created yet.

到目前为止,在上述代码中,我们已经设法将新数据库动态添加到Django设置中,而无需每次添加新数据库时都需要编辑设置文件。

下一步,我们要实现自动创建和迁移数据库的方式,并将其详细信息存储在共享数据库中存在的Organization模型中,以供以后检索。 除此之外,我们需要一种使django能够“感知”新创建的数据库并告诉它与它建立连接的方法,而无需重新启动django服务器。

因此,我们将创建一组实用程序函数来处理所有这些问题,将其放入例如utils/db.py

import psycopg2
import os
from base.models import Organization
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from django.db import connections
from django.db.backends.postgresql.base import DatabaseWrapper

def _create_new_db(postgres_db_name):
    con = psycopg2.connect(dbname='db_name', user='db_user', password='db_pass', host='localhost', port=5432)
    con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
    cur = con.cursor()
    cur.execute('CREATE DATABASE {postgres_db_name} OWNER db_user'.format(postgres_db_name=postgres_db_name))

def _update_django_settings_and_migrate(django_db_name, postgres_db_name):
    connections.databases[django_db_name] = connections.databases['default'] # copying the default db dict to be used as a template
    connections.databases[django_db_name]['NAME'] = postgres_db_name
    # This is the only way to make django aware of the new db, I came up with this through digging in the source code, there is no reference in the docs for this:
    wrapper = DatabaseWrapper(connections.databases[django_db_name], django_db_name)
    connections[django_db_name] = wrapper
    # Now migrating the db
    os.system('python manage.py migrate --database={}'.format(django_db_name))

def _store_new_settings_in_shared_db(org_name, django_db_name, postgres_db_name):
    Organization(org_name=org_name, django_db_name=django_db_name, postgres_db_name=postgres_db_name).save(using='default')

def new_user_org_setup(org_name, django_db_name, postgres_db_name):
    _create_new_db(postgres_db_name)
    _store_new_settings_in_shared_db(org_name, django_db_name, postgres_db_name)
    _update_django_settings_and_migrate(django_db_name, postgres_db_name)

因此,现在我们可以从我们的视图中调用new_user_org_setup函数,如下所示: base/views.py

from django.contrib.auth import get_user_model
from django.shortcuts import redirect
from base.forms import UserForm
from utils.db import new_user_org_setup
...
def sign_up(request):
    if request.method == 'POST':
        form = UserForm(request.POST)
        if form.is_valid():
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            organization = form.cleaned_data.get('organization')
            new_user_org_setup(org_name=organization, django_db_name=organization, postgres_db_name=organization)
            user = get_user_model()(username=username)
            user.set_password(password)
            user.organization = organization
            user.save()

            return redirect('base:home')
    else:
        form  = UserForm()
    return render(request, 'base/sign_up.html', {'form':form})

上面的设置可以满足所有需要,但是正如我之前说过的那样,我担心会出乎意料。因此,如果有人可以指出漏洞或提出更好的方法或以任何方式改进我的代码,请我将感激不尽。

0 个答案:

没有答案