Search code examples
pythondjangodjango-modelsmultiple-databasesdjango-database

Django prevent migrations on remote database


I have a Django app that connects to two databases. One of my DBs is on a different (production) server. I want to make very sure that in developing my app, I don't accidentally migrate models on the production server. My understanding is this: Suppose my settings.py DATABASES is this:

DATABASES = {
    'default': {},
    'remote_db': {
        'NAME'    : 'important_remote_db_name',
        'ENGINE'  : 'django.db.backends.mysql',
        'USER'    : 'someuser',
        'PASSWORD': 'somepass',
        'HOST'    : 'some.production.host.com',
        'PORT'    : '3306',
    },
    'myapp_db': {
        'NAME'    : 'my_app_db_name',
        'ENGINE'  : 'django.db.backends.mysql',
        'USER'    : 'localuser',
        'PASSWORD': 'localpassword'
    }
}

Now suppose I also have a router class called RemoteDBRouter. Like all routers, that class will have a method allow_migrate. Note that the remote DB uses Django auth models, so the User model will have app_label 'auth', and the remote DB also has its own models with app_label 'remoteapp'. With that info, I see two possibilities for the allow_migrate method:

#OPTION 1
def allow_migrate(self, db, app_label, model_name=None, **hints):
    if app_label == 'auth' or app_label == 'remoteapp':
        return db == 'remote_db'
    return None

#OPTION 2
def allow_migrate(self, db, app_label, model_name=None, **hints):
    if app_label == 'auth' or app_label == 'remoteapp':
        return False
    return None

Which one should I use? Option 2 is easier in that it just says that I should not migrate any models where the app_label says the models are from the remote DB. Option 1 does an additional check to see if the db field is 'remote_db' (I assume that we need to check 'remote_db' and not 'remote_db_name'). Does it matter which one I use? I'm concerned that if I use Option 1 and the db check fails, the method will return None and then the Django code will check the next router class where allow_migrate may return True.


Solution

  • You want the RemoteDBRouter to be authoritative for remote_db database. You don't want to control its migrations from the local machine neither for auth and remoteapp or other apps. Other databases are not necessarily controlled by RemoteDBRouter. Therefore you start by:

        if db == 'remote_db':
            return False
    

    The question is if you want sometimes to switch auth and remoteapp to local when you are developing write operation or if you expect only read only access and those tables need never to be created locally. Then you can add:

        if app_label == 'auth' or app_label == 'remoteapp':
            return False
    

    Migrations for other databases can be controlled by a default router or other routers:

        return None
    

    It is more complicated with tests to create a test database locally.

    DATABASES = {
        'remote_db': {
            ...
            'HOST': 'some.production.host.com',
            'USER': 'some_readonly_user',   # read-only for security
            'TEST': {
                'HOST': 'localhost',
                ...
            }
        }
    }
    

    Optionally you can support read-only access to the remote db also by a router rule:

    def db_for_write(model, **hints):
        if model._meta.app_label in ('auth', 'remoteapp'):
            return 'myapp_db'  # or maybe the db 'default' where the model doesn't exist
    

    An exception is better than to harm production data if you are writing by mistake. The router can be overridden by many ways, e.g. by using=db parameter or .using(db) method. To be safe, the connection should be by a read only user.