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

时间:2014-09-21 15:37:14

标签: python json django migration 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

official documentation没有关于如何操作的明确示例,所以我的问题是:

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

  1. 通过多次调用mymodel.create(...)
  2. 编写Python代码
  3. 使用或编写Django函数(like calling loaddata)从JSON fixture文件加载数据。
  4. 我更喜欢第二种选择。

    我不想使用South,因为Django现在似乎能够原生地使用它。

7 个答案:

答案 0 :(得分:76)

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


假设您在<yourapp>/fixtures/initial_data.json

中有一个灯具文件
  1. 创建空迁移:

    在Django 1.7中:

    python manage.py makemigrations --empty <yourapp>
    

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

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
    
  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),
        ]
    

    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) 
    

    如果您想使用自定义目录,则非常有用。

    2.3。 最简单:使用loaddata调用app_label将自动加载<yourapp>的{​​{1}}目录中的灯具:

    fixtures

    如果您未指定from django.core.management import call_command fixture = 'initial_data' def load_fixture(apps, schema_editor): call_command('loaddata', fixture, app_label='yourapp') ,则loaddata将尝试从所有应用程序fixtures目录(您可能不想要)加载app_label文件名。

  3. 运行它

    fixture

答案 1 :(得分:41)

短版

您应该直接在数据迁移中使用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),
    ]

长版

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

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

稍后,您决定将新的 required 字段添加到相应的模型中,这样您就可以对新模型进行新的迁移(并可能为新模型提供一次性值) ./manage.py makemigrations提示你的字段。

您运行下一次迁移,一切都很顺利。

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

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

但即使您使用新字段的必需数据更新灯具,数据迁移仍然失败。数据迁移正在运行时,尚未应用将相应列添加到数据库的 next 迁移。您无法将数据保存到不存在的列中!

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

解决方案

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),
    ]

答案 2 :(得分:6)

受到一些评论的启发(即n__o&#39; s)以及我有很多initial_data.*个文件散布在多个应用程序中的事实我决定创建一个Django应用程序,这将有助于创建这些数据迁移。

使用django-migration-fixture,您只需运行以下管理命令,它就会搜索INSTALLED_APPS 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. 个文件并将其转换为数据迁移。

{{1}}

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

答案 3 :(得分:2)

为了向您的数据库提供一些初始数据,请写一个data migration. 在数据迁移中,使用RunPython函数加载数据。

不要编写任何loaddata命令,因为这种方式已被弃用。

您的数据迁移只会运行一次。迁移是有序的迁移序列。运行003_xxxx.py迁移时,django迁移会在数据库中写入此应用程序迁移到此应用程序(003),并且仅运行以下迁移。

答案 4 :(得分:1)

不幸的是,上面提出的解决方案对我不起作用。我发现每次更换模型时都要更新我的灯具。理想情况下,我会编写数据迁移来修改创建的数据和夹具加载的数据。

为了方便这个I wrote a quick function,它将在当前应用的fixtures目录中查看并加载一个灯具。将此函数放入模型历史记录中与迁移中的字段匹配的位置。

答案 5 :(得分:0)

在我看来,装置有点不好。如果您的数据库经常更改,那么让它们保持最新将很快成为一场噩梦。实际上,这不仅仅是我的观点,而是在书中&#34; Django的两个Scoops&#34;它的解释要好得多。

相反,我会编写一个Python文件来提供初始设置。如果您需要更多内容,我建议您查看Factory boy

如果您需要迁移某些数据,则应使用data migrations

还有"Burn Your Fixtures, Use Model Factories"关于使用灯具的问题。

答案 6 :(得分:0)

在Django 2.1上,我想用初始数据加载某些模型(例如国家名称)。

但是我希望这在执行初始迁移后自动发生。

因此,我认为在每个需要加载初始数据的应用程序中都有一个sql/文件夹会很棒。

然后在该sql/文件夹中,我将拥有.sql个文件,其中包含将初始数据加载到相应模型中所需的DML,例如:

INSERT INTO appName_modelName(fieldName)
VALUES
    ("country 1"),
    ("country 2"),
    ("country 3"),
    ("country 4");

为了更具描述性,这是包含sql/文件夹的应用程序的外观: enter image description here

我还发现有些情况下,我需要按特定顺序执行sql脚本。因此,我决定为文件名加上一个连续的数字,如上图所示。

然后,我需要一种通过执行SQLs自动加载任何应用程序文件夹中可用的python manage.py migrate的方法。

因此,我创建了另一个名为initial_data_migrations的应用程序,然后将该应用程序添加到INSTALLED_APPS文件中的settings.py列表中。然后,我在里面创建了一个migrations文件夹,并添加了一个名为run_sql_scripts.py的文件(实际上是自定义迁移)。如下图所示:

enter image description here

我创建了run_sql_scripts.py,以便它负责运行每个应用程序中所有可用的sql脚本。然后,当有人运行python manage.py migrate时会触发此事件。此自定义migration还将所涉及的应用程序添加为依赖项,这样,只有在所需的应用程序执行了它们的sql迁移之后,它才会尝试运行0001_initial.py语句(我们不想尝试对不存在的表运行一条SQL语句。

以下是该脚本的来源:

import os
import itertools

from django.db import migrations
from YourDjangoProjectName.settings import BASE_DIR, INSTALLED_APPS

SQL_FOLDER = "/sql/"

APP_SQL_FOLDERS = [
    (os.path.join(BASE_DIR, app + SQL_FOLDER), app) for app in INSTALLED_APPS
    if os.path.isdir(os.path.join(BASE_DIR, app + SQL_FOLDER))
]

SQL_FILES = [
    sorted([path + file for file in os.listdir(path) if file.lower().endswith('.sql')])
    for path, app in APP_SQL_FOLDERS
]


def load_file(path):
    with open(path, 'r') as f:
        return f.read()


class Migration(migrations.Migration):

    dependencies = [
        (app, '__first__') for path, app in APP_SQL_FOLDERS
    ]

    operations = [
        migrations.RunSQL(load_file(f)) for f in list(itertools.chain.from_iterable(SQL_FILES))
    ]

我希望有人能对此有所帮助,对我来说效果很好!如果您有任何疑问,请告诉我。

注意:这可能不是最好的解决方案,因为我刚刚开始使用django,但是由于我没有找到太多信息,因此仍想与大家分享此“操作方法”在谷歌搜索时。