Fixed #27666 -- Delayed rendering of recursivly related models in migration operations.

This commit is contained in:
Markus Holtermann 2016-11-06 12:03:05 +01:00 committed by Tim Graham
parent 7d2db2a7b8
commit 45ded053b1
7 changed files with 92 additions and 15 deletions

View File

@ -203,6 +203,9 @@ class Command(BaseCommand):
targets, plan=plan, state=pre_migrate_state.clone(), fake=fake, targets, plan=plan, state=pre_migrate_state.clone(), fake=fake,
fake_initial=fake_initial, fake_initial=fake_initial,
) )
# post_migrate signals have access to all models. Ensure that all models
# are reloaded in case any are delayed.
post_migrate_state.clear_delayed_apps_cache()
post_migrate_apps = post_migrate_state.apps post_migrate_apps = post_migrate_state.apps
# Re-render models of real apps to include relationships now that # Re-render models of real apps to include relationships now that

View File

@ -70,7 +70,9 @@ class AddField(FieldOperation):
else: else:
field = self.field field = self.field
state.models[app_label, self.model_name_lower].fields.append((self.name, field)) state.models[app_label, self.model_name_lower].fields.append((self.name, field))
state.reload_model(app_label, self.model_name_lower) # Delay rendering of relationships if it's not a relational field
delay = not field.is_relation
state.reload_model(app_label, self.model_name_lower, delay=delay)
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)
@ -135,11 +137,16 @@ class RemoveField(FieldOperation):
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
new_fields = [] new_fields = []
old_field = None
for name, instance in state.models[app_label, self.model_name_lower].fields: for name, instance in state.models[app_label, self.model_name_lower].fields:
if name != self.name: if name != self.name:
new_fields.append((name, instance)) new_fields.append((name, instance))
else:
old_field = instance
state.models[app_label, self.model_name_lower].fields = new_fields state.models[app_label, self.model_name_lower].fields = new_fields
state.reload_model(app_label, self.model_name_lower) # Delay rendering of relationships if it's not a relational field
delay = not old_field.is_relation
state.reload_model(app_label, self.model_name_lower, delay=delay)
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)
@ -191,7 +198,11 @@ class AlterField(FieldOperation):
for n, f in for n, f in
state.models[app_label, self.model_name_lower].fields state.models[app_label, self.model_name_lower].fields
] ]
state.reload_model(app_label, self.model_name_lower) # TODO: investigate if old relational fields must be reloaded or if it's
# sufficient if the new field is (#27737).
# Delay rendering of relationships if it's not a relational field
delay = not field.is_relation
state.reload_model(app_label, self.model_name_lower, delay=delay)
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)
@ -270,7 +281,13 @@ class RenameField(FieldOperation):
[self.new_name if n == self.old_name else n for n in together] [self.new_name if n == self.old_name else n for n in together]
for together in options[option] for together in options[option]
] ]
state.reload_model(app_label, self.model_name_lower) for n, f in state.models[app_label, self.model_name_lower].fields:
if n == self.new_name:
field = f
break
# Delay rendering of relationships if it's not a relational field
delay = not field.is_relation
state.reload_model(app_label, self.model_name_lower, delay=delay)
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)

View File

@ -331,10 +331,10 @@ class RenameModel(ModelOperation):
model_state.fields[index] = name, changed_field model_state.fields[index] = name, changed_field
model_changed = True model_changed = True
if model_changed: if model_changed:
state.reload_model(model_app_label, model_name) state.reload_model(model_app_label, model_name, delay=True)
# Remove the old model. # Remove the old model.
state.remove_model(app_label, self.old_name_lower) state.remove_model(app_label, self.old_name_lower)
state.reload_model(app_label, self.new_name_lower) state.reload_model(app_label, self.new_name_lower, delay=True)
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)
@ -444,7 +444,7 @@ class AlterModelTable(ModelOperation):
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
state.models[app_label, self.name_lower].options["db_table"] = self.table state.models[app_label, self.name_lower].options["db_table"] = self.table
state.reload_model(app_label, self.name_lower) state.reload_model(app_label, self.name_lower, delay=True)
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)
@ -521,7 +521,7 @@ class AlterUniqueTogether(FieldRelatedOptionOperation):
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
model_state = state.models[app_label, self.name_lower] model_state = state.models[app_label, self.name_lower]
model_state.options[self.option_name] = self.unique_together model_state.options[self.option_name] = self.unique_together
state.reload_model(app_label, self.name_lower) state.reload_model(app_label, self.name_lower, delay=True)
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)
@ -575,7 +575,7 @@ class AlterIndexTogether(FieldRelatedOptionOperation):
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
model_state = state.models[app_label, self.name_lower] model_state = state.models[app_label, self.name_lower]
model_state.options[self.option_name] = self.index_together model_state.options[self.option_name] = self.index_together
state.reload_model(app_label, self.name_lower) state.reload_model(app_label, self.name_lower, delay=True)
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)
@ -626,7 +626,7 @@ class AlterOrderWithRespectTo(FieldRelatedOptionOperation):
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
model_state = state.models[app_label, self.name_lower] model_state = state.models[app_label, self.name_lower]
model_state.options['order_with_respect_to'] = self.order_with_respect_to model_state.options['order_with_respect_to'] = self.order_with_respect_to
state.reload_model(app_label, self.name_lower) state.reload_model(app_label, self.name_lower, delay=True)
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)
@ -705,7 +705,7 @@ class AlterModelOptions(ModelOptionOperation):
for key in self.ALTER_OPTION_KEYS: for key in self.ALTER_OPTION_KEYS:
if key not in self.options and key in model_state.options: if key not in self.options and key in model_state.options:
del model_state.options[key] del model_state.options[key]
state.reload_model(app_label, self.name_lower) state.reload_model(app_label, self.name_lower, delay=True)
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
pass pass
@ -738,7 +738,7 @@ class AlterModelManagers(ModelOptionOperation):
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
model_state = state.models[app_label, self.name_lower] model_state = state.models[app_label, self.name_lower]
model_state.managers = list(self.managers) model_state.managers = list(self.managers)
state.reload_model(app_label, self.name_lower) state.reload_model(app_label, self.name_lower, delay=True)
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
pass pass

View File

@ -181,6 +181,9 @@ 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):
# RunPython has access to all models. Ensure that all models are
# reloaded in case any are delayed.
from_state.clear_delayed_apps_cache()
if router.allow_migrate(schema_editor.connection.alias, app_label, **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.

View File

@ -52,6 +52,17 @@ def _get_related_models(m):
return related_models return related_models
def get_related_models_tuples(model):
"""
Return a list of typical (app_label, model_name) tuples for all related
models for the given model.
"""
return {
(rel_mod._meta.app_label, rel_mod._meta.model_name)
for rel_mod in _get_related_models(model)
}
def get_related_models_recursive(model): def get_related_models_recursive(model):
""" """
Return all models that have a direct or indirect relationship Return all models that have a direct or indirect relationship
@ -85,6 +96,7 @@ class ProjectState(object):
self.models = models or {} self.models = models or {}
# Apps to include from main registry, usually unmigrated ones # Apps to include from main registry, usually unmigrated ones
self.real_apps = real_apps or [] self.real_apps = real_apps or []
self.is_delayed = False
def add_model(self, model_state): def add_model(self, model_state):
app_label, model_name = model_state.app_label, model_state.name_lower app_label, model_name = model_state.app_label, model_state.name_lower
@ -100,7 +112,10 @@ class ProjectState(object):
# the cache automatically (#24513) # the cache automatically (#24513)
self.apps.clear_cache() self.apps.clear_cache()
def reload_model(self, app_label, model_name): def reload_model(self, app_label, model_name, delay=False):
if delay:
self.is_delayed = True
if 'apps' in self.__dict__: # hasattr would cache the property if 'apps' in self.__dict__: # hasattr would cache the property
try: try:
old_model = self.apps.get_model(app_label, model_name) old_model = self.apps.get_model(app_label, model_name)
@ -109,6 +124,9 @@ class ProjectState(object):
else: else:
# Get all relations to and from the old model before reloading, # Get all relations to and from the old model before reloading,
# as _meta.apps may change # as _meta.apps may change
if delay:
related_models = get_related_models_tuples(old_model)
else:
related_models = get_related_models_recursive(old_model) related_models = get_related_models_recursive(old_model)
# Get all outgoing references from the model to be rendered # Get all outgoing references from the model to be rendered
@ -130,6 +148,9 @@ class ProjectState(object):
rel_model = self.apps.get_model(rel_app_label, rel_model_name) rel_model = self.apps.get_model(rel_app_label, rel_model_name)
except LookupError: except LookupError:
pass pass
else:
if delay:
related_models.update(get_related_models_tuples(rel_model))
else: else:
related_models.update(get_related_models_recursive(rel_model)) related_models.update(get_related_models_recursive(rel_model))
@ -169,8 +190,13 @@ class ProjectState(object):
) )
if 'apps' in self.__dict__: if 'apps' in self.__dict__:
new_state.apps = self.apps.clone() new_state.apps = self.apps.clone()
new_state.is_delayed = self.is_delayed
return new_state return new_state
def clear_delayed_apps_cache(self):
if self.is_delayed and 'apps' in self.__dict__:
del self.__dict__['apps']
@cached_property @cached_property
def apps(self): def apps(self):
return StateApps(self.real_apps, self.models) return StateApps(self.real_apps, self.models)

View File

@ -421,6 +421,8 @@ It accepts two list of operations, and when asked to apply state will use the
state list, and when asked to apply changes to the database will use the database state list, and when asked to apply changes to the database will use the database
list. Do not use this operation unless you're very sure you know what you're doing. list. Do not use this operation unless you're very sure you know what you're doing.
.. _writing-your-own-migration-operation:
Writing your own Writing your own
================ ================
@ -480,6 +482,21 @@ Some things to note:
to them; these just represent the difference the ``state_forwards`` method to them; these just represent the difference the ``state_forwards`` method
would have applied, but are given to you for convenience and speed reasons. would have applied, but are given to you for convenience and speed reasons.
* If you want to work with model classes or model instances from the
``from_state`` argument in ``database_forwards()`` or
``database_backwards()``, you must render model states using the
``clear_delayed_apps_cache()`` method to make related models available::
def database_forwards(self, app_label, schema_editor, from_state, to_state):
# This operation should have access to all models. Ensure that all models are
# reloaded in case any are delayed.
from_state.clear_delayed_apps_cache()
...
.. versionadded:: 1.11
This requirement and the ``clear_delayed_apps_cache()`` method is new.
* ``to_state`` in the database_backwards method is the *older* state; that is, * ``to_state`` in the database_backwards method is the *older* state; that is,
the one that will be the current state once the migration has finished reversing. the one that will be the current state once the migration has finished reversing.

View File

@ -593,6 +593,17 @@ must receive a dictionary of context rather than ``Context`` or
dictionary instead -- doing so is backwards-compatible with older versions of dictionary instead -- doing so is backwards-compatible with older versions of
Django. Django.
Model state changes in migration operations
-------------------------------------------
To improve the speed of applying migrations, rendering of related models is
delayed until an operation that needs them (e.g. ``RunPython``). If you have a
custom operation that works with model classes or model instances from the
``from_state`` argument in ``database_forwards()`` or ``database_backwards()``,
you must render model states using the ``clear_delayed_apps_cache()`` method as
described in :ref:`writing your own migration operation
<writing-your-own-migration-operation>`.
Miscellaneous Miscellaneous
------------- -------------