Загрузка исходных данных с помощью Django 1.7 и миграции данных

96

Недавно я перешел с 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. Напишите код Python с несколькими вызовами mymodel.create(...),
  2. Используйте или напишите функцию Django ( например, вызовloaddata ) для загрузки данных из файла фикстуры JSON.

Я предпочитаю второй вариант.

Я не хочу использовать South, поскольку Django, похоже, теперь может делать это изначально.

Микаэль
источник
3
Кроме того, я хочу добавить еще один вопрос к исходному вопросу OP: как мы должны выполнять миграцию данных для данных, не принадлежащих нашим приложениям. Например, если кто-то использует фреймворк сайтов, ему необходимо иметь приспособление с данными сайтов. Поскольку фреймворк сайтов не связан с нашими приложениями, куда нам поместить эту миграцию данных? Спасибо !
Серафейм
Важный момент, который здесь никем не обсуждался, - это то, что происходит, когда вам нужно добавить данные, определенные в миграции данных, в базу данных, в которой вы имитировали миграции. Поскольку миграции были фальшивыми, миграция данных не будет запущена, и вы должны будете сделать это вручную. На этом этапе вы можете просто вызвать loaddata для файла фикстуры.
hekevintran
Другой интересный сценарий - это то, что происходит, если у вас есть миграция данных для создания экземпляров auth.Group, например, а позже у вас есть новая группа, которую вы хотите создать в качестве исходных данных. Вам нужно будет создать новую миграцию данных. Это может раздражать, потому что исходные данные вашей группы будут находиться в нескольких файлах. Также, если вы хотите сбросить миграции, вам нужно будет просмотреть, чтобы найти миграции данных, которые устанавливают исходные данные, а также переносить их.
hekevintran
@Serafeim Вопрос «Где разместить исходные данные для стороннего приложения» не изменится, если вы используете миграцию данных вместо фикстур, поскольку вы меняете только способ загрузки данных. Для подобных вещей я использую небольшое пользовательское приложение. Если стороннее приложение называется «foo», я называю свое простое приложение, содержащее перенос данных / приспособление «foo_integration».
Guettli
@guettli да, наверное, использование дополнительного приложения - лучший способ сделать это!
Серафейм

Ответы:

82

Обновление : см. Комментарий @ 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>
n__o
источник
1
хорошо, вы правы ... Также вызов loaddata('loaddata', fixture_filename, app_label='<yourapp>')также будет идти непосредственно в
каталог фикстуры
15
Используя этот метод, сериализатор будет работать с состоянием моделей из текущих models.pyфайлов, которые могут иметь некоторые дополнительные поля или некоторые другие изменения. Если после создания миграции были внесены некоторые изменения, произойдет сбой (поэтому мы даже не сможем создать миграции схемы после этой миграции). Чтобы исправить это, мы можем временно изменить реестр приложений, над которым работает сериализатор, на реестр, предоставленный для функции миграции по первому параметру. Реестр к пути находится по адресу django.core.serializers.python.apps.
GwynBleidD 02
3
Зачем мы это делаем? Почему Django становится все труднее запускать и поддерживать? Я не хочу вдаваться в подробности, мне нужен простой интерфейс командной строки, который решает эту проблему для меня, то есть как раньше с приборами. Предполагается, что Django сделает все это проще, а не сложнее :(
CpILL
1
@GwynBleidD Это очень важный момент, о котором вы говорите, и я думаю, что он должен появиться в этом принятом ответе. Это то же замечание, которое появляется как комментарий в примере кода миграции данных в документации . Знаете ли вы другой способ использования сериализаторов с предоставленным app registryбез изменения глобальной переменной (что может вызвать проблемы в гипотетическом будущем при параллельной миграции базы данных).
Объявление N
3
За этот ответ kazoo проголосовали вместе с принятием - именно поэтому я рекомендую людям не использовать stackoverflow. Даже сейчас с комментариями и анекдотами у меня все еще есть люди в #django, ссылающиеся на это.
шансяо
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покрывает все возможные вопросы?
Dário 03
7
Если вы посмотрите на источник , вы обнаружите, что этот ignorenonexistent=Trueаргумент имеет два эффекта: 1) он игнорирует модели прибора, которых нет в самых последних определениях моделей, 2) он игнорирует поля модели прибора, которые не входят в в самом последнем соответствующем определении модели. Ни один из них не справляется с ситуацией нового обязательного поля в модели . Так что да, я думаю, что он страдает той же проблемой, что и обычный loaddata.
Rockallite 08
Это отлично сработало, как только я понял, что в моем старом json модели ссылались на другие модели с использованием a natural_key(), который этот метод, похоже, не поддерживает - я просто заменил значение natural_key на фактический идентификатор модели, на которую указывает ссылка.
dsummersl
1
Вероятно, этот ответ как принятый ответ был бы более полезным, потому что при запуске тестовых случаев создается новая база данных, и все миграции применяются с нуля. Это решение устраняет проблемы, с которыми столкнется проект с unittest, если не заменить _get_model при миграции данных. Tnx
Мохаммад Али Багершемирани
Спасибо за обновление и объяснения, @Rockallite. Мой первоначальный ответ был опубликован через несколько недель после того, как миграции были введены в Django 1.7, и документация о том, как действовать, была неясна (и все еще остается, когда я проверял в последний раз). Надеюсь, Django когда-нибудь обновит свой механизм loaddata / миграции, чтобы учесть историю модели.
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 для инструкций по установке / использованию.

Alexhayes
источник
2

Чтобы дать вашей базе данных некоторые исходные данные, напишите перенос данных. При миграции данных используйте функцию RunPython для загрузки ваших данных.

Не пишите никаких команд loaddata, так как этот способ устарел.

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

FlogFR
источник
Так вы побуждаете меня повторять вызовы myModel.create(...)(или использовать цикл) в функции RunPython?
Mickaël
в значительной степени да. Базы данных транзакций справятся с этим отлично :)
FlogFR
1

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

Чтобы облегчить это, я написал быструю функцию, которая будет искать в fixturesкаталоге текущего приложения и загружать приспособление. Поместите эту функцию в миграцию в ту точку истории модели, которая соответствует полям в миграции.

Leifdenby
источник
Спасибо за это! Я написал версию, которая работает с Python 3 (и соответствует нашему строгому Pylint). Вы можете использовать его как фабрику с RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle
1

На мой взгляд, светильники немного плохие. Если ваша база данных часто меняется, поддерживать их в актуальном состоянии скоро станет кошмаром. На самом деле, это не только мое мнение, в книге «Два шарика Django» оно объяснено намного лучше.

Вместо этого я напишу файл Python для начальной настройки. Если вам нужно что-то еще, я предлагаю вам взглянуть на Factory boy .

Если вам нужно перенести некоторые данные, вы должны использовать миграцию данных .

Также есть «Запишите свои светильники , используйте фабрики моделей» об использовании светильников.

Griffosx
источник
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, но все же хотел поделиться этим «Практическим руководством» со всеми вами, так как я не нашел много информации, пока гуглил об этом.

Энтони Фуэнтес Артавиа
источник