Загрузка исходных данных с помощью Django 1.7 и миграция данных
Недавно я перешел с 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/)
Официальная документация не имеет четкого примера того, как это сделать, поэтому мой вопрос:
Каков наилучший способ импорта таких исходных данных с помощью миграции данных:
- Напишите код Python с несколькими вызовами
mymodel.create(...)
, - Используйте или напишите функцию Django ( например, вызов
loaddata
) для загрузки данных из файла JSON.
Я предпочитаю второй вариант.
Я не хочу использовать Юг, так как Джанго, кажется, теперь может делать это изначально.
9 ответов
Обновление: см. Комментарий @GwynBleidD ниже о проблемах, которые может вызвать это решение, и см. Ответ @Rockallite ниже о подходе, более устойчивом к будущим изменениям модели.
Предполагая, что у вас есть файл фикстуры в <yourapp>/fixtures/initial_data.json
Создайте пустую миграцию:
В Джанго 1.7:
python manage.py makemigrations --empty <yourapp>
В Django 1.8+ вы можете указать имя:
python manage.py makemigrations --empty <yourapp> --name load_intial_data
Отредактируйте ваш файл миграции
<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>
"sfixtures
Dir автоматически: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
имя файла из всех каталогов фиксаторов приложений (которые вы, вероятно, не хотите).Запустить его
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, которое облегчит создание этих миграций данных.
Используя https://github.com/alexhayes/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.
Смотрите https://github.com/alexhayes/django-migration-fixture для инструкций по установке / использованию.
Лучший способ загрузить исходные данные в перенесенных приложениях - это перенести данные (как это также рекомендуется в документации). Преимущество заключается в том, что прибор загружается как во время испытаний, так и на производстве.
@n__o предложил переопределить loaddata
Команда в миграции. Однако в моих тестах loaddata
Команда напрямую тоже отлично работает. Весь процесс таким образом:
Создать файл фикстуры в
<yourapp>/fixtures/initial_data.json
Создайте пустую миграцию:
python manage.py makemigrations --empty <yourapp>
Отредактируйте файл миграции /migrations/0002_auto_xxx.py
from django.db import migrations from django.core.management import call_command def loadfixture(apps, schema_editor): call_command('loaddata', 'initial_data.json') class Migration(migrations.Migration): dependencies = [ ('<yourapp>', '0001_initial'), ] operations = [ migrations.RunPython(loadfixture), ]
Чтобы дать вашей базе данных некоторые начальные данные, напишите миграцию данных. При переносе данных используйте функцию RunPython для загрузки ваших данных.
Не пишите команду loaddata, поскольку этот способ устарел.
Ваши миграции данных будут выполняться только один раз. Миграции представляют собой упорядоченную последовательность миграций. Когда выполняется миграция 003_xxxx.py, django migrations записывает в базу данных, что это приложение перенесено до этой (003), и будет запускать только следующие миграции.
На 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, но все же хотел поделиться с вами всеми этими практическими рекомендациями, поскольку я не нашел много информации, пока гуглял об этом.
Представленные выше решения не сработали для меня, к сожалению. Я обнаружил, что каждый раз, когда я меняю свои модели, я должен обновлять свои приборы. В идеале я бы вместо этого написал миграцию данных, чтобы аналогичным образом модифицировать созданные данные и данные, загружаемые из фикстур.
Чтобы облегчить это, я написал быструю функцию, которая будет выглядеть в fixtures
каталог текущего приложения и загрузить прибор. Поместите эту функцию в миграцию в точке истории модели, которая соответствует полям в миграции.
На мой взгляд, крепления немного плохие. Если ваша база данных часто меняется, то их обновление в скором времени станет кошмаром. На самом деле, это не только мое мнение, в книге "Два совка Джанго" это объясняется гораздо лучше.
Вместо этого я напишу файл Python для обеспечения начальной настройки. Если вам нужно что-то еще, я предлагаю вам взглянуть на Фабричного мальчика.
Если вам нужно перенести некоторые данные, вы должны использовать перенос данных.
Также есть "Сожги свои приборы , используйте фабрики моделей" об использовании приборов.
Хотя превосходен, он не объясняет, как обращаться с приборами, которые полагаются на естественные ключи, а не на целочисленные значения.
Упрощенная версия
Во-первых, обратите внимание, что ответ @rockalliteрешение @rockallite можно упростить, используяunittest.mock.patch
в качестве диспетчера контекста и путем исправления вместо_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() в менеджере модели по умолчанию
- наличие метода natural_key() на самой модели
Метод необходим для того, чтобы десериализатор знал, как интерпретировать естественный ключ, а не целое число.pk
ценить.
Оба метода необходимы десериализатору дляget
существующие объекты из базы данных по естественному ключу, как это также поясняется здесь .
Однакоapps
Реестр, который доступен в ваших миграциях, использует исторические модели , и они не имеют доступа к пользовательским менеджерам или пользовательским методам, таким как .
Возможное решение: шаг 1
Проблема отсутствия метода в нашем пользовательском менеджере моделей решается относительно легко: просто установитеuse_in_migrations=True
на вашем пользовательском менеджере, как описано в документации .
Это гарантирует, что ваши исторические модели могут получить доступ к текущимget_by_natural_key()
во время миграции, и загрузка фикстуры теперь должна пройти успешно.
Однако ваши исторические модели по-прежнему не имеют метода. В результате ваши приборы будут рассматриваться как новые объекты, даже если они уже присутствуют в базе данных. Это может привести к различным ошибкам, если миграция данных будет применена повторно, например:
- нарушения уникальных ограничений (если ваши модели имеют уникальные ограничения)
- дублировать объекты приспособлений (если ваши модели не имеют уникальных ограничений)
- Ошибки «получить возвращенные несколько объектов» (из-за дублирования объектов фикстур, созданных ранее)
Таким образом, вы по-прежнему упускаете своего рода поведение get_or_create во время десериализации.
Чтобы испытать это, просто примените миграцию данных, как описано выше (в тестовой среде), затем откатите ту же миграцию данных (без удаления данных), а затем повторно примените миграцию данных.
Возможное решение: шаг 2
Проблему отсутствующего метода в самой модели решить немного сложнее. Одним из решений было бы назначить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', ...)
...
Примечания:
- Для ясности я исключил из примера такие вещи, как обработка ошибок и проверка атрибутов. Вы должны реализовать их там, где это необходимо.
- Это решение использует текущую модель
natural_key
метод, который все еще может привести к проблемам в определенных сценариях, но то же самое касается и Django.use_in_migrations
вариант для менеджеров моделей.