Loading initial data with Django 1.7 and data migrations

前端 未结 7 1943
陌清茗
陌清茗 2020-11-27 09:39

I recently switched from Django 1.6 to 1.7, and I began using migrations (I never used South).

Before 1.7, I used to load initial data with a fixture/initial_d

7条回答
  •  小蘑菇
    小蘑菇 (楼主)
    2020-11-27 10:24

    Short version

    You should NOT use loaddata management command directly in a data migration.

    # Bad example for a data migration
    from django.db import migrations
    from django.core.management import call_command
    
    
    def load_fixture(apps, schema_editor):
        # No, it's wrong. DON'T DO THIS!
        call_command('loaddata', 'your_data.json', app_label='yourapp')
    
    
    class Migration(migrations.Migration):
        dependencies = [
            # Dependencies to other migrations
        ]
    
        operations = [
            migrations.RunPython(load_fixture),
        ]
    

    Long version

    loaddata utilizes django.core.serializers.python.Deserializer which uses the most up-to-date models to deserialize historical data in a migration. That's incorrect behavior.

    For example, supposed that there is a data migration which utilizes loaddata management command to load data from a fixture, and it's already applied on your development environment.

    Later, you decide to add a new required field to the corresponding model, so you do it and make a new migration against your updated model (and possibly provide a one-off value to the new field when ./manage.py makemigrations prompts you).

    You run the next migration, and all is well.

    Finally, you're done developing your Django application, and you deploy it on the production server. Now it's time for you to run the whole migrations from scratch on the production environment.

    However, the data migration fails. That's because the deserialized model from loaddata command, which represents the current code, can't be saved with empty data for the new required field you added. The original fixture lacks necessary data for it!

    But even if you update the fixture with required data for the new field, the data migration still fails. When the data migration is running, the next migration which adds the corresponding column to the database, is not applied yet. You can't save data to a column which does not exist!

    Conclusion: in a data migration, the loaddata command introduces potential inconsistency between the model and the database. You should definitely NOT use it directly in a data migration.

    The Solution

    loaddata command relies on django.core.serializers.python._get_model function to get the corresponding model from a fixture, which will return the most up-to-date version of a model. We need to monkey-patch it so it gets the historical model.

    (The following code works for Django 1.8.x)

    # Good example for a data migration
    from django.db import migrations
    from django.core.serializers import base, python
    from django.core.management import call_command
    
    
    def load_fixture(apps, schema_editor):
        # Save the old _get_model() function
        old_get_model = python._get_model
    
        # Define new _get_model() function here, which utilizes the apps argument to
        # get the historical version of a model. This piece of code is directly stolen
        # from django.core.serializers.python._get_model, unchanged. However, here it
        # has a different context, specifically, the apps variable.
        def _get_model(model_identifier):
            try:
                return apps.get_model(model_identifier)
            except (LookupError, TypeError):
                raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)
    
        # Replace the _get_model() function on the module, so loaddata can utilize it.
        python._get_model = _get_model
    
        try:
            # Call loaddata command
            call_command('loaddata', 'your_data.json', app_label='yourapp')
        finally:
            # Restore old _get_model() function
            python._get_model = old_get_model
    
    
    class Migration(migrations.Migration):
        dependencies = [
            # Dependencies to other migrations
        ]
    
        operations = [
            migrations.RunPython(load_fixture),
        ]
    

提交回复
热议问题