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


95

我最近从Django 1.6切换到1.7,并且开始使用迁移功能(我从未使用过South)。

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

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

如果应用程序使用迁移,则不会自动加载固定装置。由于Django 2.0中的应用程序需要迁移,因此该行为被视为已弃用。如果要加载应用程序的初始数据,请考虑在数据迁移中进行。(https://docs.djangoproject.com/zh-CN/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures

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

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

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

我更喜欢第二种选择。

我不想使用South,因为Django现在似乎可以本地使用。


3
另外,我想在OP的原始问题中添加另一个问题:我们应该如何对不属于我们应用程序的数据进行数据迁移。例如,如果有人使用站点框架,则他需要对站点数据进行处理。由于站点框架与我们的应用程序无关,我们应该将数据迁移放在何处?谢谢 !
塞拉菲姆2014年

这里还没有任何人解决一个重要的问题,那就是当您需要将数据迁移中定义的数据添加到伪造了迁移的数据库上时会发生什么。由于迁移是伪造的,因此您的数据迁移将不会运行,您必须手动进行。此时,您也可以仅在Fixture文件上调用loaddata。
hekevintran

另一个有趣的情况是,如果您要迁移数据来创建auth.Group实例,然后又要创建一个新的Group作为种子数据,则会发生这种情况。您需要创建一个新的数据迁移。这可能很烦人,因为您的组种子数据将位于多个文件中。同样,如果您想重置迁移,则还必须浏览以查找设置种子数据并移植它们的数据迁移。
hekevintran,2015年

@Serafeim如果使用数据迁移而不是固定装置,则“在哪里放置第三方应用程序的初始数据”的问题不会改变,因为您仅更改了数据加载的方式。我使用小型自定义应用来处理此类问题。如果第三方应用程序称为“ foo”,则我将包含数据迁移/夹具的简单应用程序称为“ foo_integration”。
guettli 2015年

@guettli是的,可能使用额外的应用程序是最好的方法!
瑟拉芬2015年

Answers:


81

更新:有关此解决方案可能导致的问题,请参见下面的@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。最简单的:调用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') 

    如果您未指定app_label,loaddata会尝试fixture所有应用程序的夹具目录(您可能不想要)中加载文件名。

  3. 运行

    python manage.py migrate <yourapp>

1
好的,您是对的...此外,调用loaddata('loaddata', fixture_filename, app_label='<yourapp>')还将直接转到应用程序的夹具目录(因此无需构建夹具的完整路径)
n__o 2015年

15
使用该方法,序列化程序将在当前models.py文件的模型状态下工作,该文件可以具有一些额外的字段或其他更改。如果在创建迁移后进行了一些更改,它将失败(因此,我们甚至无法在该迁移之后创建架构迁移)。为了解决这个问题,我们可以将序列化程序正在处理的应用程序注册表更改为提供给第一个参数的迁移功能的注册表。路径的注册表位于django.core.serializers.python.apps
GwynBleidD

3
我们为什么这样做呢?为什么Django变得越来越难以运行和维护?我不想这么做,我想要一个简单的命令行界面为我解决这个问题,即像以前与夹具一起使用。Django应该使这些事情变得简单而不是困难:(
CpILL

1
@GwynBleidD这是您要提出的非常重要的观点,我认为它应该出现在此接受的答案中。它与文档数据迁移代码示例中的注释相同。您是否知道在app registry不更改全局变量的情况下使用提供的序列化程序的另一种方法(这可能在并行数据库迁移的假想未来中引起问题)。
广告N

3
我将这个答案与kazoo一起接受,这就是为什么我建议人们不要使用stackoverflow的原因。即使现在有了评论和轶事,我仍然在#django中提到了这一点。
shangxiao

50

精简版

您应使用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管理命令从固定装置加载数据,并且该数据迁移已应用于您的开发环境。

以后,您决定将新的必填字段添加到相应的模型中,这样就可以对更新后的模型进行新迁移(并可能在./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),
    ]

1
Rockallite,您的观点很明确。您的答案让我感到疑惑,但是@ n__o / @ mlissner的答案中的解决方案2.1是否会objects = serializers.deserialize('json', fixture, ignorenonexistent=True)遇到与以下相同的问题loaddata?还是ignorenonexistent=True涵盖所有可能的问题?
达里奥

7
如果您查看源代码,您会发现该ignorenonexistent=True参数有两个作用:1)忽略不包含在最新模型定义中的灯具模型,2)忽略不包含在灯具模型中的字段在最新的相应模型定义中。他们都无法处理模型中新要求的情况。因此,是的,我认为它遇到了与plain相同的问题loaddata
Rockallite

一旦我发现我的旧json的模型使用引用了其他模型natural_key(),该方法就很好用了,此方法似乎不支持该方法-我只是将natural_key值替换为所引用模型的实际ID。
dsummersl '16

1
将此答案作为公认的答案可能会更有帮助,因为在运行测试用例时,将创建一个新数据库,并且从头开始应用所有迁移。此解决方案解决了在数据迁移中不替换_get_model的情况下具有unittest的项目将面临的问题。Tnx
Mohammad ali baghershemirani '17

感谢您的更新和解释,@ Rockallite。我的最初答案是在Django 1.7中引入迁移后几周发布的,关于如何进行的文档尚不清楚(并且仍然是,上次我检查过)。希望Django有一天会更新其loaddata / migration机制以考虑模型历史。
n__o

6

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

使用Django的迁移夹具,你可以简单地运行下面的管理命令,它会通过所有搜索你INSTALLED_APPSinitial_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.

看到 安装/使用说明, django-migration-fixture


2

为了给您的数据库一些初始数据,编写一个数据迁移。 在数据迁移中,使用RunPython函数加载数据。

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

您的数据迁移将仅运行一次。迁移是迁移的有序序列。运行003_xxxx.py迁移时,django迁移会在数据库中写入该应用已迁移到该版本(003)的信息,并将仅运行以下迁移。


因此,您鼓励我myModel.create(...)在RunPython函数中重复调用(或使用循环)吗?
米克尔(Mickaël)2014年

是的。Transaactionnal数据库将完美地处理它:)
FlogFR 2014年

1

不幸的是,上面介绍的解决方案对我不起作用。我发现每次更改模型时都必须更新固定装置。理想情况下,我会写数据迁移来类似地修改创建的数据和夹具加载的数据。

为了方便起见,我编写了一个快速功能,它将在fixtures当前应用程序的目录中查找并加载夹具。将此功能放入与迁移中的字段匹配的模型历史记录中的迁移中。


谢谢你!我写了一个与Python 3兼容的版本(并通过了我们严格的Pylint)。您可以将其用作工厂RunPython(load_fixture('badger', 'stoat'))gist.github.com/danni/1b2a0078e998ac080111
丹妮尔·马德利

1

我认为固定装置有点不好。如果您的数据库经常更改,那么使其保持最新状态将很快成为噩梦。实际上,这不仅是我的观点,在《 Django的两个独家报道》一书中,它的解释要好得多。

相反,我将编写一个Python文件来提供初始设置。如果您还需要其他东西,我建议您去看看工厂男孩

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

还有关于使用固定装置的“燃烧固定装置,使用模型工厂”


1
我同意你的点“硬如果频繁的变化,以保持”安装项目的时候,但这里的灯具不仅旨在提供初始(和最小的)数据......
的Mickaël

1
这是一次加载数据,如果在迁移的上下文中完成加载,这是有意义的。因为如果它在迁移中,则不必更改json数据。任何需要更改数据的模式更改都应通过另一次迁移来处理(此时,数据库中的其他数据可能也需要修改)。
mtnpaul 2015年

0

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

但是我希望这种情况在执行初始迁移后立即自动发生。

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

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

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

为了更具描述性,这是包含sql/文件夹的应用程序的外观: 在此处输入图片说明

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

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

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

在此处输入图片说明

我创建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,但是由于我在使用django进行搜索时没有找到太多信息,因此仍想与大家共享此“操作方法”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.