簡體   English   中英

使用 Django 1.7+ 和數據遷移加載初始數據

[英]Loading initial data with Django 1.7+ and data migrations

我最近從 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

官方文檔沒有關於如何做到這一點的明確示例,所以我的問題是:

使用數據遷移導入此類初始數據的最佳方法是什么:

  1. 通過多次調用mymodel.create(...)編寫 Python 代碼,
  2. 使用或編寫 Django function( 如調用loaddata )從 JSON 夾具文件加載數據。

我更喜歡第二種選擇。

我不想使用 South,因為 Django 現在似乎可以原生地做到這一點。

更新 :請參閱下面的@ 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 
  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。 最簡單:使用app_label調用loaddata將自動加載<yourapp>fixtures目錄中的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將嘗試從所有應用程序fixtures目錄(你可能不想要)加載fixture文件名。

  3. 運行

     python manage.py migrate <yourapp> 

精簡版

應該使用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),
    ]

受到一些評論(即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

為了給數據庫提供一些初始數據,請編寫數據遷移。 在數據遷移中,使用RunPython函數加載數據。

不要寫任何loaddata命令,因為這種方式已被棄用。

您的數據遷移只會運行一次。 遷移是有序的遷移序列。 運行003_xxxx.py遷移時,django遷移會在數據庫中寫入此應用程序遷移到此應用程序(003),並且僅運行以下遷移。

不幸的是,上面提出的解決方案對我不起作用。 我發現每次更換模型時都要更新我的燈具。 理想情況下,我會編寫數據遷移來修改創建的數據和夾具加載的數據。

為了方便這一點, 我編寫了一個快速函數 ,它將查看當前應用程序的fixtures目錄並加載一個fixture。 將此函數放入模型歷史記錄中與遷移中的字段匹配的位置。

在我看來,裝置有點不好。 如果您的數據庫經常更改,那么讓它們保持最新將很快成為一場噩夢。 實際上,不僅僅是我的觀點,在“兩個Django的Scoops”一書中,它的解釋要好得多。

相反,我會編寫一個Python文件來提供初始設置。 如果你需要更多東西,我建議你看看工廠男孩

如果您需要遷移某些數據,則應使用數據遷移

關於使用燈具還有“刻錄你的燈具,使用模型工廠”

在Django 2.1上,我想用初始數據加載一些模型(例如國家名稱)。

但我希望在執行初始遷移后立即自動執行此操作。

所以我認為在每個需要加載初始數據的應用程序中都有一個sql/文件夾會很棒。

然后在那個sql/文件夾中,我會使用帶有所需DML的.sql文件將初始數據加載到相應的模型中,例如:

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

為了更具描述性,這是包含sql/文件夾的應用程序的外觀: 在此輸入圖像描述

我還發現了一些需要以特定順序執行sql腳本的情況。 所以我決定在文件名前加一個連續的數字,如上圖所示。

然后我需要一種方法來通過執行python manage.py migrate自動加載任何應用程序文件夾中可用的任何SQLs

所以我創建了另一個名為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,但是仍然希望與大家分享這個“操作方法”,因為我在google搜索時沒有找到太多信息。

自然鍵呢?

盡管@rockallite 的答案非常好,但它沒有解釋如何使用自然鍵而不是 integer 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()來加載您的夾具對象。

基於自然鍵的夾具反序列化依賴於兩件事

get_by_natural_key()方法是反序列化器知道如何解釋自然鍵所必需的,而不是 integer pk值。

這兩種方法都是反序列化器通過自然鍵從數據庫中get現有對象所必需的,這里也有解釋。

但是,遷移中可用的apps注冊表使用歷史模型,這些模型無法訪問自定義管理器或自定義方法,例如natural_key()

可能的解決方案:步驟 1

我們自定義的 model 管理器中缺少get_by_natural_key()方法的問題相對容易解決:只需在自定義管理器上設置use_in_migrations=True如文檔中所述

這確保您的歷史模型可以在遷移期間訪問當前的get_by_natural_key() ,並且夾具加載現在應該成功。

但是,您的歷史模型仍然沒有natural_key()方法。 因此,您的設備將被視為新對象,即使它們已經存在於數據庫中。 如果重新應用數據遷移,這可能會導致各種錯誤,例如:

  • 違反唯一約束(如果您的模型具有唯一約束)
  • 重復的夾具對象(如果您的模型沒有唯一約束)
  • “獲取返回多個對象”錯誤(由於先前創建的重復夾具對象)

因此,實際上,您在反序列化期間仍然錯過了一種類似get_or_create的行為。

要體驗這一點,只需如上所述應用數據遷移(在測試環境中),然后回滾相同的數據遷移(不刪除數據),然后重新應用數據遷移。

可能的解決方案:步驟 2

model 本身缺少natural_key()方法的問題有點難以解決。 一種解決方案是將natural_key()方法從當前 model 分配給歷史 model,例如:

...
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 """
        HistoricalModel = apps.get_model(app_label=app_label)
        CurrentModel = current_apps.get_model(app_label=app_label)
        HistoricalModel.natural_key = CurrentModel.natural_key
        return HistoricalModel

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

...

筆記:

  • 為了清楚起見,我在示例中省略了錯誤處理和屬性檢查等內容。 您應該在必要時實施那些。
  • 該解決方案使用當前模型的natural_key方法,在某些場景下可能仍然會導致問題,但對於 Django 的 model 管理器的use_in_migrations選項也是如此。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM