You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

运行Django测试触发MigrationSchemaMissing异常求助

Django-tenants 迁移与测试报错解决方案

问题描述

使用django-tenants的Django项目中遭遇以下问题:

  • 无法运行任何测试用例
  • 执行迁移时触发错误:

django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table (no schema has been selected to create in LINE 1: CREATE TABLE ‘django_migrations’ (‘id’ bigint NOT NULL PRIMA...

从django-tenant-schemas迁移到django-tenants的过程不顺畅,空数据库执行迁移也报错,租户应用的迁移无法正常执行,需解决上述问题并寻求项目改进建议。


项目详情

依赖配置

[tool.poetry.dependencies]
python = "^3.13"
django = "5.1.8"  # The newest version is not compatible with django-tenants yet
django-tenants = "^3.7.0"
dj-database-url = "^2.3.0"
django-bootstrap5 = "^25.1"
django-bootstrap-icons = "^0.9.0"
uvicorn = "^0.34.0"
uvicorn-worker = "^0.3.0"
gunicorn = "^23.0.0"
whitenoise = "^6.8.2"
encrypt-decrypt-fields = "^1.3.6"
django-bootstrap-modal-forms = "^3.0.5"
django-model-utils = "^5.0.0"
werkzeug = "^3.1.3"
tzdata = "^2025.2"
pytz = "^2025.2"
psycopg = {extras = ["binary", "pool"], version = "^3.2.4"}
django-colorfield = "^0.13.0"
sentry-sdk = {extras = ["django"], version = "^2.25.1"}

Settings.py配置

import os
from pathlib import Path
from uuid import uuid4

# External Dependencies
import dj_database_url
from django.contrib.messages import constants as messages
from django.utils.translation import gettext_lazy as _

BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = os.path.join(BASE_DIR, os.pardir)
TENANT_APPS_DIR = BASE_DIR / "tenant"

DEBUG = os.environ.get("DEBUG", "False").lower() in ["true", "1", "yes"]
TEMPLATE_DEBUG = DEBUG
SECRET_KEY = os.environ.get("SECRET_KEY", str(uuid4())) if DEBUG else os.environ["SECRET_KEY"]
VERSION = os.environ.get("VERSION", "develop")
SECURE_SSL_REDIRECT = os.environ.get("SECURE_SSL_REDIRECT", "False").lower() in ["true", "1", "yes"]

try:
    ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(";")
except KeyError:
    if not DEBUG:
        raise

ALLOWED_HOSTS = ["localhost", ".localhost"]

DEFAULT_DOMAIN = os.environ.get("DEFAULT_DOMAIN", ALLOWED_HOSTS[0])

DEFAULT_FILE_STORAGE = "django_tenants.files.storage.TenantFileSystemStorage"

SHARED_APPS = [
    "django_tenants",
    "sfe.common.apps.CommonConfig",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

TENANT_APPS = [
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django_bootstrap5",
    "django_bootstrap_icons",
    "bootstrap_modal_forms",
    "sfe.tenant.apps.TenantConfig",
    "sfe.tenant.email_controller.apps.EmailControllerConfig",
]

INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]

MIDDLEWARE = [
    "sfe.common.middleware.HealthCheckMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

if DEBUG:
    INTERNAL_IPS = ["localhost", ".localhost"]

ROOT_URLCONF = "sfe.urls_tenant"
PUBLIC_SCHEMA_NAME = "public"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
                "sfe.common.context_processor.version",
                "sfe.common.context_processor.default_domain",
            ],
        },
    },
]

WSGI_APPLICATION = "sfe.wsgi.application"

default_db = dj_database_url.config(engine="django_tenants.postgresql_backend")
DATABASES = {
    "default": {
        "OPTIONS": {"pool": True},
        **default_db,
    }
}
DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",)
TEST_RUNNER = "django.test.runner.DiscoverRunner"

AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

TENANT_MODEL = "common.SystemTenant"
TENANT_DOMAIN_MODEL = "common.Domain"
PUBLIC_SCHEMA_NAME = "public"

LANGUAGE_CODE = "pl"
LANGUAGES = [("pl", "Polski"), ("en", "English")]
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)

TIME_ZONE = "UTC"
USE_TZ = True
USE_I18N = True

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(PROJECT_ROOT, "static")

LOGIN_URL = _("/login/")
LOGOUT_REDIRECT_URL = "/"
LOGIN_REDIRECT_URL = "/"
SESSION_COOKIE_AGE = 86400

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

IMAP_TIMEOUT = 60
ADMINS = [("Damian Giebas", "damian.giebas@gmail.com")]
MANAGERS = ADMINS

FIRST_DAY_OF_WEEK = 1

项目结构

项目结构

测试代码示例

# External Dependencies
from django.utils.timezone import now
from django_tenants.test.cases import TenantTestCase
from django_tenants.utils import schema_context

# Current App
from sfe.tenant.models.due_date import DueDate


class DueDateModelTests(TenantTestCase):
    def setUp(self):
        super().setUp()

    def test_create_due_date(self):
        with schema_context(self.tenant.schema_name):
            DueDate.objects.create(date=now().date(), name="TestDueDate")

        assert DueDate.objects.all().count() == 1

核心解决方案

1. 修正测试运行器与数据库配置

  • 将默认测试运行器替换为django-tenants专用版本,确保测试时自动处理schema切换:
    TEST_RUNNER = "django_tenants.test.runner.TenantTestSuiteRunner"
    
  • 确保数据库引擎正确加载,可显式指定默认数据库URL避免环境变量缺失问题:
    default_db = dj_database_url.config(
        default="postgres://username:password@localhost/your_db_name",
        engine="django_tenants.postgresql_backend"
    )
    

2. 规范迁移执行流程

django-tenants要求迁移分两步执行,不能直接用默认的migrate命令:

  • 先执行共享应用迁移(针对public schema):
    python manage.py migrate_schemas --shared
    
  • 创建租户实例后,再执行租户应用迁移:
    python manage.py migrate_schemas
    
    空数据库初始化时必须先运行migrate_schemas --shared,否则会出现"无schema可创建"的错误。

3. 优化测试代码

TenantTestCase会自动切换到测试租户的schema,无需手动使用schema_context,简化测试逻辑:

class DueDateModelTests(TenantTestCase):
    def test_create_due_date(self):
        DueDate.objects.create(date=now().date(), name="TestDueDate")
        assert DueDate.objects.count() == 1

若需要测试public schema中的模型,使用PublicSchemaTestCase:

from django_tenants.test.cases import PublicSchemaTestCase

class PublicDomainTests(PublicSchemaTestCase):
    def test_domain_creation(self):
        # 直接操作public schema中的Domain模型
        pass

4. 排查连接池冲突

psycopg的连接池配置可能与django-tenants的schema切换机制冲突,先临时移除连接池配置测试:

DATABASES = {
    "default": {
        # 注释掉连接池配置
        # "OPTIONS": {"pool": True},
        **default_db,
    }
}

确认问题解决后,再参考django-tenants官方文档适配连接池(需确保连接池支持schema切换)。


项目改进建议

  • 版本兼容性验证:确认django-tenants 3.7.0与Django 5.1.8的官方兼容性,避免使用未验证的版本组合导致隐性问题。
  • 迁移文件清理:从django-tenant-schemas迁移后,检查所有迁移文件,移除旧库相关依赖,确保迁移逻辑仅依赖django-tenants。
  • 配置分层管理:将租户配置、公共配置按环境(开发/生产)分离,使用.env文件管理敏感配置,避免硬编码。
  • 测试结构优化:按共享应用、租户应用、public schema三类组织测试用例,明确测试范围,提升可维护性。
  • 日志增强:添加schema切换的日志记录,便于排查迁移和测试中的数据库上下文问题。

内容的提问来源于stack exchange,提问作者Draqun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 12:32:03