使用 Django 1.7+ 加载初始数据并进行数据迁移

2025-03-14 08:57:00
admin
原创
66
摘要:问题描述:我最近从 Django 1.6 切换到 1.7,并且开始使用迁移(我从未使用过 South)。在 1.7 之前,我曾经使用文件加载初始数据fixture/initial_data.json,该文件是通过命令加载的python manage.py syncdb(创建数据库时)。现在,我开始使用迁移,并...

问题描述:

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

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

现在,我开始使用迁移,并且此行为已被弃用:

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

官方文档没有关于如何做到这一点的明确示例,所以我的问题是:

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

  1. mymodel.create(...)编写 Python 代码并多次调用

  2. 使用或编写 Django 函数(如调用loaddata)从 JSON 夹具文件加载数据。

我更喜欢第二种选择。

我不想使用 South,因为 Django 现在似乎能够原生地做到这一点。


解决方案 1:

更新:请参阅下面@GwynBleidD 的评论,了解该解决方案可能导致的问题,并请参阅下面@Rockallite 的回答,了解更适合未来模型变化的方法。


假设你有一个fixture文件<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
  1. 编辑迁移文件<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>`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所有应用程序装置目录中加载文件名(您可能不想要)。

  1. 运行它

python manage.py migrate <yourapp>

解决方案 2:

简短版本

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

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

但即使你用新字段所需的数据更新了装置,数据迁移仍然会失败。当数据迁移正在运行时,下一个将相应列添加到数据库的迁移尚未应用。您无法将数据保存到不存在的列中!

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

解决方案

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

(以下代码适用于 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),
    ]

解决方案 3:

受到一些评论(即 n__o)的启发,并且考虑到我在多个应用程序中分布了大量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.

查看django-migration-fixture了解安装/使用说明。

解决方案 4:

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

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

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

解决方案 5:

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

为了实现这一点,我编写了一个快速函数,它将查找fixtures当前应用程序的目录并加载一个装置。将此函数放入与迁移中的字段匹配的模型历史记录中的迁移中。

解决方案 6:

在 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_APPSsettings.py然后我在里面创建了一个文件夹并添加了一个名为(实际上是一个自定义迁移migrations)的文件。如下图所示:run_sql_scripts.py

在此处输入图片描述

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

以下是该脚本的源代码:

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,但是我仍然想与大家分享这个“操作方法”,因为我在谷歌搜索时没有找到太多信息。

解决方案 7:

我认为 Fixture 有点糟糕。如果你的数据库经常更改,那么保持它们的最新状态很快就会成为一场噩梦。实际上,这不仅仅是我的观点,在《Two Scoops of Django》一书中对此进行了更好的解释。

我将编写一个 Python 文件来提供初始设置。如果您需要更多信息,我建议您查看Factory boy。

如果您需要迁移一些数据,您应该使用数据迁移。

还有关于使用装置的“烧毁您的装置,使用模型工厂” 。

解决方案 8:

虽然@rockallite 的回答非常好,但它并没有解释如何处理依赖自然键而不是整pk数值的装置。

简化版

首先,请注意,@rockallite 的解决方案可以通过使用unittest.mock.patch上下文管理器并通过修补来apps简化_get_model

...
from unittest.mock import patch
...

def load_fixture(apps, schema_editor):
    with patch('django.core.serializers.python.apps', apps):
        call_command('loaddata', 'your_data.json', ...)

...

只要你的装置依赖于自然键,这种方法就会很有效。

如果 他们这么做 的 话, 你 可能 会 看到DeserializationError: ... value must be an integer....

自然键的问题

在引擎盖下,loaddata用于django.core.serializers.deserialize()加载你的夹具对象。

基于自然键的装置的反序列化 依赖于两件事:

  1. 模型的默认管理器上存在get_by_natural_key()方法

  2. 模型本身存在natural_key()方法

get_by_natural_key()方法是必要的,以便反序列化器知道如何解释自然键,而不是整pk数值。

get这两种方法对于通过自然键从数据库反序列化为现有对象都是必需的,正如这里所解释的。

但是,apps迁移中可用的注册表使用历史模型,并且这些模型无法访问自定义管理器或自定义方法,例如natural_key()

可能的解决方案:步骤 1

我们的自定义模型管理器缺少方法的问题get_by_natural_key()相对容易解决:只需use_in_migrations=True在自定义管理器上进行设置,如文档中所述。

get_by_natural_key()这可确保您的历史模型可以在迁移期间访问当前模型,并且夹具加载现在应该成功。

但是,您的历史模型仍然没有natural_key()方法。因此,即使您的装置已经存在于数据库中,它们也将被视为新对象。如果重新应用数据迁移,这可能会导致各种错误,例如:

  • 唯一约束违规(如果您的模型有唯一约束)

  • 重复的装置对象(如果您的模型没有唯一约束)

  • “获取返回的多个对象”错误(由于先前创建了重复的夹具对象)

因此,实际上,您仍然错过了反序列化过程中一种类似get_or_create的行为。

要体验这一点,只需应用上面描述的数据迁移(在测试环境中),然后回滚相同的数据迁移(不删除数据),然后重新应用数据迁移。

可能的解决方案:步骤 2

模型本身缺少方法的问题natural_key()有点难以解决。一种解决方案是将natural_key()当前模型中的方法分配给历史模型,例如:

...
from unittest.mock import patch

from django.apps import apps as current_apps
from django.core.management import call_command
...


def load_fixture(apps, schema_editor):
    def _get_model_patch(app_label):
        """ add natural_key method from current model to historical model """
        historical_model = apps.get_model(app_label=app_label)
        current_model = current_apps.get_model(app_label=app_label)
        historical_model.natural_key = current_model.natural_key
        return historical_model

    with patch('django.core.serializers.python._get_model', _get_model_patch):
        call_command('loaddata', 'your_data.json', ...)

...

笔记:

  • 为了清晰起见,我从示例中省略了错误处理和属性检查等内容。您应该在必要时实现这些内容。

  • 该解决方案使用了当前模型的方法,在某些情况下仍可能导致麻烦,但 Django 的模型管理器选项natural_key也是如此。use_in_migrations

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2482  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1533  
  PLM(产品生命周期管理)项目对于企业优化产品研发流程、提升产品质量以及增强市场竞争力具有至关重要的意义。然而,在项目推进过程中,范围蔓延是一个常见且棘手的问题,它可能导致项目进度延迟、成本超支以及质量下降等一系列不良后果。因此,有效避免PLM项目范围蔓延成为项目成功的关键因素之一。以下将详细阐述三大管控策略,助力企业...
plm系统   0  
  PLM(产品生命周期管理)项目管理在企业产品研发与管理过程中扮演着至关重要的角色。随着市场竞争的加剧和产品复杂度的提升,PLM项目面临着诸多风险。准确量化风险优先级并采取有效措施应对,是确保项目成功的关键。五维评估矩阵作为一种有效的风险评估工具,能帮助项目管理者全面、系统地评估风险,为决策提供有力支持。五维评估矩阵概述...
免费plm软件   0  
  引言PLM(产品生命周期管理)开发流程对于企业产品的全生命周期管控至关重要。它涵盖了从产品概念设计到退役的各个阶段,直接影响着产品质量、开发周期以及企业的市场竞争力。在当今快速发展的科技环境下,客户对产品质量的要求日益提高,市场竞争也愈发激烈,这就使得优化PLM开发流程成为企业的必然选择。缺陷管理工具和六西格玛方法作为...
plm产品全生命周期管理   0  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用