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

vnjpjtjt  于 2023-01-01  发布在  Python
关注(0)|答案(8)|浏览(121)

我最近从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-fixture)
官方文件没有明确的例子说明如何做,所以我的问题是:
使用数据迁移导入此类初始数据的最佳方法是什么:
1.编写Python代码,多次调用mymodel.create(...)
1.使用或编写Django函数(like calling loaddata)从JSON fixture文件加载数据。
我更喜欢第二种选择。
我不想使用南方,因为Django现在似乎可以自己做了。

puruo6ea

puruo6ea1#

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

假设<yourapp>/fixtures/initial_data.json中有一个fixture文件
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.* * 最简单:**使用app_label调用loaddata将从<yourapp>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将尝试从allapps fixture目录加载fixture文件名(您可能不需要)。*

1.运行它

python manage.py migrate <yourapp>
pcrecxhr

pcrecxhr2#

简短版本

您不应*****在数据迁移中直接使用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管理命令从fixture加载数据的数据迁移,并且它已经应用到您的开发环境中。
稍后,您决定向相应的模型添加一个新的 * required * 字段,因此您这样做了,并针对更新后的模型进行了新的迁移(并且可能在./manage.py makemigrations提示您时向新字段提供一次性值)。
您运行下一次迁移,一切正常。
最后,您已经完成了Django应用程序的开发,并将其部署到生产服务器上,现在是时候在生产环境中从头开始运行整个迁移了。
但是,数据迁移失败,原因是loaddata命令反序列化后的模型,代表当前代码,不能保存,新增加的 * required * 字段数据为空,原来的fixture缺少必要的数据!
但是,即使您使用新字段所需的数据更新夹具,数据迁移仍然失败。当数据迁移运行时,将相应列添加到数据库的 * 下一个 * 迁移尚未应用。您不能将数据保存到不存在的列!

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

解决方案

loaddata命令依赖于django.core.serializers.python._get_model函数从fixture获取相应的模型,fixture将返回模型的最新版本。我们需要对其进行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),
    ]
i86rm4rw

i86rm4rw3#

受一些评论(即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

8ehkhllq

8ehkhllq4#

为了给予你的数据库一些初始数据,编写一个数据迁移,在数据迁移中,使用RunPython函数加载你的数据。
不要编写任何loaddata命令,因为这种方式已被弃用。
您的数据迁移将只运行一次。迁移是一个有序的迁移序列。当003_xxxx.py迁移运行时,django迁移会在数据库中写入这个应用被迁移到这个(003),并且将只运行下面的迁移。

3htmauhk

3htmauhk5#

不幸的是,上面的解决方案并不适用于我,我发现每次我改变我的模型时,我都必须更新我的fixture,理想情况下,我应该编写数据迁移来类似地修改创建的数据和fixture加载的数据。
为了方便这个I wrote a quick function,它将在当前应用的fixtures目录中查找并加载一个fixture。将这个函数放入一个迁移中,在模型历史记录的点上匹配迁移中的字段。

jv4diomz

jv4diomz6#

在Django2.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的应用程序,然后将此应用程序添加到settings.py文件中的INSTALLED_APPS列表中。然后,我在其中创建了一个migrations文件夹,并添加了一个名为run_sql_scripts.py的文件(实际上是一个自定义迁移)。如下图所示:

我创建了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))
    ]

我希望有人觉得这对我有帮助,它对我很有效!如果你有任何问题,请让我知道。

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

2sbarzqh

2sbarzqh7#

在我看来,fixture有点糟糕。如果你的数据库经常变化,保持它们最新将很快成为一场噩梦。实际上,这不仅仅是我的观点,在书“Two Scoops of Django”中解释得更好。
相反,我将编写一个Python文件来提供初始设置。如果你需要更多的东西,我建议你看看Factory boy
如果需要迁移某些数据,则应使用数据迁移。
还有"Burn Your Fixtures, Use Model Factories"关于使用固定装置。

nnvyjq4y

nnvyjq4y8#

自然键呢?

尽管@rockallite的回答非常好,但它没有解释如何处理依赖于 * 自然键 * 而不是整数pk值的fixture。

简化版

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

...
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()来加载fixture对象。
基于 * natural keys * 的fixture的反序列化依赖于两件事:
1.在模型的默认管理器上存在get_by_natural_key()方法
1.模型本身上natural_key()方法的存在
get_by_natural_key()方法对于反序列化程序了解如何解释自然键而不是整数pk值是必需的。
这两种方法对于反序列化器来说都是必要的,以便通过自然键从数据库中get现有对象,如here所述。
但是,迁移中可用的apps注册表使用历史模型,这些模型不能访问定制管理器或定制方法(如natural_key())。

可能的解决方案:第一步

我们的定制模型管理器中缺少get_by_natural_key()方法的问题相对容易解决:只需在自定义管理器上设置use_in_migrations=True,如文档中所述。
这确保了您的历史模型可以在迁移期间访问当前的get_by_natural_key(),并且fixture加载现在应该成功了。
但是,您的历史模型仍然没有natural_key()方法。因此,您的装置将被视为新对象,即使它们已经存在于数据库中。如果重新应用数据迁移,这可能会导致各种错误,例如:

  • 唯一约束违规(如果模型具有唯一约束)
  • 复制夹具对象(如果模型没有唯一约束)
  • "获取返回的多个对象"错误(由于之前创建的重复夹具对象)

因此,实际上,您仍然错过了反序列化过程中类似get_or_create的行为。
要体验这一点,只需应用上述数据迁移(在测试环境中),然后回滚相同的数据迁移(不删除数据),然后重新应用数据迁移。

可能的解决方案:第二步

模型本身缺少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', ...)

...

注:

  • 为了清楚起见,我在示例中省略了错误处理和属性检查,您应该在必要的地方实现它们。
  • 这个解决方案使用了 * current * 模型的natural_key方法,这在某些场景中仍然会导致问题,但Django的use_in_migrations选项也是如此。

相关问题