Fixed #24351, #24346 -- Changed the signature of allow_migrate().

The new signature enables better support for routing RunPython and
RunSQL operations, especially w.r.t. reusable and third-party apps.

This commit also takes advantage of the deprecation cycle for the old
signature to remove the backward incompatibility introduced in #22583;
RunPython and RunSQL won't call allow_migrate() when when the router
has the old signature.

Thanks Aymeric Augustin and Tim Graham for helping shape up the patch.

Refs 22583.
This commit is contained in:
Loic Bistuer 2015-02-19 14:27:58 +07:00
parent dd0b487872
commit bed504d70b
26 changed files with 221 additions and 117 deletions

View File

@ -66,7 +66,7 @@ def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_
except LookupError: except LookupError:
return return
if not router.allow_migrate(using, Permission): if not router.allow_migrate_model(using, Permission):
return return
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType

View File

@ -17,7 +17,7 @@ def update_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT
except LookupError: except LookupError:
return return
if not router.allow_migrate(using, ContentType): if not router.allow_migrate_model(using, ContentType):
return return
ContentType.objects.clear_cache() ContentType.objects.clear_cache()

View File

@ -33,6 +33,7 @@ class Migration(migrations.Migration):
migrations.RunPython( migrations.RunPython(
migrations.RunPython.noop, migrations.RunPython.noop,
add_legacy_name, add_legacy_name,
hints={'model_name': 'contenttype'},
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='contenttype', model_name='contenttype',

View File

@ -14,7 +14,7 @@ def create_default_site(app_config, verbosity=2, interactive=True, using=DEFAULT
except LookupError: except LookupError:
return return
if not router.allow_migrate(using, Site): if not router.allow_migrate_model(using, Site):
return return
if not Site.objects.using(using).exists(): if not Site.objects.using(using).exists():

View File

@ -30,6 +30,7 @@ class Options(object):
self.abstract = False self.abstract = False
self.managed = True self.managed = True
self.proxy = False self.proxy = False
self.swapped = False
class BaseDatabaseCache(BaseCache): class BaseDatabaseCache(BaseCache):

View File

@ -38,7 +38,7 @@ class Command(BaseCommand):
def create_table(self, database, tablename): def create_table(self, database, tablename):
cache = BaseDatabaseCache(tablename, {}) cache = BaseDatabaseCache(tablename, {})
if not router.allow_migrate(database, cache.cache_model_class): if not router.allow_migrate_model(database, cache.cache_model_class):
return return
connection = connections[database] connection = connections[database]

View File

@ -132,7 +132,7 @@ class Command(BaseCommand):
for model in serializers.sort_dependencies(app_list.items()): for model in serializers.sort_dependencies(app_list.items()):
if model in excluded_models: if model in excluded_models:
continue continue
if not model._meta.proxy and router.allow_migrate(using, model): if not model._meta.proxy and router.allow_migrate_model(using, model):
if use_base_manager: if use_base_manager:
objects = model._base_manager objects = model._base_manager
else: else:

View File

@ -139,7 +139,7 @@ class Command(BaseCommand):
for obj in objects: for obj in objects:
objects_in_fixture += 1 objects_in_fixture += 1
if router.allow_migrate(self.using, obj.object.__class__): if router.allow_migrate_model(self.using, obj.object.__class__):
loaded_objects_in_fixture += 1 loaded_objects_in_fixture += 1
self.models.add(obj.object.__class__) self.models.add(obj.object.__class__)
try: try:

View File

@ -107,7 +107,7 @@ class BaseDatabaseCreation(object):
def get_objects(): def get_objects():
for model in serializers.sort_dependencies(app_list): for model in serializers.sort_dependencies(app_list):
if (not model._meta.proxy and model._meta.managed and if (not model._meta.proxy and model._meta.managed and
router.allow_migrate(self.connection.alias, model)): router.allow_migrate_model(self.connection.alias, model)):
queryset = model._default_manager.using(self.connection.alias).order_by(model._meta.pk.name) queryset = model._default_manager.using(self.connection.alias).order_by(model._meta.pk.name)
for obj in queryset.iterator(): for obj in queryset.iterator():
yield obj yield obj

View File

@ -99,15 +99,17 @@ class Operation(object):
""" """
return self.references_model(model_name, app_label) return self.references_model(model_name, app_label)
def allowed_to_migrate(self, connection_alias, model, hints=None): def allow_migrate_model(self, connection_alias, model):
""" """
Returns if we're allowed to migrate the model. Returns if we're allowed to migrate the model.
This is a thin wrapper around router.allow_migrate_model() that
preemptively rejects any proxy, swapped out, or unmanaged model.
""" """
# Always skip if proxy, swapped out, or unmanaged. if model._meta.proxy or model._meta.swapped or not model._meta.managed:
if model and (model._meta.proxy or model._meta.swapped or not model._meta.managed):
return False return False
return router.allow_migrate(connection_alias, model, **(hints or {})) return router.allow_migrate_model(connection_alias, model)
def __repr__(self): def __repr__(self):
return "<%s %s%s>" % ( return "<%s %s%s>" % (

View File

@ -52,7 +52,7 @@ class AddField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
to_model = to_state.apps.get_model(app_label, self.model_name) to_model = to_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allow_migrate_model(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
field = to_model._meta.get_field(self.name) field = to_model._meta.get_field(self.name)
if not self.preserve_default: if not self.preserve_default:
@ -66,7 +66,7 @@ class AddField(Operation):
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, from_model): if self.allow_migrate_model(schema_editor.connection.alias, from_model):
schema_editor.remove_field(from_model, from_model._meta.get_field(self.name)) schema_editor.remove_field(from_model, from_model._meta.get_field(self.name))
def describe(self): def describe(self):
@ -117,12 +117,12 @@ class RemoveField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, from_model): if self.allow_migrate_model(schema_editor.connection.alias, from_model):
schema_editor.remove_field(from_model, from_model._meta.get_field(self.name)) schema_editor.remove_field(from_model, from_model._meta.get_field(self.name))
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
to_model = to_state.apps.get_model(app_label, self.model_name) to_model = to_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allow_migrate_model(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
schema_editor.add_field(from_model, to_model._meta.get_field(self.name)) schema_editor.add_field(from_model, to_model._meta.get_field(self.name))
@ -184,7 +184,7 @@ class AlterField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
to_model = to_state.apps.get_model(app_label, self.model_name) to_model = to_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allow_migrate_model(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
from_field = from_model._meta.get_field(self.name) from_field = from_model._meta.get_field(self.name)
to_field = to_model._meta.get_field(self.name) to_field = to_model._meta.get_field(self.name)
@ -267,7 +267,7 @@ class RenameField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
to_model = to_state.apps.get_model(app_label, self.model_name) to_model = to_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allow_migrate_model(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
schema_editor.alter_field( schema_editor.alter_field(
from_model, from_model,
@ -277,7 +277,7 @@ class RenameField(Operation):
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
to_model = to_state.apps.get_model(app_label, self.model_name) to_model = to_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allow_migrate_model(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
schema_editor.alter_field( schema_editor.alter_field(
from_model, from_model,

View File

@ -55,12 +55,12 @@ class CreateModel(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
model = to_state.apps.get_model(app_label, self.name) model = to_state.apps.get_model(app_label, self.name)
if self.allowed_to_migrate(schema_editor.connection.alias, model): if self.allow_migrate_model(schema_editor.connection.alias, model):
schema_editor.create_model(model) schema_editor.create_model(model)
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
model = from_state.apps.get_model(app_label, self.name) model = from_state.apps.get_model(app_label, self.name)
if self.allowed_to_migrate(schema_editor.connection.alias, model): if self.allow_migrate_model(schema_editor.connection.alias, model):
schema_editor.delete_model(model) schema_editor.delete_model(model)
def describe(self): def describe(self):
@ -111,12 +111,12 @@ class DeleteModel(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
model = from_state.apps.get_model(app_label, self.name) model = from_state.apps.get_model(app_label, self.name)
if self.allowed_to_migrate(schema_editor.connection.alias, model): if self.allow_migrate_model(schema_editor.connection.alias, model):
schema_editor.delete_model(model) schema_editor.delete_model(model)
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
model = to_state.apps.get_model(app_label, self.name) model = to_state.apps.get_model(app_label, self.name)
if self.allowed_to_migrate(schema_editor.connection.alias, model): if self.allow_migrate_model(schema_editor.connection.alias, model):
schema_editor.create_model(model) schema_editor.create_model(model)
def references_model(self, name, app_label=None): def references_model(self, name, app_label=None):
@ -189,7 +189,7 @@ class RenameModel(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
new_model = to_state.apps.get_model(app_label, self.new_name) new_model = to_state.apps.get_model(app_label, self.new_name)
if self.allowed_to_migrate(schema_editor.connection.alias, new_model): if self.allow_migrate_model(schema_editor.connection.alias, new_model):
old_model = from_state.apps.get_model(app_label, self.old_name) old_model = from_state.apps.get_model(app_label, self.old_name)
# Move the main table # Move the main table
schema_editor.alter_db_table( schema_editor.alter_db_table(
@ -287,7 +287,7 @@ class AlterModelTable(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
new_model = to_state.apps.get_model(app_label, self.name) new_model = to_state.apps.get_model(app_label, self.name)
if self.allowed_to_migrate(schema_editor.connection.alias, new_model): if self.allow_migrate_model(schema_editor.connection.alias, new_model):
old_model = from_state.apps.get_model(app_label, self.name) old_model = from_state.apps.get_model(app_label, self.name)
schema_editor.alter_db_table( schema_editor.alter_db_table(
new_model, new_model,
@ -347,7 +347,7 @@ class AlterUniqueTogether(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
new_model = to_state.apps.get_model(app_label, self.name) new_model = to_state.apps.get_model(app_label, self.name)
if self.allowed_to_migrate(schema_editor.connection.alias, new_model): if self.allow_migrate_model(schema_editor.connection.alias, new_model):
old_model = from_state.apps.get_model(app_label, self.name) old_model = from_state.apps.get_model(app_label, self.name)
schema_editor.alter_unique_together( schema_editor.alter_unique_together(
new_model, new_model,
@ -399,7 +399,7 @@ class AlterIndexTogether(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
new_model = to_state.apps.get_model(app_label, self.name) new_model = to_state.apps.get_model(app_label, self.name)
if self.allowed_to_migrate(schema_editor.connection.alias, new_model): if self.allow_migrate_model(schema_editor.connection.alias, new_model):
old_model = from_state.apps.get_model(app_label, self.name) old_model = from_state.apps.get_model(app_label, self.name)
schema_editor.alter_index_together( schema_editor.alter_index_together(
new_model, new_model,
@ -448,7 +448,7 @@ class AlterOrderWithRespectTo(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
to_model = to_state.apps.get_model(app_label, self.name) to_model = to_state.apps.get_model(app_label, self.name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allow_migrate_model(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.name) from_model = from_state.apps.get_model(app_label, self.name)
# Remove a field if we need to # Remove a field if we need to
if from_model._meta.order_with_respect_to and not to_model._meta.order_with_respect_to: if from_model._meta.order_with_respect_to and not to_model._meta.order_with_respect_to:

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import router
from .base import Operation from .base import Operation
@ -94,13 +96,13 @@ class RunSQL(Operation):
state_operation.state_forwards(app_label, state) state_operation.state_forwards(app_label, state)
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
if self.allowed_to_migrate(schema_editor.connection.alias, None, hints=self.hints): if router.allow_migrate(schema_editor.connection.alias, app_label, **self.hints):
self._run_sql(schema_editor, self.sql) self._run_sql(schema_editor, self.sql)
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
if self.reverse_sql is None: if self.reverse_sql is None:
raise NotImplementedError("You cannot reverse this operation") raise NotImplementedError("You cannot reverse this operation")
if self.allowed_to_migrate(schema_editor.connection.alias, None, hints=self.hints): if router.allow_migrate(schema_editor.connection.alias, app_label, **self.hints):
self._run_sql(schema_editor, self.reverse_sql) self._run_sql(schema_editor, self.reverse_sql)
def describe(self): def describe(self):
@ -171,7 +173,7 @@ class RunPython(Operation):
pass pass
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
if self.allowed_to_migrate(schema_editor.connection.alias, None, hints=self.hints): if router.allow_migrate(schema_editor.connection.alias, app_label, **self.hints):
# We now execute the Python code in a context that contains a 'models' # We now execute the Python code in a context that contains a 'models'
# object, representing the versioned models as an app registry. # object, representing the versioned models as an app registry.
# We could try to override the global cache, but then people will still # We could try to override the global cache, but then people will still
@ -181,7 +183,7 @@ class RunPython(Operation):
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
if self.reverse_code is None: if self.reverse_code is None:
raise NotImplementedError("You cannot reverse this operation") raise NotImplementedError("You cannot reverse this operation")
if self.allowed_to_migrate(schema_editor.connection.alias, None, hints=self.hints): if router.allow_migrate(schema_editor.connection.alias, app_label, **self.hints):
self.reverse_code(from_state.apps, schema_editor) self.reverse_code(from_state.apps, schema_editor)
def describe(self): def describe(self):

View File

@ -1549,7 +1549,7 @@ class Model(six.with_metaclass(ModelBase)):
# Find the minimum max allowed length among all specified db_aliases. # Find the minimum max allowed length among all specified db_aliases.
for db in settings.DATABASES.keys(): for db in settings.DATABASES.keys():
# skip databases where the model won't be created # skip databases where the model won't be created
if not router.allow_migrate(db, cls): if not router.allow_migrate_model(db, cls):
continue continue
connection = connections[db] connection = connections[db]
max_name_length = connection.ops.max_name_length() max_name_length = connection.ops.max_name_length()

View File

@ -1,5 +1,7 @@
import inspect
import os import os
import pkgutil import pkgutil
import warnings
from importlib import import_module from importlib import import_module
from threading import local from threading import local
@ -7,6 +9,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils import six from django.utils import six
from django.utils._os import upath from django.utils._os import upath
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
@ -276,22 +279,42 @@ class ConnectionRouter(object):
return allow return allow
return obj1._state.db == obj2._state.db return obj1._state.db == obj2._state.db
def allow_migrate(self, db, model, **hints): def allow_migrate(self, db, app_label, **hints):
for router in self.routers: for router in self.routers:
try: try:
method = router.allow_migrate method = router.allow_migrate
except AttributeError: except AttributeError:
# If the router doesn't have a method, skip to the next one. # If the router doesn't have a method, skip to the next one.
pass continue
argspec = inspect.getargspec(router.allow_migrate)
if len(argspec.args) == 3 and not argspec.keywords:
warnings.warn(
"The signature of allow_migrate has changed from "
"allow_migrate(self, db, model) to "
"allow_migrate(self, db, app_label, model_name=None, **hints). "
"Support for the old signature will be removed in Django 2.0.",
RemovedInDjango20Warning)
model = hints.get('model')
allow = None if model is None else method(db, model)
else: else:
allow = method(db, model, **hints) allow = method(db, app_label, **hints)
if allow is not None: if allow is not None:
return allow return allow
return True return True
def allow_migrate_model(self, db, model):
return self.allow_migrate(
db,
model._meta.app_label,
model_name=model._meta.model_name,
model=model,
)
def get_migratable_models(self, app_config, db, include_auto_created=False): def get_migratable_models(self, app_config, db, include_auto_created=False):
""" """
Return app models allowed to be synchronized on provided db. Return app models allowed to be synchronized on provided db.
""" """
models = app_config.get_models(include_auto_created=include_auto_created) models = app_config.get_models(include_auto_created=include_auto_created)
return [model for model in models if self.allow_migrate(db, model)] return [model for model in models if self.allow_migrate_model(db, model)]

View File

@ -46,7 +46,7 @@ method of database routers as ``**hints``:
class MyRouter(object): class MyRouter(object):
def allow_migrate(self, db, model, **hints): def allow_migrate(self, db, app_label, model_name=None, **hints):
if 'target_db' in hints: if 'target_db' in hints:
return db == hints['target_db'] return db == hints['target_db']
return True return True
@ -68,6 +68,10 @@ Then, to leverage this in your migrations, do the following::
migrations.RunPython(forwards, hints={'target_db': 'default'}), migrations.RunPython(forwards, hints={'target_db': 'default'}),
] ]
If your ``RunPython`` or ``RunSQL`` operation only affects one model, it's good
practice to pass ``model_name`` as a hint to make it as transparent as possible
to the router. This is especially important for reusable and third-party apps.
Migrations that add unique fields Migrations that add unique fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -183,6 +183,10 @@ details on these changes.
* Ability to specify ``ContentType.name`` when creating a content type instance * Ability to specify ``ContentType.name`` when creating a content type instance
will be removed. will be removed.
* Support for the old signature of ``allow_migrate`` will be removed. It changed
from ``allow_migrate(self, db, model)`` to
``allow_migrate(self, db, app_label, model_name=None, **hints)``.
.. _deprecation-removed-in-1.9: .. _deprecation-removed-in-1.9:
1.9 1.9

View File

@ -480,11 +480,13 @@ Migrations
method/attribute were added to ease in making ``RunPython`` and ``RunSQL`` method/attribute were added to ease in making ``RunPython`` and ``RunSQL``
operations reversible. operations reversible.
* The :class:`~django.db.migrations.operations.RunPython` and * The migration operations :class:`~django.db.migrations.operations.RunPython`
:class:`~django.db.migrations.operations.RunSQL` operations now accept a and :class:`~django.db.migrations.operations.RunSQL` now call the
``hints`` parameter that will be passed to :meth:`allow_migrate`. To take :meth:`allow_migrate` method of database routers. The router can use the
advantage of this feature you must ensure that the ``allow_migrate()`` method newly introduced ``app_label`` and ``hints`` arguments to make a routing
of all your routers accept ``**hints``. decision. To take advantage of this feature you need to update the router to
the new ``allow_migrate`` signature, see the :ref:`deprecation section
<deprecated-signature-of-allow-migrate>` for more details.
Models Models
^^^^^^ ^^^^^^
@ -1146,14 +1148,6 @@ Miscellaneous
* :func:`django.utils.translation.get_language()` now returns ``None`` instead * :func:`django.utils.translation.get_language()` now returns ``None`` instead
of :setting:`LANGUAGE_CODE` when translations are temporarily deactivated. of :setting:`LANGUAGE_CODE` when translations are temporarily deactivated.
* The migration operations :class:`~django.db.migrations.operations.RunPython`
and :class:`~django.db.migrations.operations.RunSQL` now call the
:meth:`allow_migrate` method of database routers. In these cases the
``model`` argument of ``allow_migrate()`` is set to ``None``, so the router
must properly handle this value. This is most useful when used together with
the newly introduced ``hints`` parameter for these operations, but it can
also be used to disable migrations from running on a particular database.
* The ``name`` field of :class:`django.contrib.contenttypes.models.ContentType` * The ``name`` field of :class:`django.contrib.contenttypes.models.ContentType`
has been removed by a migration and replaced by a property. That means it's has been removed by a migration and replaced by a property. That means it's
not possible to query or filter a ``ContentType`` by this field any longer. not possible to query or filter a ``ContentType`` by this field any longer.
@ -1651,6 +1645,23 @@ aggregate methods are deprecated and should be replaced by their function-based
aggregate equivalents (``Collect``, ``Extent``, ``Extent3D``, ``MakeLine``, and aggregate equivalents (``Collect``, ``Extent``, ``Extent3D``, ``MakeLine``, and
``Union``). ``Union``).
.. _deprecated-signature-of-allow-migrate:
Signature of the ``allow_migrate`` router method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The signature of the :meth:`allow_migrate` method of database routers has
changed from ``allow_migrate(db, model)`` to
``allow_migrate(db, app_label, model_name=None, **hints)``.
When ``model_name`` is set, the value that was previously given through the
``model`` positional argument may now be found inside the ``hints`` dictionary
under the key ``'model'``.
After switching to the new signature the router will also be called by the
:class:`~django.db.migrations.operations.RunPython` and
:class:`~django.db.migrations.operations.RunSQL` operations.
.. removed-features-1.8: .. removed-features-1.8:
Features removed in 1.8 Features removed in 1.8

View File

@ -226,19 +226,19 @@ operations to ``cache_replica``, and all write operations to
def db_for_read(self, model, **hints): def db_for_read(self, model, **hints):
"All cache read operations go to the replica" "All cache read operations go to the replica"
if model._meta.app_label in ('django_cache',): if model._meta.app_label == 'django_cache':
return 'cache_replica' return 'cache_replica'
return None return None
def db_for_write(self, model, **hints): def db_for_write(self, model, **hints):
"All cache write operations go to primary" "All cache write operations go to primary"
if model._meta.app_label in ('django_cache',): if model._meta.app_label == 'django_cache':
return 'cache_primary' return 'cache_primary'
return None return None
def allow_migrate(self, db, model): def allow_migrate(self, db, app_label, model_name=None, **hints):
"Only install the cache model on primary" "Only install the cache model on primary"
if model._meta.app_label in ('django_cache',): if app_label == 'django_cache':
return db == 'cache_primary' return db == 'cache_primary'
return None return None

View File

@ -128,7 +128,7 @@ A database Router is a class that provides up to four methods:
provided in the ``hints`` dictionary. Details on valid hints are provided in the ``hints`` dictionary. Details on valid hints are
provided :ref:`below <topics-db-multi-db-hints>`. provided :ref:`below <topics-db-multi-db-hints>`.
Returns None if there is no suggestion. Returns ``None`` if there is no suggestion.
.. method:: db_for_write(model, **hints) .. method:: db_for_write(model, **hints)
@ -140,32 +140,53 @@ A database Router is a class that provides up to four methods:
provided in the ``hints`` dictionary. Details on valid hints are provided in the ``hints`` dictionary. Details on valid hints are
provided :ref:`below <topics-db-multi-db-hints>`. provided :ref:`below <topics-db-multi-db-hints>`.
Returns None if there is no suggestion. Returns ``None`` if there is no suggestion.
.. method:: allow_relation(obj1, obj2, **hints) .. method:: allow_relation(obj1, obj2, **hints)
Return True if a relation between obj1 and obj2 should be Return ``True`` if a relation between ``obj1`` and ``obj2`` should be
allowed, False if the relation should be prevented, or None if allowed, ``False`` if the relation should be prevented, or ``None`` if
the router has no opinion. This is purely a validation operation, the router has no opinion. This is purely a validation operation,
used by foreign key and many to many operations to determine if a used by foreign key and many to many operations to determine if a
relation should be allowed between two objects. relation should be allowed between two objects.
.. method:: allow_migrate(db, model, **hints) .. method:: allow_migrate(db, app_label, model_name=None, **hints)
Determine if the ``model`` should have tables/indexes created in the Determine if the migration operation is allowed to run on the database with
database with alias ``db``. Return True if the model should be alias ``db``. Return ``True`` if the operation should run, ``False`` if it
migrated, False if it should not be migrated, or None if shouldn't run, or ``None`` if the router has no opinion.
the router has no opinion. This method can be used to determine
the availability of a model on a given database.
Note that migrations will just silently not perform any operations The ``app_label`` positional argument is the label of the application
on a model for which this returns ``False``. This may result in broken being migrated.
ForeignKeys, extra tables or missing tables if you change it once you
have applied some migrations.
The value passed for ``model`` may be a ``model_name`` is set by most migration operations to the value of
:ref:`historical model <historical-models>`, and thus not have any ``model._meta.model_name`` (the lowercased version of the model
custom attributes, methods or managers. You should only rely on ``_meta``. ``__name__``) of the model being migrated. Its value is ``None`` for the
:class:`~django.db.migrations.operations.RunPython` and
:class:`~django.db.migrations.operations.RunSQL` operations unless they
provide it using hints.
``hints`` are used by certain operations to communicate additional
information to the router.
When ``model_name`` is set, ``hints`` normally contains the model class
under the key ``'model'``. Note that it may be a :ref:`historical model
<historical-models>`, and thus not have any custom attributes, methods, or
managers. You should only rely on ``_meta``.
This method can also be used to determine the availability of a model on a
given database.
Note that migrations will just silently not perform any operations on a
model for which this returns ``False``. This may result in broken foreign
keys, extra tables, or missing tables if you change it once you have
applied some migrations.
.. versionchanged:: 1.8
The signature of ``allow_migrate`` has changed significantly from previous
versions. See the :ref:`deprecation notes
<deprecated-signature-of-allow-migrate>` for more details.
A router doesn't have to provide *all* these methods -- it may omit one A router doesn't have to provide *all* these methods -- it may omit one
or more of them. If one of the methods is omitted, Django will skip or more of them. If one of the methods is omitted, Django will skip
@ -293,15 +314,13 @@ send queries for the ``auth`` app to ``auth_db``::
return True return True
return None return None
def allow_migrate(self, db, model, **hints): def allow_migrate(self, db, app_label, model, **hints):
""" """
Make sure the auth app only appears in the 'auth_db' Make sure the auth app only appears in the 'auth_db'
database. database.
""" """
if db == 'auth_db': if app_label == 'auth':
return model._meta.app_label == 'auth' return db == 'auth_db'
elif model._meta.app_label == 'auth':
return False
return None return None
And we also want a router that sends all other apps to the And we also want a router that sends all other apps to the
@ -333,7 +352,7 @@ from::
return True return True
return None return None
def allow_migrate(self, db, model, **hints): def allow_migrate(self, db, app_label, model, **hints):
""" """
All non-auth models end up in this pool. All non-auth models end up in this pool.
""" """

View File

@ -956,14 +956,17 @@ class DBCacheRouter(object):
def db_for_read(self, model, **hints): def db_for_read(self, model, **hints):
if model._meta.app_label == 'django_cache': if model._meta.app_label == 'django_cache':
return 'other' return 'other'
return None
def db_for_write(self, model, **hints): def db_for_write(self, model, **hints):
if model._meta.app_label == 'django_cache': if model._meta.app_label == 'django_cache':
return 'other' return 'other'
return None
def allow_migrate(self, db, model): def allow_migrate(self, db, app_label, **hints):
if model._meta.app_label == 'django_cache': if app_label == 'django_cache':
return db == 'other' return db == 'other'
return None
@override_settings( @override_settings(

View File

@ -322,7 +322,7 @@ class OtherRouter(object):
def allow_relation(self, obj1, obj2, **hints): def allow_relation(self, obj1, obj2, **hints):
return None return None
def allow_migrate(self, db, model): def allow_migrate(self, db, app_label, **hints):
return True return True

View File

@ -16,7 +16,7 @@ class AgnosticRouter(object):
""" """
A router that doesn't have an opinion regarding migrating. A router that doesn't have an opinion regarding migrating.
""" """
def allow_migrate(self, db, model, **hints): def allow_migrate(self, db, app_label, **hints):
return None return None
@ -24,7 +24,7 @@ class MigrateNothingRouter(object):
""" """
A router that doesn't allow migrating. A router that doesn't allow migrating.
""" """
def allow_migrate(self, db, model, **hints): def allow_migrate(self, db, app_label, **hints):
return False return False
@ -32,7 +32,7 @@ class MigrateEverythingRouter(object):
""" """
A router that always allows migrating. A router that always allows migrating.
""" """
def allow_migrate(self, db, model, **hints): def allow_migrate(self, db, app_label, **hints):
return True return True
@ -40,7 +40,7 @@ class MigrateWhenFooRouter(object):
""" """
A router that allows migrating depending on a hint. A router that allows migrating depending on a hint.
""" """
def allow_migrate(self, db, model, **hints): def allow_migrate(self, db, app_label, **hints):
return hints.get('foo', False) return hints.get('foo', False)

View File

@ -4,8 +4,11 @@ from django.db import DEFAULT_DB_ALIAS
class TestRouter(object): class TestRouter(object):
# A test router. The behavior is vaguely primary/replica, but the """
# databases aren't assumed to propagate changes. Vaguely behave like primary/replica, but the databases aren't assumed to
propagate changes.
"""
def db_for_read(self, model, instance=None, **hints): def db_for_read(self, model, instance=None, **hints):
if instance: if instance:
return instance._state.db or 'other' return instance._state.db or 'other'
@ -17,13 +20,14 @@ class TestRouter(object):
def allow_relation(self, obj1, obj2, **hints): def allow_relation(self, obj1, obj2, **hints):
return obj1._state.db in ('default', 'other') and obj2._state.db in ('default', 'other') return obj1._state.db in ('default', 'other') and obj2._state.db in ('default', 'other')
def allow_migrate(self, db, model): def allow_migrate(self, db, app_label, **hints):
return True return True
class AuthRouter(object): class AuthRouter(object):
"""A router to control all database operations on models in """
the contrib.auth application""" Control all database operations on models in the contrib.auth application.
"""
def db_for_read(self, model, **hints): def db_for_read(self, model, **hints):
"Point all read operations on auth models to 'default'" "Point all read operations on auth models to 'default'"
@ -45,12 +49,10 @@ class AuthRouter(object):
return True return True
return None return None
def allow_migrate(self, db, model): def allow_migrate(self, db, app_label, **hints):
"Make sure the auth app only appears on the 'other' db" "Make sure the auth app only appears on the 'other' db"
if db == 'other': if app_label == 'auth':
return model._meta.app_label == 'auth' return db == 'other'
elif model._meta.app_label == 'auth':
return False
return None return None

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
import datetime import datetime
import pickle import pickle
import warnings
from operator import attrgetter from operator import attrgetter
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -11,6 +12,7 @@ from django.db import DEFAULT_DB_ALIAS, connections, router, transaction
from django.db.models import signals from django.db.models import signals
from django.db.utils import ConnectionRouter from django.db.utils import ConnectionRouter
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils.encoding import force_text
from django.utils.six import StringIO from django.utils.six import StringIO
from .models import Book, Person, Pet, Review, UserProfile from .models import Book, Person, Pet, Review, UserProfile
@ -901,28 +903,58 @@ class RouterTestCase(TestCase):
def test_migrate_selection(self): def test_migrate_selection(self):
"Synchronization behavior is predictable" "Synchronization behavior is predictable"
self.assertTrue(router.allow_migrate('default', User)) self.assertTrue(router.allow_migrate_model('default', User))
self.assertTrue(router.allow_migrate('default', Book)) self.assertTrue(router.allow_migrate_model('default', Book))
self.assertTrue(router.allow_migrate('other', User)) self.assertTrue(router.allow_migrate_model('other', User))
self.assertTrue(router.allow_migrate('other', Book)) self.assertTrue(router.allow_migrate_model('other', Book))
with override_settings(DATABASE_ROUTERS=[TestRouter(), AuthRouter()]): with override_settings(DATABASE_ROUTERS=[TestRouter(), AuthRouter()]):
# Add the auth router to the chain. TestRouter is a universal # Add the auth router to the chain. TestRouter is a universal
# synchronizer, so it should have no effect. # synchronizer, so it should have no effect.
self.assertTrue(router.allow_migrate('default', User)) self.assertTrue(router.allow_migrate_model('default', User))
self.assertTrue(router.allow_migrate('default', Book)) self.assertTrue(router.allow_migrate_model('default', Book))
self.assertTrue(router.allow_migrate('other', User)) self.assertTrue(router.allow_migrate_model('other', User))
self.assertTrue(router.allow_migrate('other', Book)) self.assertTrue(router.allow_migrate_model('other', Book))
with override_settings(DATABASE_ROUTERS=[AuthRouter(), TestRouter()]): with override_settings(DATABASE_ROUTERS=[AuthRouter(), TestRouter()]):
# Now check what happens if the router order is reversed. # Now check what happens if the router order is reversed.
self.assertFalse(router.allow_migrate('default', User)) self.assertFalse(router.allow_migrate_model('default', User))
self.assertTrue(router.allow_migrate('default', Book)) self.assertTrue(router.allow_migrate_model('default', Book))
self.assertTrue(router.allow_migrate('other', User)) self.assertTrue(router.allow_migrate_model('other', User))
self.assertFalse(router.allow_migrate('other', Book)) self.assertTrue(router.allow_migrate_model('other', Book))
def test_migrate_legacy_router(self):
class LegacyRouter(object):
def allow_migrate(self, db, model):
"""
Deprecated allow_migrate signature should trigger
RemovedInDjango20Warning.
"""
assert db == 'default'
assert model is User
return True
with override_settings(DATABASE_ROUTERS=[LegacyRouter()]):
with warnings.catch_warnings(record=True) as recorded:
warnings.filterwarnings('always')
msg = (
"The signature of allow_migrate has changed from "
"allow_migrate(self, db, model) to "
"allow_migrate(self, db, app_label, model_name=None, **hints). "
"Support for the old signature will be removed in Django 2.0."
)
self.assertTrue(router.allow_migrate_model('default', User))
self.assertEqual(force_text(recorded.pop().message), msg)
self.assertEqual(recorded, [])
self.assertTrue(router.allow_migrate('default', 'app_label'))
self.assertEqual(force_text(recorded.pop().message), msg)
def test_partial_router(self): def test_partial_router(self):
"A router can choose to implement a subset of methods" "A router can choose to implement a subset of methods"
@ -939,8 +971,8 @@ class RouterTestCase(TestCase):
self.assertTrue(router.allow_relation(dive, dive)) self.assertTrue(router.allow_relation(dive, dive))
self.assertTrue(router.allow_migrate('default', User)) self.assertTrue(router.allow_migrate_model('default', User))
self.assertTrue(router.allow_migrate('default', Book)) self.assertTrue(router.allow_migrate_model('default', Book))
with override_settings(DATABASE_ROUTERS=[WriteRouter(), AuthRouter(), TestRouter()]): with override_settings(DATABASE_ROUTERS=[WriteRouter(), AuthRouter(), TestRouter()]):
self.assertEqual(router.db_for_read(User), 'default') self.assertEqual(router.db_for_read(User), 'default')
@ -951,8 +983,8 @@ class RouterTestCase(TestCase):
self.assertTrue(router.allow_relation(dive, dive)) self.assertTrue(router.allow_relation(dive, dive))
self.assertFalse(router.allow_migrate('default', User)) self.assertFalse(router.allow_migrate_model('default', User))
self.assertTrue(router.allow_migrate('default', Book)) self.assertTrue(router.allow_migrate_model('default', Book))
def test_database_routing(self): def test_database_routing(self):
marty = Person.objects.using('default').create(name="Marty Alchin") marty = Person.objects.using('default').create(name="Marty Alchin")
@ -1492,11 +1524,11 @@ class AntiPetRouter(object):
# A router that only expresses an opinion on migrate, # A router that only expresses an opinion on migrate,
# passing pets to the 'other' database # passing pets to the 'other' database
def allow_migrate(self, db, model): def allow_migrate(self, db, app_label, model_name, **hints):
if db == 'other': if db == 'other':
return model._meta.object_name == 'Pet' return model_name == 'pet'
else: else:
return model._meta.object_name != 'Pet' return model_name != 'pet'
class FixtureTestCase(TestCase): class FixtureTestCase(TestCase):
@ -1757,7 +1789,7 @@ class RouterModelArgumentTestCase(TestCase):
class SyncOnlyDefaultDatabaseRouter(object): class SyncOnlyDefaultDatabaseRouter(object):
def allow_migrate(self, db, model): def allow_migrate(self, db, app_label, **hints):
return db == DEFAULT_DB_ALIAS return db == DEFAULT_DB_ALIAS

View File

@ -137,7 +137,7 @@ class SitesFrameworkTests(TestCase):
class JustOtherRouter(object): class JustOtherRouter(object):
def allow_migrate(self, db, model): def allow_migrate(self, db, app_label, **hints):
return db == 'other' return db == 'other'