[Fixed]-Use multiple databases in Django with only one table "django_migrations"

9👍

Thanks to the comments on my question I did some research and came up with the following findings.

Using multiple databases results in creating a table django_migrationswhen migrations are used. There is no option to record the migrations in only one table django_migrations, as the comment from Kamil Niski explains. This is clear after reading the file django/db/migrations/recorder.py.

I will illustrate an example with a project foo and an app bar inside the project. The app bar has only one model Baz.

We create the project:

django-admin startproject foo

Now we have these contents inside the main project directory:

- foo
- manage.py

I have a habit to group all apps inside the project directory:

mkdir foo/bar
python manage.py bar foo/bar

In the file foo/settings.py we adjust the settings to use two different databases, for the purposes of this example we use sqlite3:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db1.sqlite3'),
    },
    'remote': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db2.sqlite3'),
    }
}

Now we run the migrations:

python manage.py migrate --database=default

This runs all migrations, the part --database=default is optional, because if not specified Django uses the default database.

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK

Django has applied all migrations to the default database:

1           contenttypes  0001_initial  2019-11-13 16:51:04.767382
2           auth          0001_initial  2019-11-13 16:51:04.792245
3           admin         0001_initial  2019-11-13 16:51:04.827454
4           admin         0002_logentr  2019-11-13 16:51:04.846627
5           admin         0003_logentr  2019-11-13 16:51:04.864458
6           contenttypes  0002_remove_  2019-11-13 16:51:04.892220
7           auth          0002_alter_p  2019-11-13 16:51:04.906449
8           auth          0003_alter_u  2019-11-13 16:51:04.923902
9           auth          0004_alter_u  2019-11-13 16:51:04.941707
10          auth          0005_alter_u  2019-11-13 16:51:04.958371
11          auth          0006_require  2019-11-13 16:51:04.965527
12          auth          0007_alter_v  2019-11-13 16:51:04.981532
13          auth          0008_alter_u  2019-11-13 16:51:05.004149
14          auth          0009_alter_u  2019-11-13 16:51:05.019705
15          auth          0010_alter_g  2019-11-13 16:51:05.037023
16          auth          0011_update_  2019-11-13 16:51:05.054449
17          sessions      0001_initial  2019-11-13 16:51:05.063868

Now we create the model Baz:

models.py:

from django.db import models

class Baz(models.Model):
    name = models.CharField(max_length=255, unique=True)

register the app bar into INSTALLED_APPS (foo/settings.py) and create the migrations:

python manage.py makemigrations bar

Before we run the migrations we create routers.py inside the bar app:

class BarRouter(object):
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'bar':
            return 'remote'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'bar':
            return 'remote'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == 'bar':
            return db == 'remote'
        if db == 'remote':
            return False
        return None

and register it in foo/settings.py:

DATABASE_ROUTERS = ['foo.bar.routers.BarRouter']

Now the naive approach would be to run the migrations for bar into the remote database:

python manage.py migrate bar --database=remote
Operations to perform:
  Apply all migrations: bar
Running migrations:
  Applying bar.0001_initial... OK

The migrations has been applied to the remote database:

1           bar         0001_initial  2019-11-13 17:32:39.701784

When we run:

python manage.py runserver

the following warning will be raised:

You have 1 unapplied migration(s). Your project may not work properly
until you apply the migrations for app(s): bar.
Run ‘python manage.py migrate’ to apply them.

Everything seems to work fine though. However it isn’t satisfying having this warning.

The proper way would be to run all migrations for each database as suggested in this answer.

It would look like this:

python manage.py migrate --database=default
python manage.py migrate --database=remote

and after creating the migrations for bar:

python manage.py migrate bar --database=default
python manage.py migrate bar --database=remote

The router will take care that the table bar_baz is created only in the remote database, but Django will mark the migrations as applied in both databases. Also the tables for auth, admin, sessions, etc. will be created only in the default database, as specified in routers.py. The table django_migrations in the remote database will have records for these migrations too.

It is a long reading, but I hope it sheds some light on this, in my opinion, not thoroughly explained issue in the official documentation.

👤cezar

Leave a comment