I've created a cut down scenario from scratch to hopefully find a solution to what I have witnessed in some production code - which has stopped me dead in my tracks.
The code is here:
https://github.com/andez2000/issue-django-multi-db
The problem is that I have a Django project - which will support multiple databases. So for this scenario:
- acmeapp targets acmeapp_db.
- auth targets auth_db.
- There is no default connection - all connections are aliased.
This is not a direct result from watching the following:
Django Multiple Database Setup Ex1
Django Testing - Model Testing Introduction
Routers have been setup and configured to direct the models to connections.
When running py manage.py test
the following errors are evident.
AssertionError: Database queries to 'acmeapp_db' are not allowed in this test. Add 'acmeapp_db' to acmeapp.tests.model.TestModels.databases to ensure proper test isolation and silence this failure.
Fixing this as suggested yields an additional error. This is done by adding databases = {'acmeapp_db'}
to TestModels
. This produces:
ImproperlyConfigured("Circular dependency in TEST[DEPENDENCIES]") django.core.exceptions.ImproperlyConfigured: Circular dependency in TEST[DEPENDENCIES]
core/settings.py
DATABASES = {
'default': {},
'auth_db': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'auth_db.sqlite3',
},
'acmeapp_db': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'acmeapp_db.sqlite3',
}
}
DATABASE_ROUTERS = [
'core.db_routers.AuthRouter',
'core.db_routers.AcmeAppRouter'
]
tests/model.py
from django.test import TestCase
from acmeapp.models import Post
class TestModels(TestCase):
def setUp(self):
Post.objects.create(title="This is a test")
def test_Post_str(self):
post = Post.objects.first()
self.assertEqual(str(post), "This is a test")
core/db_routers.py
class AuthRouter:
route_app_labels = ['auth', 'contenttypes', 'sessions', 'admin']
def db_for_read(self, model, **hints):
if model._meta.app_label in self.route_app_labels:
return 'auth_db'
return None
def db_for_write(self, model, **hints):
if model._meta.app_label in self.route_app_labels:
return 'auth_db'
return None
def allow_relation(self, obj1, obj2, **hints):
if (
obj1._meta.app_label in self.route_app_labels or
obj2._meta.app_label in self.route_app_labels
):
return True
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
if app_label in self.route_app_labels:
return db == 'auth_db'
return None
class AcmeAppRouter:
route_app_labels = ['acmeapp']
def db_for_read(self, model, **hints):
if model._meta.app_label in self.route_app_labels:
return 'acmeapp_db'
return None
def db_for_write(self, model, **hints):
if model._meta.app_label in self.route_app_labels:
return 'acmeapp_db'
return None
def allow_relation(self, obj1, obj2, **hints):
if (
obj1._meta.app_label in self.route_app_labels or
obj2._meta.app_label in self.route_app_labels
):
return True
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
if app_label in self.route_app_labels:
return db == 'acmeapp_db'
return None
Having debugged the routers - the database alias in the parameter db
is always 'default'
.
Is this setup even possible? Am I missing something?
For this scenario the mechanism for applying migrations has somewhat changed from a single migration call py manage.py migrate
to:
py manage.py migrate --database=auth_db
py manage.py migrate --database=acmeapp_db
A little bit more debugging, it would appear that core.settings.DATABASES
has:
{
"default": {
"ENGINE": "django.db.backends.dummy",
"ATOMIC_REQUESTS": False,
"AUTOCOMMIT": True,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"OPTIONS": {},
"TIME_ZONE": None,
"NAME": "",
"USER": "",
"PASSWORD": "",
"HOST": "",
"PORT": "",
"TEST": {
"CHARSET": None,
"COLLATION": None,
"MIGRATE": True,
"MIRROR": None,
"NAME": None,
},
},
"auth_db": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": WindowsPath(
"SOMEFOLDER\auth_db.sqlite3"
),
"ATOMIC_REQUESTS": False,
"AUTOCOMMIT": True,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"OPTIONS": {},
"TIME_ZONE": None,
"USER": "",
"PASSWORD": "",
"HOST": "",
"PORT": "",
"TEST": {
"CHARSET": None,
"COLLATION": None,
"MIGRATE": True,
"MIRROR": None,
"NAME": None,
},
},
"acmeapp_db": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": WindowsPath(
"SOMEFOLDER\acmeapp_db.sqlite3"
),
"ATOMIC_REQUESTS": False,
"AUTOCOMMIT": True,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"OPTIONS": {},
"TIME_ZONE": None,
"USER": "",
"PASSWORD": "",
"HOST": "",
"PORT": "",
"TEST": {
"CHARSET": None,
"COLLATION": None,
"MIGRATE": True,
"MIRROR": None,
"NAME": None,
},
},
}