Search code examples
pythondjangodjango-modelsdjango-rest-frameworkdjoser

Django always accesses default database when creating users


I have a django project with multiple databases:

DATABASES = {
    'default': {},
    'db1': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / DB1_NAME,
    },
    'db2': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / DB2_NAME,            
    }
}

DATABASE_ROUTERS = ['app.router1.Router1', 'app.router2.Router2']

The router for db1 is defined:

class Router1:
    route_app_labels = {'app', 'sessions', 'auth', 'admin', 'contenttypes'}

    def db_for_read(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return 'db1'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return 'db1'
        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 == 'db1'
        return None

Everything seems to work well, except creating new users. When I try to create a new user, I get the following error:

django.core.exceptions.ImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.

When I change db1 to default, user creation seems to work well.

Full error traceback:

Traceback (most recent call last):
  File "/.venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/.venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/.venv/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/.venv/lib/python3.9/site-packages/rest_framework/viewsets.py", line 125, in view
    return self.dispatch(request, *args, **kwargs)
  File "/.venv/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "/.venv/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/.venv/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "/.venv/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "/.venv/lib/python3.9/site-packages/rest_framework/mixins.py", line 19, in create
    self.perform_create(serializer)
  File "/.venv/lib/python3.9/site-packages/djoser/views.py", line 136, in perform_create
    user = serializer.save()
  File "/.venv/lib/python3.9/site-packages/rest_framework/serializers.py", line 212, in save
    self.instance = self.create(validated_data)
  File "/.venv/lib/python3.9/site-packages/djoser/serializers.py", line 65, in create
    user = self.perform_create(validated_data)
  File "/.venv/lib/python3.9/site-packages/djoser/serializers.py", line 72, in perform_create
    with transaction.atomic():
  File "/.venv/lib/python3.9/site-packages/django/db/transaction.py", line 197, in __enter__
    if not connection.get_autocommit():
  File "/.venv/lib/python3.9/site-packages/django/db/backends/base/base.py", line 455, in get_autocommit
    self.ensure_connection()
  File "/.venv/lib/python3.9/site-packages/django/db/backends/dummy/base.py", line 20, in complain
    raise ImproperlyConfigured(
django.core.exceptions.ImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.

Solution

  • I think I found the root cause of this issue. It seems that before the new user is saved, a transaction is being created, but this transaction always tries to access the default database. If default database is not defined, the above error is returned (in case the default database is defined, it means that the transaction is accessing the default, while users are being written to a different database).

    In djoser\serializers.py, the perform_create method first creates an atomic transaction with no arguments:

    def perform_create(self, validated_data):
        with transaction.atomic():
            user = User.objects.create_user(**validated_data)
            if settings.SEND_ACTIVATION_EMAIL:
                user.is_active = False
                user.save(update_fields=["is_active"])
        return user
    

    The atomic function creates it with using=None:

    def atomic(using=None, savepoint=True, durable=False):
        # Bare decorator: @atomic -- although the first argument is called
        # `using`, it's actually the function being decorated.
        if callable(using):
            return Atomic(DEFAULT_DB_ALIAS, savepoint, durable)(using)
        # Decorator: @atomic(...) or context manager: with atomic(...): ...
        else:
            return Atomic(using, savepoint, durable)
    

    Then inside Atomic class, in __enter__ method the function get_connection is called. This function returns default database in case using is not defined:

    def get_connection(using=None):
        """
        Get a database connection by name, or the default database connection
        if no name is provided. This is a private API.
        """
        if using is None:
            using = DEFAULT_DB_ALIAS
        return connections[using]
    

    The fix for this should probably be to pass the right the database to atomic function. As mentioned in the question, renaming db1 to default bypasses this issue.