Assume a Django application which is supposed to use two MySQL databases:
default
- for storing data represented by models A
and B
(read-write access)support
- for importing data represented by models C
and D
(read-only access)The support
database is a part of an external application and cannot be modified.
Since the Django application uses the built-in ORM for models A
and B
I figured it should use the very same ORM for models C
and D
, even though they map to tables in an external database (support
.)
In order to achieve that I defined the models C
and D
as follows:
from django.db import models
class ExternalModel(models.Model):
class Meta:
managed = False
abstract = True
class ModelC(ExternalModel):
some_field = models.TextField(db_column='some_field')
class Meta(ExternalModel.Meta):
db_table = 'some_table_c'
class ModelD(ExternalModel):
some_other_field = models.TextField(db_column='some_other_field')
class Meta(ExternalModel.Meta):
db_table = 'some_table_d'
Then I defined a database router:
from myapp.myapp.models import ExternalModel
class DatabaseRouter(object):
def db_for_read(self, model, **hints):
if issubclass(model, ExternalModel):
return 'support'
return 'default'
def db_for_write(self, model, **hints):
if issubclass(model, ExternalModel):
return None
return 'default'
def allow_relation(self, obj1, obj2, **hints):
return (isinstance(obj1, ExternalModel) == isinstance(obj2, ExternalModel))
def allow_migrate(self, db, app_label, model_name=None, **hints):
return (db == 'default')
And finally adjusted settings.py
:
# (...)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'OPTIONS': {
'read_default_file': os.path.join(BASE_DIR, 'resources', 'default.cnf'),
},
},
'support': {
'ENGINE': 'django.db.backends.mysql',
'OPTIONS': {
'read_default_file': os.path.join(BASE_DIR, 'resources', 'support.cnf'),
},
},
}
DATABASE_ROUTERS = ['myapp.database_router.DatabaseRouter']
# (...)
The user specified in support.conf
for the support
database has been assigned read-only privileges.
But when I run python manage.py makemigrations
it fails with the following output:
Traceback (most recent call last):
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/backends/utils.py", line 62, in execute
return self.cursor.execute(sql)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/backends/mysql/base.py", line 112, in execute
return self.cursor.execute(query, args)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/cursors.py", line 226, in execute
self.errorhandler(self, exc, value)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler
raise errorvalue
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/cursors.py", line 217, in execute
res = self._query(query)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/cursors.py", line 378, in _query
rowcount = self._do_query(q)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/cursors.py", line 341, in _do_query
db.query(q)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/connections.py", line 280, in query
_mysql.connection.query(self, query)
_mysql_exceptions.OperationalError: (1142, "CREATE command denied to user 'somedbuser'@'somehost' for table 'django_migrations'")
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/migrations/recorder.py", line 57, in ensure_schema
editor.create_model(self.Migration)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/backends/base/schema.py", line 295, in create_model
self.execute(sql, params or None)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/backends/base/schema.py", line 112, in execute
cursor.execute(sql, params)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/backends/utils.py", line 79, in execute
return super(CursorDebugWrapper, self).execute(sql, params)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/backends/utils.py", line 64, in execute
return self.cursor.execute(sql, params)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/utils.py", line 94, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/utils/six.py", line 685, in reraise
raise value.with_traceback(tb)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/backends/utils.py", line 62, in execute
return self.cursor.execute(sql)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/backends/mysql/base.py", line 112, in execute
return self.cursor.execute(query, args)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/cursors.py", line 226, in execute
self.errorhandler(self, exc, value)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler
raise errorvalue
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/cursors.py", line 217, in execute
res = self._query(query)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/cursors.py", line 378, in _query
rowcount = self._do_query(q)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/cursors.py", line 341, in _do_query
db.query(q)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/MySQLdb/connections.py", line 280, in query
_mysql.connection.query(self, query)
django.db.utils.OperationalError: (1142, "CREATE command denied to user 'somedbuser'@'somehost' for table 'django_migrations'")
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "manage.py", line 22, in <module>
execute_from_command_line(sys.argv)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/core/management/__init__.py", line 367, in execute_from_command_line
utility.execute()
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/core/management/__init__.py", line 359, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/core/management/base.py", line 305, in run_from_argv
self.execute(*args, **cmd_options)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/core/management/base.py", line 356, in execute
output = self.handle(*args, **options)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/core/management/commands/makemigrations.py", line 100, in handle
loader.check_consistent_history(connection)
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/migrations/loader.py", line 276, in check_consistent_history
applied = recorder.applied_migrations()
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/migrations/recorder.py", line 65, in applied_migrations
self.ensure_schema()
File "/Users/username/Development/stuff/myapp/lib/python3.5/site-packages/django/db/migrations/recorder.py", line 59, in ensure_schema
raise MigrationSchemaMissing("Unable to create the django_migrations table (%s)" % exc)
django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1142, "CREATE command denied to user 'somedbuser'@'somehost' for table 'django_migrations'"))
It appears that Django tries to create the django_migrations
table in the read-only database support
nevertheless.
Is there any clean way to prevent the migrations mechanism from attempting that? Or do I have to employ another ORM library for this read-only access to the support
database?
Django's admin doesn't have any explicit support for multiple databases. If you want to provide an admin interface for a model on a database other than that specified by your router chain, you'll need to write custom ModelAdmin classes that will direct the admin to use a specific database for content.
Instead, django-read-only uses always installed database instrumentation to inspect executed queries and only allow those which look like reads. It uses a “fail closed” philosophy, so anything unknown will fail, which should be fairly reasonable.
I encountered the same issue (using Django 1.11) and this question was at the top of my Google results for it.
Your initial solution is only missing one critical piece. You need to tell Django what database models 'C' and 'D' are using. What worked for me:
class ExternalModel(models.Model):
class Meta:
managed = False
abstract = True
app_label = 'support'
Then tell your database router how to behave when it encounters that app_label in the allow_migrate() section:
def allow_migrate(self, db, app_label, model_name=None, **hints):
if app_label == 'support':
return False
return (db == 'default')
I'm not sure that is the most-correct-solution in the eyes of the Django team, but effect is allow_migrate() returning False for any models defined with that app_label attribute value.
The Django documentation on routers doesn't mention this explicitly (or, at least with model code samples that make it clear how the ORM passes the value for 'db' to allow_migrate()), but between the 'app_label' and 'managed' attributes you can get it to work*.
* In my case the default is postgres and the read-only database is Oracle 12 via cx_Oracle.
It seems around the Django 1.10.1 timeframe, Tim Graham (the primary Django maintainer), accepted a patch that suppressed this specific exception but later withdrew the patch in favor of (roughly) the following method to work around this issue and to support read-only databases using the Django ORM.
Define a database router as described in the Django documentation on routers I've attached an example router below that routes to a different database based on an 'app' flag in the model meta.
In your routers allow_migrations method, return False for any db argument that corresponds to a read-only database. This prevents the migration of the model tables regardless of where they would be routed to.
This next part is a little weird but where the rubber hits the road and actually answers the original question. To keep makemigrations from attempting to create the django_migrations table in your read-only database, the database traffic should not be routed. In the example router, that means 'read_only' is not in DATABASE_APPS_MAPPING.
So, instead, Read-only databases are accessed explicitly with "using" (e.g. MyReadOnlyModel.objects.using('read_only').all()
Django database apps router
Had the same problem. Django is trying to create the 'django_migrations' table in all DBs. This happens even if there are no models associated with the read-only DB and all routers are pointing a different DB.
I also ended up using peewee.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With