29

I have a hard time with creating data migrations. I use two databases for my apps. I configured databases in settings.py and also created a router like in Django docs.

# settings.py
DB_HOST = 'localhost'
DATABASES = {
'default': {
    'ENGINE': 'django.db.backends.mysql',
    'NAME': 'helios',
    'HOST': DB_HOST,
    'OPTIONS': {
        'read_default_file': join(dirname(__file__), 'default.cnf'),
    },
},
'other': {
    'ENGINE': 'django.db.backends.mysql',
    'NAME': 'gala_pol',
    'HOST': DB_HOST,
    'OPTIONS': {
        'read_default_file': join(dirname(__file__), 'other.cnf'),
    },
},

DATABASE_APPS_MAPPING = {
    'contenttypes': 'default',
    'auth': 'default',
    'admin': 'default',
    'sessions': 'default',
    'messages': 'default',
    'staticfiles': 'default',
    'woodsmen': 'default',
    'helios': 'default',
    'hush': 'default',
    'hunt': 'other',
    'meat': 'other',
    'beast': 'other',
}

# routers.py

class DatabaseAppsRouter(object):

    def db_for_read(self, model, **hints):

        if model._meta.app_label in settings.DATABASE_APPS_MAPPING:
            return settings.DATABASE_APPS_MAPPING[model._meta.app_label]
        return None

    def db_for_write(self, model, **hints):

        if model._meta.app_label in settings.
            return settings.DATABASE_APPS_MAPPING[model._meta.app_label]
        return None

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

        db1 = settings.DATABASE_APPS_MAPPING.get(obj1._meta.app_label)
        db2 = settings.DATABASE_APPS_MAPPING.get(obj2._meta.app_label)
        if db1 and db2:
            return db1 == db2
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):

        if db in settings.DATABASE_APPS_MAPPING.values():
            return settings.DATABASE_APPS_MAPPING.get(app_label) == db
    elif app_label in settings.DATABASE_APPS_MAPPING:
            return False

Here is the model and migrations of one of those apps:

# hunt.models.py

class Dish(models.Model):
    """
    Investigation case
    """
    display_name = models.CharField(max_length=64, unique=True)
    department = models.ForeignKey(Kitchen, null=True)
    case_type = models.PositiveSmallIntegerField(choices=CASE_TYPE_CHOICES, default=DEF_CASE_TYPE)
    created_at = models.DateTimeField(blank=True, null=True)
    comment = models.CharField(max_length=256, blank=True, null=True)

    class Meta:
        verbose_name = 'case'
        app_label = 'hunt'

    def __unicode__(self):
        return (u'%s (%s)' % (self.display_name, self.created_at)).strip()


# hunt.migrations.0001_initial.py

class Migration(migrations.Migration):

    app_label = 'hunt'

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Dish',
            fields=[
                ('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)),
                ('display_name', models.CharField(max_length=64, unique=True)),
                ('case_type', models.PositiveSmallIntegerField(default=0, choices=[(0, 'Unknown'), (1, 'General'), (2, 'Terror'), (3, 'Narco'), (4, 'Fraud'), (5, 'Slavery'), (6, 'Traffic'), (7, 'RICO'), (8, 'War'), (9, 'Cyber'), (20, 'Other')])),
                ('created_at', models.DateTimeField(null=True, blank=True)),
                ('comment', models.CharField(max_length=256, null=True, blank=True)),
            ],
            options={
                'verbose_name': 'case',
            },
        ),
    ]

# hunt.migrations.0002_add_hunts.py


def create_initial_hunts(apps, schema_editor):

    if settings.DEBUG:    
        print('\nContent added')


class Migration(migrations.Migration):
    dependencies = [
        ('hunt', '0001_initial'),
    ]


    operations = [
        migrations.RunPython(create_initial_hunts, hints={'schema_editor': 'other'}),
    ]

The problem is: When i run "migrate" command, only applications that connected to default database are migrated. The migrations in rest of the apps are never run. If I launch migrate for such an app with --database option - it works fine.

How can I specify the database per migration? Isn't the router supposed to manage exactly this? Or I missed something else?

Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102
Leon Kladnitsky
  • 393
  • 1
  • 3
  • 7

2 Answers2

38

You have to run migrate once for each database, specifying the target with --database. Each time it will consult your router to see which migrations to actually perform on that database.

I'm guessing it was designed this way to favor explicitness over implicitness. For example, your workflow might require you to migrate the different databases at different times.

Note, though, that you won't be able to tell from the output which migrations were actually performed, since:

If allow_migrate() returns False, any migration operations for the model_name will be silently skipped when running migrate on the db.

Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102
  • 1
    Thanks, I ended doing exactly this. – Leon Kladnitsky May 15 '16 at 18:45
  • 13
    Why Django don't support this multiple DBs issue, when they offers `DATABASE_ROUTERS`? This is strange. – dannydedog Aug 22 '17 at 11:54
  • Migration creates the same auth structure for the two databases. On the default side is the default Django tables and on the other side are the same default django tables more the tables in the chosen template. Is this the trick? – marcelo.delta Apr 06 '18 at 00:25
0

Using these nice helpers you can run Python/SQL migrations on specific Database

[Helpers]

from django.db.migrations import RunPython, RunSQL

def run_python_specific_db_migration(migration_func, use_db):
    """calls RunPython command only for specific database """
    return RunPython(migration_func, hints={'use_db': use_db})


def run_sql_specific_db_migration(sql_commands, use_db):
    """Runs one or list of sql commands only on specific database """
    return RunSQL(sql_commands, hints={'use_db': use_db})

# ** Your specific db_helpers for your DB_KEY **

def run_sql_your_db_migration(sql_commands):
    return run_sql_specific_db_migration(sql_commands, use_db=DB_KEY)

def run_python_your_db_migration(migration_func):
    return run_python_specific_db_migration(migration_func, use_db=DB_KEY)

[Usage]

def data_migration(apps, schema_editor):
    ...your data migration logic..better to wrap with @atomic...

class Migration(migrations.Migration):
    operations = [ run_python_your_db_migration(data_migration) ]

pymen
  • 5,737
  • 44
  • 35