为默认的Django数据库配置PostgreSQL模式以与PgBouncer连接池配合使用

3
我需要为Django项目设置默认的数据库模式,以便所有应用程序(包括第三方应用程序)的所有表都将其表存储在配置的PostgreSQL模式中。
其中一个解决方案是使用数据库连接选项,例如:
# in Django settings module add "OPTIONS" to default DB, specifying "search_path" for the connection

DB_DEFAULT_SCHEMA = os.environ.get('DB_DEFAULT_SCHEMA', 'public')  # use postgresql default "public" if not overwritten

DATABASES['default']['OPTIONS'] = {'options': f'-c search_path={DB_DEFAULT_SCHEMA}'}

这适用于直接连接到PostgreSQL,但在连接到 PgBouncer(使用连接池)时失败,并显示OperationalError: unsupported startup parameter: options"。目前看来,PgBouncer 无法识别options作为启动参数。
另一种在不使用启动参数的情况下设置模式的解决方案是,在所有表名前加上模式前缀。为了确保这对内置和第三方应用程序也有效(而不仅仅是我的应用程序),一个解决方案是在Django加载所有模型时,使用class_prepared信号和一个AppConfig将模式名称注入到所有模型的db_table属性中。这种方法与类似于django-db-prefix的项目接近,只需要确保模式名称被正确引用即可。
from django.conf import settings
from django.db.models.signals import class_prepared


def set_model_schema(sender, **kwargs):
    schema = getattr(settings, "DB_DEFAULT_SCHEMA", "")
    db_table = sender._meta.db_table
    if schema and not db_table[1:].startswith(schema):
        sender._meta.db_table = '"{}"."{}"'.format(schema, db_table)


class_prepared.connect(set_model_schema)

这也适用于连接池,但与 Django 迁移不兼容。 使用此解决方案,python manage.py migrate 将无法工作,因为 migrate 命令 通过 内省现有表格 确保 django_migrations 表存在,而模型的 db_table 前缀对其没有影响。
我很想知道解决这个问题的正确方法是什么。
1个回答

1
这是我想出的解决方案。将上述两种方法混合使用,使用两个分离的数据库连接。
  • 使用连接启动参数(适用于应用和迁移),但仅用于运行迁移,而不是应用程序服务器。这意味着Django迁移必须直接连接到PostgreSQL,而不是通过PgBouncer连接,对于我的情况来说这很好。

  • 使用class_prepared信号处理程序前缀带有模式的DB表,但不包括django_migrations表。该处理程序使用Django应用程序(例如django_dbschema)注册,并使用AppConfig.__init __()方法,这是项目初始化过程的第一阶段,因此所有其他应用都会受到影响。使用环境变量标记绕过此注册,当运行迁移时设置该标记。这样,当应用程序运行以提供请求时,它可以像往常一样连接到PgBouncer,但Django迁移不知道模式前缀。

将使用两个环境变量(由Django设置模块使用)来配置此行为:DB_DEFAULT_SCHEMA是模式的名称,DB_SCHEMA_NO_PREFIX标志禁用信号处理程序的注册。就像这样: django_dbschema应用程序结构(位于项目根目录中)
django_dbschema/
├── apps.py
├── __init__.py

apps.py 定义了信号处理程序和 AppConfig 用于注册它:

from django.apps import AppConfig
from django.conf import settings
from django.db.models.signals import class_prepared


def set_model_schema(sender, **kwargs):
    """Prefix the DB table name for the model with the configured DB schema.
    Excluding Django migrations table itself (django_migrations).
    Because django migartion command directly introspects tables in the DB, looking
    for eixsting "django_migrations" table, prefixing the table with schemas won't work
    so Django migrations thinks, the table doesn't exist, and tries to create it.

    So django migrations can/should not use this app to target a schema.
    """

    schema = getattr(settings, "DB_DEFAULT_SCHEMA", "")
    if schema == "":
        return
    db_table = sender._meta.db_table
    if db_table != "django_migrations" and not db_table[1:].startswith(schema):
        # double quotes are important to target a schema
        sender._meta.db_table = '"{}"."{}"'.format(schema, db_table)


class DjangoDbschemaConfig(AppConfig):
    """Django app to register a signal handler for model class preparation
    signal, to prefix all models' DB tables with the schema name from "DB_DEFAULT_SCHEMA"
    in settings.

    This is better than specifying "search_path" as "options" of the connection,
    because this approach works both for direct connections AND connection pools (where
    the "options" connection parameter is not accepted by PGBouncer)

    NOTE: This app defines __init__(), to register class_prepared signal.
    Make sure no models are imported in __init__. see
    https://docs.djangoproject.com/en/3.2/ref/signals/#class-prepared

    NOTE: The signal handler for this app excludes django migrations,
    So django migrations can/should not use this app to target a schema.
    This means with this enabled, when starting the app server, Django thinks
    migrations are missing and always warns with:
    You have ... unapplied migration(s). Your project may not work properly until you apply the migrations for ...
    To actually run migrations (python manage.py migrate) use another way to set the schema
    """

    name = "django_dbschema"
    verbose_name = "Configure DB schema for Django models"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        schema = getattr(settings, "DB_DEFAULT_SCHEMA", "")
        if schema and not getattr(
            settings, "DB_SCHEMA_NO_PREFIX", False
        ):  # don't register signal handler if no schema or disabled
            class_prepared.connect(set_model_schema)


这个应用程序已注册到INSTALLED_APPS列表中(我必须使用完整的类路径来配置应用程序,否则Django将无法加载我的AppConfig定义)。
此外,Django设置模块(例如settings.py)将定义1个额外的数据库连接(一个default的副本),但具有连接选项:
# ...

INSTALLED_APPS = [
    'django_dbschema.apps.DjangoDbschemaConfig',  # has to be full path to class otherwise django won't load local app
    'django.contrib.admin',
    # ...
]

# 2 new settings to control the schema and prefix
DB_DEFAULT_SCHEMA = os.environ.get('DB_DEFAULT_SCHEMA', '')
DB_SCHEMA_NO_PREFIX = os.environ.get('DB_SCHEMA_NO_PREFIX', False)  # if should force disable prefixing DB tables with schema

DATABASES = {
    'default': {  # default DB connection definition, used by app not migrations, to work with PgBouncer no connection options
        # ...
    }
}


# default_direct: the default DB connection, but a direct connection NOT A CONNECTION POOL, so can have connection options

DATABASES['default_direct'] = deepcopy(DATABASES['default'])

# explicit test db info, prevents django test from confusing multi DB aliases to the same actual DB with circular dependencies
DATABASES['default_direct']['TEST'] = {'DEPENDENCIES': [], 'NAME': 'test_default_direct'}

# allow overriding connection parameters if necessary
if os.environ.get('DIRECT_DB_HOST'):
    DATABASES['default_direct']['HOST'] = os.environ.get('DIRECT_DB_HOST')
if os.environ.get('DIRECT_DB_PORT'):
    DATABASES['default_direct']['PORT'] = os.environ.get('DIRECT_DB_PORT')
if os.environ.get('DIRECT_DB_NAME'):
    DATABASES['default_direct']['NAME'] = os.environ.get('DIRECT_DB_NAME')

if DB_DEFAULT_SCHEMA:
    DATABASES['default_direct']['OPTIONS'] = {'options': f'-c search_path={DB_DEFAULT_SCHEMA}'}

# ...

现在设置环境变量DB_DEFAULT_SCHEMA=myschema可以配置模式。为了运行迁移,我们将设置正确的环境变量,并明确使用直接的数据库连接:
env DB_SCHEMA_NO_PREFIX=True python manage.py migrate --database default_direct

当应用程序服务器运行时,它将使用默认的数据库连接,该连接与 PgBouncer 兼容。

缺点是,由于 Django 迁移被排除在信号处理程序之外,它会认为没有运行迁移,因此它总是发出以下警告:

“您有未应用的迁移。在应用 ... 的迁移之前,您的项目可能无法正常工作。”

但事实上,只要确保在运行应用服务器之前始终运行迁移,这种情况就不成立了。

关于这个解决方案的一个注意点是,现在 Django 项目有多个 DB 连接设置(如果以前没有的话)。例如,DB 迁移应该已经编写成使用显式连接并且不能依赖于默认连接。例如,如果在迁移中使用RunPython,则应在查询对象管理器时将连接(schema_editor.connection.alias)传递给它。例如:

my_model.save(using=schema_editor.connection.alias)
# or
my_model.objects.using(schema_editor.connection.alias).all()

你现在还在使用相同的配置吗?还是找到了更简单的解决方案? - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接