Завантаження початкових даних за допомогою Django 1.7 та перенесення даних


95

Нещодавно я перейшов з Django 1.6 на 1.7 і почав використовувати міграції (ніколи не використовував південь).

До 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-fixture )

В офіційній документації немає чіткого прикладу, як це зробити, тому моє запитання:

Який найкращий спосіб імпортувати такі вихідні дані за допомогою міграції даних:

  1. Напишіть код Python з кількома дзвінками mymodel.create(...),
  2. Використовуйте або записуйте функцію Django ( наприклад, викликloaddata ) для завантаження даних із файлу приладу JSON.

Я віддаю перевагу другому варіанту.

Я не хочу використовувати Південь, оскільки, здається, Джанго зараз може це зробити.


3
Крім того, я хочу додати ще одне запитання до оригінального запитання OP: Як нам робити міграцію даних для даних, які не належать до наших додатків. Наприклад, якщо хтось використовує фреймворк веб-сайтів, йому потрібно мати пристрій з даними сайтів. Оскільки фреймворк веб-сайтів не пов'язаний з нашими додатками, куди ми повинні помістити цю міграцію даних? Дякую !
Серафейм,

Важливим моментом, до якого тут ще ніхто не звертався, є те, що відбувається, коли вам потрібно додати дані, визначені в міграції даних, до бази даних, на якій ви підроблені міграції. Оскільки міграції були фальшивими, ваша міграція даних не запускатиметься, і ви повинні робити це вручну. На цьому етапі ви можете просто викликати loaddata у файлі приладу.
hekevintran

Інший цікавий сценарій - це те, що відбувається, якщо у вас є міграція даних для створення автентифікації. Наприклад, екземпляри групи, а пізніше у вас є нова група, яку ви хочете створити як початкові дані. Вам потрібно буде створити нову міграцію даних. Це може дратувати, оскільки дані про насіння вашої групи будуть у декількох файлах. Також у випадку, якщо ви хочете скинути міграції, вам доведеться переглянути, щоб знайти міграції даних, які налаштовують насіннєві дані, а також перенести їх.
hekevintran

@Serafeim Питання "Куди помістити вихідні дані для сторонньої програми" не змінюється, якщо ви використовуєте міграцію даних замість пристосувань, оскільки ви змінюєте лише спосіб завантаження даних. Я використовую невеликий спеціальний додаток для таких речей. Якщо сторонній додаток називається "foo", я називаю свій простий додаток, що містить міграцію даних / пристрій, "foo_integration".
guettli

@guettli так, можливо, використання додаткової програми - найкращий спосіб це зробити!
Серафейм

Відповіді:


81

Оновлення : див. Коментар @ GwynBleidD нижче щодо проблем, які може спричинити це рішення, і відповідь @ Rockallite нижче щодо підходу, який є більш стійким до майбутніх змін моделі.


Припускаючи, що у вас є файл приладу в <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
  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. Найпростіший: виклик loaddataз app_labelзавантажать світильники від <yourapp>«s 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ім'я файлу з усіх каталогів приладів додатків (чого ви, мабуть, не хочете).

  3. Виконати його

    python manage.py migrate <yourapp>

1
гаразд, ти маєш рацію ... Також дзвінок loaddata('loaddata', fixture_filename, app_label='<yourapp>')також надходитиме безпосередньо до директорії приладу додатка (отже, не потрібно будувати повний шлях приладу)
n__o

15
За допомогою цього методу серіалізатор працюватиме над станом моделей із поточних models.pyфайлів, які можуть мати додаткові поля або деякі інші зміни. Якщо деякі зміни були внесені після створення міграції, це не вдасться (тому ми навіть не можемо створити міграції схем після цієї міграції). Щоб виправити, що ми можемо щомісячно змінювати реєстр програм, над яким працює серіалізатор, на реєстр, який надається на функцію міграції за першим параметром. Реєстр до шляху знаходиться за адресою django.core.serializers.python.apps.
GwynBleidD

3
Чому ми це робимо? Чому Django стає все складнішим для запуску та обслуговування? Я не хочу йти, хоча це, я хочу простий інтерфейс командного рядка, який вирішує цю проблему для мене, тобто, як це було раніше зі світильниками. Django повинен зробити ці речі простішими, а не складнішими :(
CpILL

1
@GwynBleidD Це дуже важливий момент, який ви робите, і я думаю, що він повинен бути вказаний у цій прийнятій відповіді. Це те саме зауваження, яке з’являється як коментар у прикладі документації до коду міграції даних . Чи знаєте ви ще один спосіб використання серіалізаторів із наданими app registry, без зміни глобальної змінної (що може призвести до проблем у гіпотетичному майбутньому при паралельних міграціях баз даних).
Оголошення N

3
Ця відповідь, яку проголосували за kazoo разом із прийняттям, саме тому я рекомендую людям не використовувати stackoverflow. Навіть зараз із коментарями та анекдотами, я все ще маю людей у ​​#django, які посилаються на це.
shangxiao

50

Коротка версія

НЕ слід використовувати 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),
    ]

1
Рокалліт, ви робите дуже сильну сторону. Ваша відповідь змусила мене задуматись, хоча б рішення 2.1 із відповіді @ n__o / @ mlissner, яке спирається на те, objects = serializers.deserialize('json', fixture, ignorenonexistent=True)постраждало від тієї ж проблеми, що і loaddata? Або ignorenonexistent=Trueохоплює всі можливі проблеми?
Даріо

7
Якщо ви подивитесь на джерело , то виявите, що ignorenonexistent=Trueаргумент має два ефекти: 1) він ігнорує моделі кріплення, яких немає в найсучасніших визначеннях моделі, 2) ігнорує поля моделі кріплення, які не є в найсучаснішому відповідному визначенні моделі. Жоден з них не справляється з новою необхідною ситуацією в області моделі . Так, так, я думаю, це страждає тим самим питанням, що і звичайне loaddata.
Rockallite

Це спрацювало чудово, як тільки я зрозумів, що мій старий json мав моделі, на які посилаються інші моделі, використовуючи a natural_key(), який цей метод, схоже, не підтримує - я просто замінив значення natural_key фактичним ідентифікатором посилальної моделі.
dsummersl

1
Можливо, така відповідь як прийнята відповідь була б більш корисною, оскільки під час запуску тестів створюється нова база даних і всі міграції застосовуються з нуля. Це рішення вирішує проблеми, з якими зіткнеться проект з unittest у разі не заміни _get_model при міграції даних. Tnx
Мохаммад Алі Багершемірані

Дякуємо за оновлення та пояснення, @Rockallite. Моя початкова відповідь була опублікована через кілька тижнів після введення міграцій у Django 1.7, а документація щодо того, як діяти, була незрозумілою (і досі, останнього разу, коли я перевіряв). Сподіваємось, Django оновить свій механізм завантаження даних / міграції з урахуванням історії моделі одного дня.
n__o 02

6

Натхненний деякими коментарями (а саме n__o) та тим фактом, що у мене багато initial_data.*файлів, розподілених по кількох додатках, я вирішив створити програму Django, яка полегшить створення цих міграцій даних.

Використання Джанго-міграційне пристосування ви можете просто запустити наступну команду управління , і він буде шукати через всі ваші 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 для отримання інструкцій щодо встановлення / використання.


2

Для того, щоб надати вашій базі даних деякі вихідні дані, напишіть міграцію даних. Під час міграції даних використовуйте функцію RunPython для завантаження даних.

Не пишіть жодної команди loaddata, оскільки цей спосіб застарілий.

Міграція даних буде виконана лише один раз. Міграції - це впорядкована послідовність міграцій. Коли запущено міграцію 003_xxxx.py, django migrations пише у базі даних, що ця програма мігрує до цієї (003), і буде виконувати лише такі міграції.


Тож ви закликаєте мене повторювати дзвінки myModel.create(...)(або за допомогою циклу) у функції RunPython?
Міккаел

майже так. Транзакційні бази даних з цим чудово
впораються

1

Наведені вище рішення для мене, на жаль, не спрацювали. Я виявив, що кожного разу, коли я міняю свої моделі, мені доводиться оновлювати свої пристосування. В ідеалі я б замість цього писав міграції даних, щоб аналогічним чином змінювати створені дані та дані, завантажені приладами.

Щоб полегшити це, я написав швидку функцію, яка буде шукати в fixturesкаталозі поточної програми та завантажувати пристрій. Помістіть цю функцію в міграцію в точці історії моделі, яка відповідає полям у міграції.


Дякую за це! Я написав версію, яка працює з Python 3 (і передає наш суворий Pylint). Ви можете використовувати його як завод з RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Даніель

1

На мій погляд, світильники трохи погані. Якщо ваша база даних часто змінюється, їх оновлення стане найшвидшим кошмаром. Власне, це не лише моя думка, у книзі "Два совки Джанго" це пояснено набагато краще.

Натомість я напишу файл Python для початкового налаштування. Якщо вам потрібно щось більше, я запропоную вам поглянути на фабричного хлопчика .

Якщо вам потрібно перенести деякі дані, вам слід скористатися перенесенням даних .

Існує також "Спаліть свої світильники, використовуйте фабричні моделі" про використання світильників.


1
Я погоджуюсь з Вашим твердженням "важко підтримувати, якщо часто змінюються", але тут пристрій має на меті лише надати початкові (і мінімальні) дані при встановленні проекту ...
Мікаел,

1
Це для одноразового завантаження даних, що, якщо це робиться в контексті міграцій, має сенс. Оскільки якщо це відбувається в рамках міграції, не потрібно вносити зміни до даних json. Будь-які зміни схеми, які вимагають змін даних далі по дорозі, повинні оброблятися за допомогою іншої міграції (на той момент інші дані, можливо, в базі даних, які також потрібно буде змінити).
mtnpaul

0

У 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_APPSу settings.pyфайлі. Потім я створив migrationsпапку всередині і додав файл із назвою run_sql_scripts.py( що насправді є власною міграцією ). Як видно на зображенні нижче:

введіть тут опис зображення

Я створив run_sql_scripts.pyтак, що він дбає про запуск усіх sqlсценаріїв, доступних у кожному додатку. Потім цього звільняють, коли хтось біжить python manage.py migrate. Цей звичай migrationтакож додає залучені додатки як залежності, таким чином, він намагається запустити sqlоператори лише після того, як необхідні програми виконають свої 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, однак все-таки хотів поділитися цим "Посібником" з усіма вами, оскільки я не знайшов багато інформації, гуглюючи про це.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.