首页 文章

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

提问于
浏览
89

我最近从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没有关于如何做到这一点的明确例子,所以我的问题是:

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

  • 编写多次调用 mymodel.create(...) 的Python代码,

  • 使用或编写Django函数(like calling loaddata)从JSON fixture文件加载数据 .

我更喜欢第二种选择 .

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

8 回答

  • 2

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

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

  • 4

    更新:请参阅下面的@GwynBleidD 's comment below for the problems this solution can cause, and see @Rockallite'答案,以获得对未来模型更改更持久的方法 .


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

    • 创建空迁移:

    在Django 1.7中:

    python manage.py makemigrations --empty <yourapp>
    

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

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
    
    • 编辑您的迁移文件 <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 . Simplest: 使用 app_label 调用 loaddata 将自动加载 <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将尝试从 all apps fixtures目录(您可能不需要)加载 fixture filename .

    • 运行它
    python manage.py migrate <yourapp>
    
  • 0

    短版

    您应该 NOT 直接在数据迁移中使用 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应用程序的开发,并将其部署在 生产环境 服务器上 . 现在是时候在 生产环境 环境中从头开始运行整个迁移了 .

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

    但即使您使用新字段的必需数据更新夹具, the data migration still fails . 数据迁移正在运行时,尚未应用将相应列添加到数据库的下一次迁移 . 您无法将数据保存到不存在的列!

    _255416__在数据迁移中, 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),
        ]
    
  • 76

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

    使用django-migration-fixture,您只需运行以下管理命令,它将搜索所有 INSTALLED_APPS for 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.
    

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

  • 38

    该在迁移的应用程序中加载初始数据的最佳方法是通过数据迁移(也在文档中推荐) . 优点是,在测试和 生产环境 过程中,夹具都被加载 .

    @n__o建议在迁移中重新实现 loaddata 命令 . 但是,在我的测试中,直接调用 loaddata 命令也可以正常工作 . 因此整个过程是:

    • <yourapp>/fixtures/initial_data.json 中创建一个夹具文件

    • 创建空迁移:

    python manage.py makemigrations --empty <yourapp>
    
    • 编辑您的迁移文件/migrations/0002_auto_xxx.py
    from django.db import migrations
    from django.core.management import call_command
    
    
    def loadfixture(apps, schema_editor):
        call_command('loaddata', 'initial_data.json')
    
    
    class Migration(migrations.Migration):
    
        dependencies = [
            ('<yourapp>', '0001_initial'),
        ]
    
        operations = [
            migrations.RunPython(loadfixture),
        ]
    
  • 1

    为了给你的数据库一些初始数据,写一个data migration.在数据迁移中,使用RunPython函数来加载你的数据 .

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

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

  • 5

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

    相反,我建议你看看Factory boy .

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

    关于使用灯具也有"Burn Your Fixtures, Use Model Factories" .

  • 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 脚本的情况 . 所以我决定在文件名前加一个连续的数字,如上图所示 .

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

    所以我创建了另一个名为 initial_data_migrations 的应用程序,然后我将此应用程序添加到 settings.py 文件中的 INSTALLED_APPS 列表中 . 然后我在里面创建了一个 migrations 文件夹并添加了一个名为 run_sql_scripts.pyWhich actually is a custom migration )的文件 . 如下图所示:

    enter image description here

    我创建了 run_sql_scripts.py ,因此它负责运行每个应用程序中可用的所有 sql 脚本 . 当有人运行 python manage.py migrate 时会触发这个 . 此自定义 migration 还将所涉及的应用程序添加为依赖项,这样它只有在所需的应用程序执行了 0001_initial.py 迁移后才尝试运行 sql 语句(我们不想尝试针对不存在的表运行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))
        ]
    

    我希望有人觉得这很有帮助,对我来说效果很好!如果您有任何疑问,请告诉我 .

    NOTE: This might not be the best solution since I'm just getting started with django, however still wanted to share this "How-to" with you all since I didn't find much information while googling about this.

相关问题