ChatGPT解决这个技术问题 Extra ChatGPT

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

我最近从 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 函数(如调用 loaddata)从 JSON 夹具文件加载数据。

我更喜欢第二种选择。

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

另外,我想在 OP 的原始问题中添加另一个问题:我们应该如何对不属于我们的应用程序的数据进行数据迁移。例如,如果有人使用站点框架,他需要有一个带有站点数据的夹具。由于站点框架与我们的应用程序无关,我们应该将数据迁移放在哪里?谢谢 !
此处任何人尚未解决的重要一点是,当您需要将数据迁移中定义的数据添加到您伪造迁移的数据库时会发生什么。由于迁移是伪造的,因此您的数据迁移将无法运行,您必须手动完成。在这一点上,您也可以只在夹具文件上调用 loaddata。
另一个有趣的场景是,如果您进行数据迁移以创建 auth.Group 实例,然后您有一个新的组要创建为种子数据。您需要创建一个新的数据迁移。这可能很烦人,因为您的组种子数据将位于多个文件中。此外,如果您想重置迁移,则必须仔细查找设置种子数据并移植它们的数据迁移。
@Serafeim 如果您使用数据迁移而不是固定装置,那么“将第三方应用程序的初始数据放在哪里”的问题不会改变,因为您只会更改数据的加载方式。我为这样的事情使用了一个小型自定义应用程序。如果第三方应用程序称为“foo”,我将包含数据迁移/夹具的简单应用程序称为“foo_integration”。
@guettli 是的,可能使用额外的应用程序是最好的方法!

f
fjsj

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

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

创建空迁移: 在 Django 1.7 中: python manage.py makemigrations --empty 在 Django 1.8+ 中,您可以提供一个名称: python manage.py makemigrations --empty --name load_intial_data 编辑您的迁移文件<您的应用程序>/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): "粗暴地删除这个模型的所有条目......" 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.最简单的:使用 app_label 调用 loaddata 将自动从 的 fixtures 目录加载fixture: 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 目录(你可能不想要)加载fixture 文件名。运行它 python manage.py migrate


好的,你是对的......另外调用 loaddata('loaddata', fixture_filename, app_label='<yourapp>') 也将直接转到应用程序夹具目录(因此无需构建夹具的完整路径)
使用该方法,序列化程序将处理当前 models.py 文件中的模型状态,该文件可能有一些额外的字段或一些其他更改。如果在创建迁移后进行了一些更改,它将失败(因此我们甚至无法在迁移之后创建架构迁移)。为了解决这个问题,我们可以暂时将序列化程序正在处理的应用程序注册表更改为提供给第一个参数的迁移功能的注册表。路径的注册表位于 django.core.serializers.python.apps
我们为什么这样做呢?为什么 Django 越来越难运行和维护?我不想这样做,我想要一个简单的命令行界面来为我解决这个问题,就像它曾经与固定装置一样。 Django 应该让这些东西变得更容易,而不是更难:(
@GwynBleidD 这是您提出的一个非常重要的观点,我认为它应该出现在这个公认的答案中。它与 data migration code example of the documentation 中的注释相同。您是否知道另一种在提供的 app registry 中使用序列化程序的方法,而无需更改全局变量(这可能会在假设的未来并行数据库迁移中导致问题)。
这个答案被推荐给 kazoo 并被接受,这正是我建议人们不要使用 stackoverflow 的原因。即使现在有了评论和轶事,我仍然有#django 中的人提到这个。
R
Rockallite

精简版

您应该在数据迁移中直接使用 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 字段,因此您这样做并针对更新的模型进行新的迁移(并且可能在 { 1} 提示您)。

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

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

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

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

结论:在数据迁移中,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),
    ]

Rockallite,你提出了一个非常强的观点。不过,您的回答让我想知道,来自 @n__o/@mlissner 的依赖于 objects = serializers.deserialize('json', fixture, ignorenonexistent=True) 的答案的解决方案 2.1 是否会遇到与 loaddata 相同的问题?还是 ignorenonexistent=True 涵盖了所有可能的问题?
如果您查看 the source,您会发现 ignorenonexistent=True 参数有两个效果:1) 它忽略不在最新模型定义中的夹具模型, 2) 它忽略不在最新对应模型定义中的夹具模型的字段。他们都没有处理 new-required-field-in-the-model 的情况。所以,是的,我认为它和普通的 loaddata 有同样的问题。
一旦我发现我的旧 json 的模型使用 natural_key() 引用了其他模型,这种方法效果很好,这种方法似乎不支持 - 我只是用引用模型的实际 id 替换了 natural_key 值。
可能这个答案作为公认的答案会更有帮助,因为在运行测试用例时会创建一个新数据库并且所有迁移都是从头开始应用的。该解决方案修复了在数据迁移中没有替换 _get_model 的情况下,使用 unittest 的项目将面临的问题。肿瘤坏死因子
感谢@Rockallite 的更新和解释。我最初的答案是在 Django 1.7 中引入迁移几周后发布的,关于如何进行的文档尚不清楚(我上次检查时仍然如此)。希望 Django 有一天会更新他们的加载数据/迁移机制以考虑模型历史。
a
alexhayes

受到一些评论(即 n__o'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.

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


F
FlogFR

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

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

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


所以你鼓励我在 RunPython 函数中重复调用 myModel.create(...) (或使用循环)?
几乎是的。 Transaactionnal 数据库将完美处理它:)
l
leifdenby

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

为了方便此 I wrote a quick function,它将在当前应用程序的 fixtures 目录中查找并加载固定装置。将此函数放入与迁移中的字段匹配的模型历史记录点中的迁移中。


谢谢你!我编写了一个适用于 Python 3 的版本(并通过了我们严格的 Pylint)。您可以将其用作具有 RunPython(load_fixture('badger', 'stoat')) 的工厂。 gist.github.com/danni/1b2a0078e998ac080111
t
the Tin Man

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

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

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

还有关于使用固定装置的"Burn Your Fixtures, Use Model Factories"


我同意您的观点“如果频繁更改则难以维护”,但这里的夹具仅旨在在安装项目时提供初始(和最少)数据......
这是一次性加载数据,如果它在迁移的上下文中完成是有意义的。因为如果它在迁移中,则不必对 json 数据进行更改。任何需要进一步更改数据的模式更改都应该通过另一个迁移来处理(此时可能在数据库中的其他数据也需要修改)。
A
Antony Fuentes

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

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

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

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

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

https://i.stack.imgur.com/YJQ5K.png

我还发现了一些需要以特定顺序执行 sql 脚本的情况。所以我决定在文件名前加上一个连续的数字,如上图所示。

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

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

https://i.stack.imgur.com/4pjZv.png

我创建了 run_sql_scripts.py,以便它负责运行每个应用程序中可用的所有 sql 脚本。然后当有人运行 python manage.py migrate 时触发这个。此自定义 migration 还将所涉及的应用程序添加为依赖项,这样它仅在所需的应用程序执行其 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,但是仍然想与大家分享这个“操作方法”,因为我在谷歌搜索时没有找到太多信息。


也许我错了...但是如果您修改 .sql 文件,甚至添加新的 .sql 文件,python manage.py migrate load_initial_data 将不会检测到任何更改。所以这对于真正静态的初始数据很有用,不允许更改。虽然,这是对公认答案的改进

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅