使用Django 1.7加载初始数据和数据迁移

Mic*_*aël 91 python migration django json data-migration

我最近从Django 1.6切换到1.7,我开始使用迁移(我从未使用过South).

在1.7之前,我曾经用一个fixture/initial_data.json文件加载初始数据,该文件是用python manage.py syncdb命令加载的(创建数据库时).

现在,我开始使用迁移,并且不推荐使用此行为:

如果应用程序使用迁移,则不会自动加载灯具.由于Django 2.0中的应用程序将需要迁移,因此不推荐使用此行为.如果要加载应用程序的初始数据,请考虑在数据迁移中执行此操作.(https://docs.djangoproject.com/en/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures)

官方文件并没有对如何做一个明显的例子,所以我的问题是:

使用数据迁移导入此类初始数据的最佳方法是什么:

  1. 通过多次调用编写Python代码mymodel.create(...),
  2. 使用或编写Django函数(如调用loaddata)从JSON fixture文件加载数据.

我更喜欢第二种选择.

我不想使用South,因为Django现在似乎可以原生地使用它.

n__*_*__o 80

更新:请参阅下面的@ GwynBleidD关于此解决方案可能导致的问题的评论,并参阅下面@ Rockallite的答案,了解对未来模型更改更持久的方法.


假设你有一个夹具文件 <yourapp>/fixtures/initial_data.json

  1. 创建空迁移:

    在Django 1.7中:

    python manage.py makemigrations --empty <yourapp>
    
    Run Code Online (Sandbox Code Playgroud)

    在Django 1.8+中,您可以提供一个名称:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
    
    Run Code Online (Sandbox Code Playgroud)
  2. 编辑您的迁移文件 <yourapp>/migrations/0002_auto_xxx.py

    2.1.自定义实现,灵感来自Django' loaddata(初步答案):

    import os
    from sys import path
    from django.core import serializers
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
    
        fixture = open(fixture_file, 'rb')
        objects = serializers.deserialize('json', fixture, ignorenonexistent=True)
        for obj in objects:
            obj.save()
        fixture.close()
    
    def unload_fixture(apps, schema_editor):
        "Brutally deleting all entries for this model..."
    
        MyModel = apps.get_model("yourapp", "ModelName")
        MyModel.objects.all().delete()
    
    class Migration(migrations.Migration):  
    
        dependencies = [
            ('yourapp', '0001_initial'),
        ]
    
        operations = [
            migrations.RunPython(load_fixture, reverse_code=unload_fixture),
        ]
    
    Run Code Online (Sandbox Code Playgroud)

    2.2.一个更简单的解决方案load_fixture(根据@juliocesar的建议):

    from django.core.management import call_command
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
        call_command('loaddata', fixture_file) 
    
    Run Code Online (Sandbox Code Playgroud)

    如果要使用自定义目录,则很有用.

    2.3.最简单的:调用loaddataapp_label将负载从灯具<yourapp>fixtures自动目录:

    from django.core.management import call_command
    
    fixture = 'initial_data'
    
    def load_fixture(apps, schema_editor):
        call_command('loaddata', fixture, app_label='yourapp') 
    
    Run Code Online (Sandbox Code Playgroud)

    如果您未指定app_label,loaddata将尝试fixture所有应用程序fixtures目录(您可能不需要)加载文件名.

  3. 运行

    python manage.py migrate <yourapp>
    
    Run Code Online (Sandbox Code Playgroud)

  • 使用该方法,序列化程序将处理来自当前`models.py`文件的模型状态,这些文件可能有一些额外的字段或一些其他更改.如果在创建迁移后进行了一些更改,则会失败(因此我们甚至无法在迁移后创建模式迁移).为了解决这个问题,我们可以将序列化程序正在处理的应用程序注册表更改为第一个参数上提供给迁移功能的注册表.Registry to path位于`django.core.serializers.python.apps`. (15认同)
  • 我们为什么这样做?为什么Django越来越难以运行和维护?我不想这样做,我想要一个简单的命令行界面,为我解决这个问题,就像它曾经与灯具一样.Django应该让这些东西更容易,而不是更难:( (3认同)
  • 这个答案被赞成kazoo以及接受正是我建议人们不要使用stackoverflow.即使现在有了评论和轶事,我仍然会在#django中有人提到这个. (2认同)

Roc*_*ite 44

精简版

您应使用loaddata的数据迁移直接管理命令.

# 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),
    ]
Run Code Online (Sandbox Code Playgroud)

长版

loaddata利用django.core.serializers.python.Deserializer最新的模型来对迁移中的历史数据进行反序列化.这是不正确的行为.

例如,假设存在利用loaddata管理命令从夹具加载数据的数据迁移,并且它已经应用于您的开发环境.

稍后,您决定向相应的模型添加新的必填字段,因此您可以对新模型进行新的迁移(并在./manage.py makemigrations提示时为新字段提供一次性值).

你运行下一次迁移,一切都很顺利.

最后,您已经完成了Django应用程序的开发,并将其部署在生产服务器上.现在是时候在生产环境中从头开始运行整个迁移了.

但是,数据迁移失败.这是因为loaddata命令的反序列化模型(代表当前代码)无法与您添加的新必填字段的空数据一起保存.原始夹具缺少必要的数据!

但即使您使用新字段所需的数据更新夹具,数据迁移仍会失败.数据迁移正在运行时,尚未应用将相应列添加到数据库的下一次迁移.您无法将数据保存到不存在的列!

结论:在数据迁移中,该loaddata命令会在模型和数据库之间引入潜在的不一致.绝对应该在数据迁移中直接使用它.

解决方案

loaddata命令依赖于django.core.serializers.python._get_model函数从夹具中获取相应的模型,这将返回最新版本的模型.我们需要对其进行修补,以便获得历史模型.

(以下代码适用于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),
    ]
Run Code Online (Sandbox Code Playgroud)

  • 如果你看一下[来源](https://github.com/django/django/blob/stable/1.8.x/django/core/serializers/python.py#L91),你会发现`ignorenonexistent = True`参数有两个效果:**1)**它忽略了不在最新模型定义中的夹具模型,**2)**它忽略了夹具模型的字段,而不是在最新的相应模型定义.他们都没有处理_new-required-field-in-model_情况.所以,是的,我认为它遇到了与普通`loaddata`相同的问题. (6认同)
  • Rockallite,你的观点非常有力。不过,你的回答让我想知道,@n__o/@mlissner 的答案中的解决方案 2.1 依赖于 `objects = serializers.deserialize('json', Fixture,ignorenonexistent=True)` 是否会遇到与 `loaddata` 相同的问题?或者 `ignorenonexistent=True` 是否涵盖了所有可能的问题? (2认同)

ale*_*yes 6

受到一些评论(即n__o)的启发以及我在很多initial_data.*应用程序中分布了大量文件的事实,我决定创建一个Django应用程序,以便于创建这些数据迁移.

使用django-migration-fixture,您只需运行以下管理命令,它将搜索所有INSTALLED_APPSfor initial_data.*文件并将其转换为数据迁移.

./manage.py create_initial_data_fixtures
Migrations for 'eggs':
  0002_auto_20150107_0817.py:
Migrations for 'sausage':
  Ignoring 'initial_data.yaml' - migration already exists.
Migrations for 'foo':
  Ignoring 'initial_data.yaml' - not migrated.
Run Code Online (Sandbox Code Playgroud)

有关安装/使用说明,请参阅django-migration-fixture.