Django Model custom User creates auth_user table in my app database when using multiple databases

This is fresh from my stack overflow post… Really could do with some help here…

I’ve created a new project from scratch. By default it uses the auth package for user and permission management.

Given the following from official Django Docs

Using a custom user model when starting a project¶
If you’re starting a new project, it’s highly recommended to set up a custom user model, even if the default User model is sufficient for you. This model behaves identically to the default user model, but you’ll be able to customize it in the future if the need arises:

I am wanting to override the default User - using AbstractBaseUser.

I am trying to setup this in a multi-database environment. One database for the auth, ‘contenttypes’, ‘sessions’, and ‘admin’. And the other for my application related stuff.

Having created migrations and executed them on my databases, the auth_user table resides in my application database - not the auth database.

Is this expected behaviour? I was expecting - and I want the auth_user table to reside in the auth database.

Is there a way to make the auth_user reside in the auth database?

Create Project

py -m venv venv
venv\scripts\activate
pip install django

django-admin startproject core .
py manage.py startapp acmeapp

acmeapp/models.py

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager


class Foo(models.Model):
    foo_id = models.BigAutoField(primary_key=True)
    name = models.TextField()

    class Meta:
        db_table = 'foos'

class AccountManager(BaseUserManager):
    """
    The management class to handle user creation.
    """

    def create_user(self, username, password=None):
        if not username:
            raise ValueError(_('The Username must be set'))

        user = self.model(username=username)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, username, password=None):
        return self.create_user(username, password)


class Account(AbstractBaseUser):

    """
    The custom user model.
    """
    username = models.CharField(max_length=40, unique=True)
    email = None
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['access_platforms', 'access_missions', 'access_rule_sets', 'access_mdsr']

    objects = AccountManager()

    def __str__(self):
        return ''.join(c for c in self.username if c.isupper())

    class Meta:
        db_table = 'auth_user'
                

core/settings.py

DATABASES = {
    'default': {},
    'auth_db': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'auth_db.sqlite3',
        "TEST": {
            "NAME": "auth_db",
            'DEPENDENCIES': [],
        },
    },
    'acmeapp_db': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'acmeapp_db.sqlite3',
        "TEST": {
            "NAME": "acmeapp_db",
            'DEPENDENCIES': [],
        },
    }
}

DATABASE_ROUTERS = [
    'core.db_routers.AuthRouter',
    'core.db_routers.AcmeAppRouter'
]

AUTH_USER_MODEL = 'acmeapp.Account'

core/db_routers

from acmeapp.models import Account


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'
        
        if model == Account:
            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'
        
        if model == Account:
            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

Make and Apply Migrations

Once the above code is created, I create migrations and apply them to each database.

py manage.py makemigrations
py manage.py migrate --database=acmeapp_db
py manage.py migrate --database=auth_db

/acmeapp/migrations/0001_initial.py

# Generated by Django 4.0.4 on 2022-08-11 07:50

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Account',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('username', models.CharField(max_length=40, unique=True)),
            ],
            options={
                'db_table': 'auth_user',
            },
        ),
        migrations.CreateModel(
            name='Foo',
            fields=[
                ('mission_id', models.BigAutoField(primary_key=True, serialize=False)),
                ('name', models.TextField()),
                ('retired', models.BooleanField(default=False)),
            ],
            options={
                'db_table': 'foos',
            },
        ),
    ]

Which has the two models including Account.

As a result the two databases are created and have tables as follows:

acmeapp_db.sqlite

auth_user
django_migrations
foos
sqlite_sequence

auth_db.sqlite

auth_group
auth_group_permissions
auth_permission
django_admin_log
django_content_type
django_migrations
django_session
sqlite_sequence

Research

I have not found an answer to the problem. Every blog post on setting this up never covers the what happens in this scenario.

I havent found an answer in any Django documentation either.

Curiously, https://code.djangoproject.com/wiki/MultipleDatabaseSupport

Different models/applications living on different databases, for example a ‘blog’ application on db1 and a forum application on db2. This should include the ability to assign a different database to an existing application without modifying it, e.g. telling Django where to keep the django.contrib.auth.User table.

Reread the section about Routers in the multiple databases docs.

Returning None from a router test method does not mean “do not do this”, it means “this router doesn’t care”.

1 Like

Thanks for the response Ken.

Yep - I understood it meant the router didn’t care. However, I’ve gone back through and debugged the router logic when applying migrations. I think it was out somewhat.

AuthRouter

def allow_migrate(self, db, app_label, model_name=None, **hints):
        
        if app_label in self.route_app_labels or model_name == 'account':
            return db == 'auth_db'
        
        
        return None

AcmeAppRouter

def allow_migrate(self, db, app_label, model_name=None, **hints):
        
        if app_label in self.route_app_labels and model_name != 'account':
            return db == 'acmeapp_db'
        

        return None

Which gives the error when migrating the auth_db with:

sqlite3.OperationalError: table "auth_user" already exists

Which makes sense as the migrations applied would have created that table in the auth package - assuming it would be in auth.0001_initial.

Do we have any user customisation guidance at this point. I’ve tried looking back here - Customizing authentication in Django | Django documentation | Django (djangoproject.com).

Although there was never a hint around adding Meta to the user class to specify the table which we have done here:

class Meta:
        db_table = 'auth_user'

Changing the table name to something else super_duper_auth_user fixes the issue. Additionally the auth_user table is not created.

I am assuming this was the other issue at play here - although I am slightly confused given sqlite3.OperationalError: table “auth_user” already exists - but where did it disappear to when I changed that Meta db_table.

I hope the changes look ok to you here.

Thanks

Paul

Side note: Personally, I would find it confusing to have two separate routers - especially if your tables should only belong in one database.

We use a slightly different technique. We create a dict mapping the app to a database, and so our various methods do things like:
return target_db[app_label] or return target_db.get(app_label, 'default') depending on the system.
(target_db might end up looking something like this:
target_db = {'app1': 'db1', 'app2': 'db1', 'app3': 'db2', ...})

This allows us to keep all the logic in a single router - we find it easier that way.

OK thanks… We are just getting our feet wet with Django so to speak… Any advice is welcomed. I don’t think we are anticipating connecting any further databases up so will look at merging.