Ładowanie danych początkowych za pomocą Django 1.7 i migracje danych

96

Niedawno przestawiłem się z Django 1.6 na 1.7 i zacząłem używać migracji (nigdy nie używałem South).

Przed 1.7 ładowałem dane początkowe za pomocą fixture/initial_data.jsonpliku, który był ładowany python manage.py syncdbpoleceniem (podczas tworzenia bazy danych).

Teraz zacząłem używać migracji i to zachowanie jest przestarzałe:

Jeśli aplikacja korzysta z migracji, nie ma automatycznego ładowania urządzeń. Ponieważ migracje będą wymagane dla aplikacji w Django 2.0, to zachowanie jest uważane za przestarzałe. Jeśli chcesz załadować początkowe dane aplikacji, rozważ zrobienie tego w ramach migracji danych. ( https://docs.djangoproject.com/en/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

Oficjalna dokumentacja nie posiada wyraźny przykład o tym, jak to zrobić, więc moje pytanie brzmi:

Jaki jest najlepszy sposób importowania takich początkowych danych przy użyciu migracji danych:

  1. Napisz kod w Pythonie z wieloma wywołaniami mymodel.create(...),
  2. Użyj lub napisz funkcję Django ( jak wywołanieloaddata ), aby załadować dane z pliku ustawień JSON.

Wolę drugą opcję.

Nie chcę używać South, ponieważ wydaje się, że Django jest teraz w stanie to zrobić natywnie.

Mickaël
źródło
3
Chciałbym również dodać kolejne pytanie do pierwotnego pytania OP: Jak powinniśmy przeprowadzać migracje danych dla danych, które nie należą do naszych aplikacji. Na przykład, jeśli ktoś używa frameworka witryn, musi mieć urządzenie z danymi witryn. Skoro struktura witryn nie jest powiązana z naszymi aplikacjami, gdzie powinniśmy umieścić tę migrację danych? Dzięki !
Serafeim
Ważną kwestią, której nikt tutaj jeszcze nie poruszył, jest to, co się dzieje, gdy trzeba dodać dane zdefiniowane podczas migracji danych do bazy danych, w której migracje zostały sfałszowane. Ponieważ migracje zostały sfałszowane, migracja danych nie zostanie uruchomiona i musisz to zrobić ręcznie. W tym momencie równie dobrze możesz po prostu wywołać loaddata w pliku ustawień.
hekevintran
Innym interesującym scenariuszem jest to, co się dzieje, jeśli masz migrację danych, na przykład w celu utworzenia instancji grupy auth.Group, a później masz nową grupę, którą chcesz utworzyć jako dane źródłowe. Musisz utworzyć nową migrację danych. Może to być denerwujące, ponieważ dane początkowe grupy będą znajdować się w wielu plikach. Jeśli chcesz zresetować migracje, będziesz musiał przejrzeć, aby znaleźć migracje danych, które konfigurują dane początkowe, i również je przenieść.
hekevintran
@Serafeim Pytanie „Gdzie umieścić dane początkowe dla aplikacji innej firmy” nie zmienia się, jeśli używasz migracji danych zamiast urządzeń, ponieważ zmieniasz tylko sposób ładowania danych. Do takich rzeczy używam małej, niestandardowej aplikacji. Jeśli aplikacja innej firmy nazywa się „foo”, nazywam moją prostą aplikację zawierającą migrację danych / urządzenie „foo_integration”.
guettli
@ guettli tak, prawdopodobnie najlepszym sposobem na to jest użycie dodatkowej aplikacji!
Serafeim,

Odpowiedzi:

82

Aktualizacja : Zobacz komentarz @ GwynBleidD poniżej, aby poznać problemy, które może spowodować to rozwiązanie, i zobacz odpowiedź @ Rockallite poniżej, aby uzyskać podejście, które jest bardziej trwałe na przyszłe zmiany modelu.


Zakładając, że masz plik urządzenia w formacie <yourapp>/fixtures/initial_data.json

  1. Utwórz pustą migrację:

    W Django 1.7:

    python manage.py makemigrations --empty <yourapp>
    

    W Django 1.8+ możesz podać nazwę:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
    
  2. Edytuj plik migracji <yourapp>/migrations/0002_auto_xxx.py

    2.1. Niestandardowa implementacja, inspirowana Django ' loaddata(wstępna odpowiedź):

    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. Prostsze rozwiązanie dla load_fixture(zgodnie z sugestią @ 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) 
    

    Przydatne, jeśli chcesz użyć katalogu niestandardowego.

    2.3. Najprostszy: wywołanie loaddataz app_labelopraw obciążenie będzie od <yourapp>„s fixturesreż automatycznie:

    from django.core.management import call_command
    
    fixture = 'initial_data'
    
    def load_fixture(apps, schema_editor):
        call_command('loaddata', fixture, app_label='yourapp') 
    

    Jeśli nie określisz app_label, loaddata spróbuje załadować fixturenazwę pliku ze wszystkich katalogów urządzeń aplikacji (których prawdopodobnie nie chcesz).

  3. Uruchom

    python manage.py migrate <yourapp>
    
Nie
źródło
1
ok, masz rację ... Również wywołanie loaddata('loaddata', fixture_filename, app_label='<yourapp>')również przejdzie bezpośrednio do katalogu urządzeń aplikacji (stąd nie ma potrzeby tworzenia pełnej ścieżki urządzenia)
n__o Lutego
15
Używając tej metody, serializator będzie działał na stanie modeli z bieżących models.pyplików, które mogą mieć dodatkowe pola lub inne zmiany. Jeśli jakieś zmiany zostały wprowadzone po utworzeniu migracji, to się nie powiedzie (więc nie możemy nawet utworzyć migracji schematu po tej migracji). Aby to naprawić, możemy ręcznie zmienić rejestr aplikacji, nad którymi pracuje serializator, na rejestr dostarczony do funkcji migracji dla pierwszego parametru. Rejestr do ścieżki znajduje się pod adresem django.core.serializers.python.apps.
GwynBleidD
3
Dlaczego to robimy? Dlaczego Django staje się coraz trudniejsze w obsłudze i utrzymaniu? Nie chcę tego robić, chcę prostego interfejsu wiersza poleceń, który rozwiązuje ten problem za mnie, tj. Tak, jak to było z urządzeniami. Django ma to ułatwić, a nie utrudnić :(
CpILL
1
@GwynBleidD Jest to bardzo ważna kwestia, którą poruszasz i myślę, że powinna pojawić się w tej zaakceptowanej odpowiedzi. Jest to ta sama uwaga, która pojawia się jako komentarz w przykładzie kodu migracji danych w dokumentacji . Czy znasz inny sposób używania serializatorów z dostarczonymi app registry, bez zmiany zmiennej globalnej (co może powodować problemy w hipotetycznej przyszłości z równoległymi migracjami baz danych).
Ad N
3
Ta odpowiedź, która jest pozytywna dla kazoo wraz z akceptacją, jest dokładnie powodem, dla którego zalecam ludziom, aby nie używali stackoverflow. Nawet teraz z komentarzami i anegdotami wciąż mam ludzi na #django, którzy się do tego odnoszą.
shangxiao,
52

Krótka wersja

NIE należy używać loaddatapolecenia zarządzania bezpośrednio podczas migracji danych.

# 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),
    ]

Długa wersja

loaddatawykorzystuje django.core.serializers.python.Deserializernajbardziej aktualne modele do deserializacji danych historycznych podczas migracji. To nieprawidłowe zachowanie.

Załóżmy na przykład, że istnieje migracja danych, która wykorzystuje loaddatapolecenie zarządzania do ładowania danych z urządzenia i jest już zastosowana w twoim środowisku programistycznym.

Później decydujesz się dodać nowe wymagane pole do odpowiedniego modelu, więc robisz to i wykonujesz nową migrację do zaktualizowanego modelu (i ewentualnie ./manage.py makemigrationspodajesz jednorazową wartość do nowego pola, gdy pojawi się monit).

Przeprowadzasz następną migrację i wszystko jest w porządku.

Wreszcie, skończyłeś programować swoją aplikację Django i wdrożyć ją na serwerze produkcyjnym. Teraz pora na uruchomienie całej migracji od podstaw na środowisku produkcyjnym.

Jednak migracja danych kończy się niepowodzeniem . Dzieje się tak, ponieważ zdeserializowanego modelu z loaddatapolecenia, który reprezentuje bieżący kod, nie można zapisać z pustymi danymi dla nowego dodanego wymaganego pola. W oryginalnym urządzeniu brakuje niezbędnych do tego danych!

Ale nawet jeśli zaktualizujesz urządzenie o wymagane dane dla nowego pola, migracja danych nadal się nie powiedzie . Gdy migracja danych jest uruchomiona, kolejna migracja, która dodaje odpowiednią kolumnę do bazy danych, nie jest jeszcze stosowana. Nie możesz zapisać danych w kolumnie, która nie istnieje!

Wniosek: w migracji danychloaddatakomenda wprowadza potencjalną niespójność między modelem a bazą danych. Zdecydowanie NIE powinieneśużywać go bezpośrednio podczas migracji danych.

Rozwiązanie

loaddatapolecenie opiera się na django.core.serializers.python._get_modelfunkcji, która pobiera odpowiedni model z urządzenia, co zwróci najbardziej aktualną wersję modelu. Musimy go małpować, aby uzyskał model historyczny.

(Poniższy kod działa dla 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),
    ]
Rockallite
źródło
1
Rockallite, masz bardzo mocną stronę. Twoja odpowiedź sprawiła jednak, że zacząłem się zastanawiać, czy rozwiązanie 2.1 z odpowiedzi @ n__o / @ mlissner, która opiera się na, ma objects = serializers.deserialize('json', fixture, ignorenonexistent=True)ten sam problem, co loaddata? Czy ignorenonexistent=Trueobejmuje wszystkie możliwe problemy?
Dário
7
Jeśli spojrzysz na źródło , zauważysz, że ignorenonexistent=Trueargument ma dwa efekty: 1) ignoruje modele urządzenia, które nie znajdują się w najbardziej aktualnych definicjach modelu, 2) ignoruje pola modelu urządzenia, które nie są w najbardziej aktualnej definicji odpowiedniego modelu. Żaden z nich nie radzi sobie z sytuacją nowego wymaganego pola w modelu . Więc tak, myślę, że cierpi na ten sam problem, co zwykły loaddata.
Rockallite
To zadziałało świetnie, gdy zorientowałem się, że mój stary json miał modele odwołujące się do innych modeli przy użyciu a natural_key(), czego ta metoda nie wydaje się obsługiwać - po prostu zastąpiłem wartość natural_key rzeczywistym identyfikatorem modelu, do którego się odwołuje.
dsummersl
1
Prawdopodobnie ta odpowiedź jako zaakceptowana byłaby bardziej pomocna, ponieważ podczas uruchamiania przypadków testowych tworzona jest nowa baza danych i wszystkie migracje są stosowane od podstaw. To rozwiązanie rozwiązuje problemy, które napotka projekt z unittest w przypadku niezastąpienia _get_model w migracji danych. Tnx
Mohammad ali baghershemirani
Dzięki za aktualizację i wyjaśnienia, @Rockallite. Moja wstępna odpowiedź została opublikowana kilka tygodni po wprowadzeniu migracji w Django 1.7, a dokumentacja dotycząca tego, jak postępować, była niejasna (i nadal jest, kiedy ostatnio sprawdzałem). Miejmy nadzieję, że Django zaktualizuje pewnego dnia swój mechanizm ładowania / migracji danych, aby uwzględnić historię modelu.
n__o
6

Zainspirowany niektórymi komentarzami (a mianowicie n__o) i faktem, że mam dużo initial_data.*plików rozrzuconych po wielu aplikacjach, zdecydowałem się stworzyć aplikację Django, która ułatwiłaby tworzenie tych migracji danych.

Korzystanie django-migracja-Uchwyt można po prostu uruchom następujące polecenie zarządzania i będzie wyszukiwanie wszystkich INSTALLED_APPSdo initial_data.*plików i włączyć je do migracji danych.

./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.

Zobacz django-migracja-fixture, aby uzyskać instrukcje dotyczące instalacji / użytkowania.

alexhayes
źródło
2

Aby dać swojej bazie danych trochę danych początkowych, napisz migrację danych. Podczas migracji danych użyj funkcji RunPython, aby załadować dane.

Nie pisz żadnej komendy loaddata, ponieważ ta metoda jest przestarzała.

Twoje migracje danych zostaną uruchomione tylko raz. Migracje są uporządkowaną sekwencją migracji. Po uruchomieniu migracji 003_xxxx.py django migrations zapisuje w bazie danych, że ta aplikacja jest migrowana aż do tej (003) i uruchomi tylko poniższe migracje.

FlogFR
źródło
Więc myModel.create(...)zachęcasz mnie do powtarzania wywołań (lub używania pętli) w funkcji RunPython?
Mickaël
prawie tak. Transakcjonalne bazy danych sobie z tym
poradzą
1

Przedstawione powyżej rozwiązania niestety nie zadziałały. Odkryłem, że za każdym razem, gdy zmieniam modele, muszę aktualizować swoje urządzenia. Idealnie byłoby zamiast tego pisać migracje danych, aby podobnie modyfikować utworzone dane i dane ładowane przez urządzenia.

Aby to ułatwić , napisałem szybką funkcję, która zajrzy do fixtureskatalogu bieżącej aplikacji i załaduje urządzenie. Umieść tę funkcję w migracji w punkcie historii modelu, który jest zgodny z polami w migracji.

leifdenby
źródło
Dzięki za to! Napisałem wersję, która działa z Pythonem 3 (i spełnia nasz ścisły Pylint). Możesz go używać jako fabryki z RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle Madeley
1

Moim zdaniem urządzenia są trochę kiepskie. Jeśli baza danych zmienia się często, aktualizowanie ich wkrótce stanie się koszmarem. Właściwie to nie tylko moja opinia, w książce „Two Scoops of Django” jest to wyjaśnione znacznie lepiej.

Zamiast tego napiszę plik w Pythonie, aby zapewnić początkową konfigurację. Jeśli potrzebujesz czegoś więcej, proponuję zajrzeć do Factory boy .

Jeśli musisz migrować niektóre dane, powinieneś użyć migracji danych .

Jest też „Burn Your Fixtures, Use Model Factories” o używaniu urządzeń.

Griffosx
źródło
1
Zgadzam się na swoim miejscu „trudne do utrzymania, jeśli częste zmiany”, ale tu tylko uchwytu ma na celu zapewnienie początkowych (i minimalne) dane podczas instalacji projektu ...
Mickaël
1
Dotyczy to jednorazowego załadowania danych, co ma sens, jeśli jest wykonywane w kontekście migracji. Ponieważ jeśli jest to migracja, nie należy wprowadzać zmian w danych json. Wszelkie zmiany schematu, które wymagają zmian w danych w dalszej części drogi, powinny być obsługiwane przez kolejną migrację (w tym momencie inne dane mogą być w bazie danych, które również będą wymagały modyfikacji).
mtnpaul
0

W Django 2.1 chciałem załadować niektóre modele (na przykład nazwy krajów) z danymi początkowymi.

Ale chciałem, żeby stało się to automatycznie zaraz po wykonaniu początkowych migracji.

Pomyślałem więc, że byłoby wspaniale mieć sql/folder w każdej aplikacji, który wymagałby załadowania danych początkowych.

Następnie w tym sql/folderze miałbym .sqlpliki z wymaganymi plikami DML, aby załadować dane początkowe do odpowiednich modeli, na przykład:

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

Aby być bardziej opisowym, tak wyglądałaby aplikacja zawierająca sql/folder: wprowadź opis obrazu tutaj

Znalazłem również przypadki, w których potrzebowałem sqlskryptów do wykonania w określonej kolejności. Postanowiłem więc poprzedzić nazwy plików kolejnym numerem, jak widać na powyższym obrazku.

Potem potrzebowałem sposobu, aby automatycznie załadować wszystkie SQLsdostępne w dowolnym folderze aplikacji, wykonując python manage.py migrate.

Utworzyłem więc inną aplikację o nazwie, initial_data_migrationsa następnie dodałem tę aplikację do listy INSTALLED_APPSw settings.pypliku. Następnie utworzyłem migrationsfolder w środku i dodałem plik o nazwie run_sql_scripts.py( co w rzeczywistości jest niestandardową migracją ). Jak widać na poniższym obrazku:

wprowadź opis obrazu tutaj

Stworzyłem run_sql_scripts.pytak, aby zadbał o uruchomienie wszystkich sqlskryptów dostępnych w ramach każdej aplikacji. Ten jest następnie odpalany, gdy ktoś biegnie python manage.py migrate. Ten niestandardowy migrationdodaje również zaangażowane aplikacje jako zależności, w ten sposób próbuje uruchomić sqlinstrukcje dopiero po wykonaniu 0001_initial.pymigracji przez wymagane aplikacje (nie chcemy próbować uruchamiać instrukcji SQL na nieistniejącej tabeli).

Oto źródło tego skryptu:

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))
    ]

Mam nadzieję, że ktoś uzna to za pomocne, działało dobrze dla mnie !. Jeśli masz jakieś pytania, daj mi znać.

UWAGA: To może nie być najlepsze rozwiązanie, ponieważ dopiero zaczynam pracę z django, jednak nadal chciałem się z wami podzielić tym "Jak to zrobić", ponieważ nie znalazłem zbyt wielu informacji podczas wyszukiwania go w Google.

Antony Fuentes Artavia
źródło