Laden der Anfangsdaten mit Django 1.7 und Datenmigrationen

95

Ich habe kürzlich von Django 1.6 auf 1.7 umgestellt und angefangen, Migrationen zu verwenden (ich habe South nie verwendet).

Vor 1.7 habe ich Anfangsdaten mit einer fixture/initial_data.jsonDatei geladen, die mit dem python manage.py syncdbBefehl geladen wurde (beim Erstellen der Datenbank).

Jetzt habe ich angefangen, Migrationen zu verwenden, und dieses Verhalten ist veraltet:

Wenn eine Anwendung Migrationen verwendet, werden keine Fixtures automatisch geladen. Da für Anwendungen in Django 2.0 Migrationen erforderlich sind, gilt dieses Verhalten als veraltet. Wenn Sie Anfangsdaten für eine App laden möchten, sollten Sie dies bei einer Datenmigration in Betracht ziehen. ( https://docs.djangoproject.com/de/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

Die offizielle Dokumentation enthält kein klares Beispiel dafür, daher lautet meine Frage:

Was ist der beste Weg, um solche Anfangsdaten mithilfe von Datenmigrationen zu importieren:

  1. Schreiben Sie Python-Code mit mehreren Aufrufen an mymodel.create(...),
  2. Verwenden oder schreiben Sie eine Django-Funktion ( wie das Aufrufenloaddata ), um Daten aus einer JSON-Fixture-Datei zu laden.

Ich bevorzuge die zweite Option.

Ich möchte South nicht verwenden, da Django dies jetzt nativ zu tun scheint.

Mickaël
quelle
3
Außerdem möchte ich der ursprünglichen Frage des OP eine weitere Frage hinzufügen: Wie sollen Datenmigrationen für Daten durchgeführt werden, die nicht zu unseren Anwendungen gehören? Wenn beispielsweise jemand das Site-Framework verwendet, muss er mit den Site-Daten übereinstimmen. Wo sollten wir diese Datenmigration platzieren, da das Site-Framework nicht mit unseren Anwendungen zusammenhängt? Vielen Dank !
Serafeim
Ein wichtiger Punkt, der hier noch von niemandem angesprochen wurde, ist das, was passiert, wenn Sie Daten, die bei einer Datenmigration definiert wurden, zu einer Datenbank hinzufügen müssen, für die Sie Migrationen gefälscht haben. Da die Migrationen gefälscht wurden, wird Ihre Datenmigration nicht ausgeführt und Sie müssen dies von Hand tun. An dieser Stelle können Sie auch einfach loaddata für eine Fixture-Datei aufrufen.
Hekevintran
Ein weiteres interessantes Szenario ist, was passiert, wenn Sie beispielsweise eine Datenmigration zum Erstellen von auth.Group-Instanzen durchführen und später eine neue Gruppe haben, die Sie als Startdaten erstellen möchten. Sie müssen eine neue Datenmigration erstellen. Dies kann ärgerlich sein, da sich Ihre Group Seed-Daten in mehreren Dateien befinden. Auch für den Fall, dass Sie Migrationen zurücksetzen möchten, müssen Sie nach den Datenmigrationen suchen, mit denen Startdaten eingerichtet wurden, und diese ebenfalls portieren.
Hekevintran
@Serafeim Die Frage "Wo werden die ursprünglichen Daten für eine Drittanbieter-App abgelegt?" Ändert sich nicht, wenn Sie eine Datenmigration anstelle von Fixtures verwenden, da Sie nur die Art und Weise ändern, in der die Daten geladen werden. Ich benutze eine kleine benutzerdefinierte App für solche Dinge. Wenn die Drittanbieter-App "foo" heißt, nenne ich meine einfache App mit der Datenmigration / Fixture "foo_integration".
Guettli
@guettli ja, wahrscheinlich ist die Verwendung einer zusätzlichen Anwendung der beste Weg, dies zu tun!
Serafeim

Antworten:

81

Update : Siehe @ GwynBleidDs Kommentar unten für die Probleme, die diese Lösung verursachen kann, und siehe @ Rockallites Antwort unten für einen Ansatz, der für zukünftige Modelländerungen dauerhafter ist.


Angenommen, Sie haben eine Fixture-Datei in <yourapp>/fixtures/initial_data.json

  1. Erstellen Sie Ihre leere Migration:

    In Django 1.7:

    python manage.py makemigrations --empty <yourapp>

    In Django 1.8+ können Sie einen Namen angeben:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
  2. Bearbeiten Sie Ihre Migrationsdatei <yourapp>/migrations/0002_auto_xxx.py

    2.1. Benutzerdefinierte Implementierung, inspiriert von Django ' loaddata(erste Antwort):

    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. Eine einfachere Lösung für load_fixture(per @ juliocesar Vorschlag):

    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) 

    Nützlich, wenn Sie ein benutzerdefiniertes Verzeichnis verwenden möchten.

    2.3. Einfachstes: Aufruf loaddatamit app_labelWillen Lastbefestigungen aus dem <yourapp>‚s fixturesdir automatisch:

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

    Wenn Sie nichts angeben app_label, versucht loaddata, den fixtureDateinamen aus allen Apps-Fixtures-Verzeichnissen zu laden (was Sie wahrscheinlich nicht möchten).

  3. Starte es

    python manage.py migrate <yourapp>
Nein
quelle
1
ok, du hast recht ... Auch das Anrufen loaddata('loaddata', fixture_filename, app_label='<yourapp>')geht direkt zum App-Fixture-Verzeichnis (daher muss nicht der vollständige Pfad des
Fixtures erstellt werden
15
Mit dieser Methode arbeitet der Serializer mit dem Modellstatus aus aktuellen models.pyDateien, die einige zusätzliche Felder oder andere Änderungen enthalten können. Wenn nach dem Erstellen der Migration einige Änderungen vorgenommen wurden, schlägt dies fehl (daher können wir nach dieser Migration nicht einmal Schemamigrationen erstellen). Um dies zu beheben, können wir die Apps-Registrierung, an der der Serializer arbeitet, teporaly in die Registrierung ändern, die für die Migrationsfunktion beim ersten Parameter bereitgestellt wird. Die Registrierung zum Pfad befindet sich unter django.core.serializers.python.apps.
GwynBleidD
3
Warum machen wir das? Warum wird es immer schwieriger, Django zu betreiben und zu warten? Ich möchte dies nicht tun, ich möchte eine einfache Befehlszeilenschnittstelle, die dieses Problem für mich löst, dh wie es früher bei Geräten der Fall war. Django soll dieses Zeug einfacher machen, nicht schwerer :(
CpILL
1
@GwynBleidD Dies ist ein sehr wichtiger Punkt, den Sie ansprechen, und ich denke, er sollte in dieser akzeptierten Antwort erscheinen. Dieselbe Bemerkung wird im Beispiel für den Datenmigrationscode der Dokumentation als Kommentar angezeigt . Kennen Sie eine andere Möglichkeit, Serialisierer mit den bereitgestellten zu verwenden app registry, ohne eine globale Variable zu ändern (was in einer hypothetischen Zukunft bei parallelen Datenbankmigrationen zu Problemen führen kann) ?
Anzeige N
3
Diese Antwort, die zusammen mit der Akzeptanz auf Kazoo hochgestuft wird, ist genau der Grund, warum ich Leuten empfehle, Stackoverflow nicht zu verwenden. Selbst jetzt mit den Kommentaren und Anekdoten habe ich immer noch Leute in #django, die sich darauf beziehen.
shangxiao
50

Kurzfassung

Sie sollten den Verwaltungsbefehl NICHTloaddata direkt in einer Datenmigration verwenden.

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

Lange Version

loaddatanutzt , django.core.serializers.python.Deserializerdie die meisten up-to-date verwendet Modelle , historische Daten in einer Migration deserialisieren. Das ist falsches Verhalten.

Angenommen, es gibt eine Datenmigration, bei der der loaddataVerwaltungsbefehl zum Laden von Daten von einem Fixture verwendet wird und die bereits auf Ihre Entwicklungsumgebung angewendet wird.

Später beschließen Sie, dem entsprechenden Modell ein neues erforderliches Feld hinzuzufügen. Führen Sie dies aus und führen Sie eine neue Migration für Ihr aktualisiertes Modell durch (und geben Sie dem neuen Feld möglicherweise einen einmaligen Wert, wenn ./manage.py makemigrationsSie dazu aufgefordert werden ).

Sie führen die nächste Migration aus und alles ist gut.

Schließlich sind Sie mit der Entwicklung Ihrer Django-Anwendung fertig und stellen sie auf dem Produktionsserver bereit. Jetzt ist es Zeit für Sie, die gesamten Migrationen in der Produktionsumgebung von Grund auf neu auszuführen.

Die Datenmigration schlägt jedoch fehl . Dies liegt daran, dass das deserialisierte Modell aus dem loaddataBefehl, der den aktuellen Code darstellt, nicht mit leeren Daten für das neue erforderliche Feld gespeichert werden kann, das Sie hinzugefügt haben. Dem Originalgerät fehlen die notwendigen Daten dafür!

Aber selbst wenn Sie das Gerät mit den erforderlichen Daten für das neue Feld aktualisieren, schlägt die Datenmigration immer noch fehl . Wenn die Datenmigration ausgeführt wird, wird die nächste Migration, bei der die entsprechende Spalte zur Datenbank hinzugefügt wird, noch nicht angewendet. Sie können keine Daten in einer nicht vorhandenen Spalte speichern!

Schlussfolgerung: Bei einer Datenmigration führt derloaddataBefehl zu potenziellen Inkonsistenzen zwischen dem Modell und der Datenbank. Sie sollten es definitiv NICHT direkt in einer Datenmigration verwenden.

Die Lösung

loaddataDer Befehl basiert auf der django.core.serializers.python._get_modelFunktion, um das entsprechende Modell von einem Gerät abzurufen, das die aktuellste Version eines Modells zurückgibt. Wir müssen es mit Affen patchen, damit es das historische Modell erhält.

(Der folgende Code funktioniert für 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
quelle
1
Rockallite, du machst einen sehr starken Punkt. Ihre Antwort ließ mich jedoch fragen, ob die Lösung 2.1 aus der Antwort von @ n__o / @ mlissner, die sich darauf stützt, objects = serializers.deserialize('json', fixture, ignorenonexistent=True)unter demselben Problem leiden würde wie loaddata? Oder ignorenonexistent=Truedeckt alle möglichen Probleme ab?
Dário
7
Wenn Sie sich die Quelle ansehen , werden Sie feststellen, dass das ignorenonexistent=TrueArgument zwei Auswirkungen hat: 1) Es ignoriert Modelle eines Geräts, die nicht in den aktuellsten Modelldefinitionen enthalten sind, 2) Es ignoriert Felder eines Modells eines Geräts, die dies nicht sind in der aktuellsten entsprechenden Modelldefinition. Keiner von ihnen behandelt die Situation des neuen erforderlichen Feldes im Modell . Also, ja, ich denke, es hat das gleiche Problem wie einfach loaddata.
Rockallite
Dies funktionierte hervorragend, als ich herausfand, dass mein alter json Modelle hatte, auf die andere Modelle mit a verwiesen wurden natural_key(), was diese Methode nicht zu unterstützen scheint. Ich habe nur den Wert natural_key durch die tatsächliche ID des referenzierten Modells ersetzt.
dsummersl
1
Wahrscheinlich wäre diese Antwort als akzeptierte Antwort hilfreicher, da beim Ausführen von Testfällen eine neue Datenbank erstellt wird und alle Migrationen von Grund auf neu angewendet werden. Diese Lösung behebt Probleme, mit denen ein Projekt mit unittest konfrontiert wird, wenn _get_model bei der Datenmigration nicht ersetzt wird. Tnx
Mohammad ali baghershemirani
Vielen Dank für das Update und die Erklärungen, @Rockallite. Meine erste Antwort wurde einige Wochen nach Einführung der Migrationen in Django 1.7 veröffentlicht, und die Dokumentation zum weiteren Vorgehen war unklar (und ist es immer noch, als ich das letzte Mal nachgesehen habe). Hoffentlich wird Django eines Tages seinen Ladedaten- / Migrationsmechanismus aktualisieren, um den Modellverlauf zu berücksichtigen.
n__o
6

Inspiriert von einigen Kommentaren (n__o's) und der Tatsache, dass viele initial_data.*Dateien auf mehrere Apps verteilt sind, habe ich beschlossen, eine Django-App zu erstellen, die die Erstellung dieser Datenmigrationen erleichtert.

Mit django-Migration-Halterung können Sie einfach den folgenden Management - Befehl ausführen , und es wird durch all Ihre Suche INSTALLED_APPSnach initial_data.*Dateien und sie in Datenmigrationen.

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

Anweisungen zur Installation / Verwendung finden Sie unter django-migrations-fixture .

alexhayes
quelle
2

Schreiben Sie eine Datenmigration , um Ihrer Datenbank erste Daten zu geben . Verwenden Sie bei der Datenmigration die RunPython- Funktion, um Ihre Daten zu laden.

Schreiben Sie keinen Befehl loaddata, da dieser Weg veraltet ist.

Ihre Datenmigrationen werden nur einmal ausgeführt. Die Migrationen sind eine geordnete Folge von Migrationen. Wenn die Migrationen 003_xxxx.py ausgeführt werden, schreibt django migrations in die Datenbank, dass diese App bis zu dieser migriert wird (003), und führt nur die folgenden Migrationen aus.

FlogFR
quelle
Sie ermutigen mich also, Aufrufe myModel.create(...)(oder die Verwendung einer Schleife) in der RunPython-Funktion zu wiederholen ?
Mickaël
so ziemlich ja. Transaactionnal Datenbanken werden perfekt damit umgehen :)
FlogFR
1

Die oben vorgestellten Lösungen haben bei mir leider nicht funktioniert. Ich habe festgestellt, dass ich jedes Mal, wenn ich meine Modelle ändere, meine Geräte aktualisieren muss. Im Idealfall würde ich stattdessen Datenmigrationen schreiben, um erstellte Daten und mit Geräten geladene Daten auf ähnliche Weise zu ändern.

Um dies zu erleichtern, habe ich eine Schnellfunktion geschrieben, die im fixturesVerzeichnis der aktuellen App nachschaut und ein Gerät lädt. Fügen Sie diese Funktion an der Stelle des Modellverlaufs in eine Migration ein, die mit den Feldern in der Migration übereinstimmt.

leifdenby
quelle
Danke dafür! Ich habe eine Version geschrieben, die mit Python 3 funktioniert (und unseren strengen Pylint besteht). Sie können es als Fabrik mit verwenden RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle Madeley
1

Meiner Meinung nach sind die Spiele etwas schlecht. Wenn sich Ihre Datenbank häufig ändert, wird es bald ein Albtraum sein, sie auf dem neuesten Stand zu halten. Eigentlich ist es nicht nur meine Meinung, in dem Buch "Two Scoops of Django" wird es viel besser erklärt.

Stattdessen schreibe ich eine Python-Datei, um die Ersteinrichtung zu ermöglichen. Wenn Sie etwas mehr brauchen, schlage ich vor, dass Sie sich Factory Boy ansehen .

Wenn Sie einige Daten migrieren müssen, sollten Sie Datenmigrationen verwenden .

Es gibt auch "Brennen Sie Ihre Geräte, verwenden Sie Modellfabriken" über die Verwendung von Geräten.

Griffosx
quelle
1
Ich stimme Ihrem Punkt zu, "schwer zu pflegen, wenn häufige Änderungen vorgenommen werden", aber hier zielt das Gerät nur darauf ab, anfängliche (und minimale) Daten bei der Installation des Projekts bereitzustellen ...
Mickaël
1
Dies ist eine einmalige Datenladung, die sinnvoll ist, wenn sie im Rahmen von Migrationen durchgeführt wird. Da es sich um eine Migration handelt, sollten keine Änderungen an den JSON-Daten vorgenommen werden müssen. Alle Schemaänderungen, die später Änderungen an den Daten erfordern, sollten über eine andere Migration behandelt werden (zu diesem Zeitpunkt befinden sich möglicherweise andere Daten in der Datenbank, die ebenfalls geändert werden müssen).
Mtnpaul
0

Auf Django 2.1 wollte ich einige Modelle (wie z. B. Ländernamen) mit Anfangsdaten laden.

Ich wollte jedoch, dass dies direkt nach der Ausführung der ersten Migrationen automatisch geschieht.

Daher dachte ich, dass es großartig wäre, sql/in jeder Anwendung einen Ordner zu haben , in den die anfänglichen Daten geladen werden müssen.

Dann sql/hätte ich in diesem Ordner .sqlDateien mit den erforderlichen DMLs, um die Anfangsdaten in die entsprechenden Modelle zu laden, zum Beispiel:

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

Um es genauer zu beschreiben, sql/würde eine App mit einem Ordner folgendermaßen aussehen: Geben Sie hier die Bildbeschreibung ein

Außerdem habe ich einige Fälle gefunden, in denen die sqlSkripte in einer bestimmten Reihenfolge ausgeführt werden mussten. Deshalb habe ich beschlossen, den Dateinamen eine fortlaufende Nummer voranzustellen, wie im obigen Bild gezeigt.

Dann brauchte ich eine Möglichkeit, alle SQLsverfügbaren Inhalte in einem Anwendungsordner automatisch zu laden python manage.py migrate.

Also habe ich eine andere Anwendung mit dem Namen initial_data_migrationsund dann habe ich diese App in die Liste der INSTALLED_APPSin settings.pyDatei. Dann habe ich einen migrationsOrdner darin erstellt und eine Datei namens run_sql_scripts.py( was eigentlich eine benutzerdefinierte Migration ist ) hinzugefügt . Wie im Bild unten zu sehen:

Geben Sie hier die Bildbeschreibung ein

Ich habe es run_sql_scripts.pyso erstellt , dass alle sqlin jeder Anwendung verfügbaren Skripte ausgeführt werden. Dieser wird dann abgefeuert, wenn jemand rennt python manage.py migrate. Dieser Brauch migrationfügt auch die beteiligten Anwendungen als Abhängigkeiten hinzu. Auf diese Weise wird versucht, die sqlAnweisungen erst auszuführen, nachdem die erforderlichen Anwendungen ihre 0001_initial.pyMigrationen ausgeführt haben (wir möchten nicht versuchen, eine SQL-Anweisung für eine nicht vorhandene Tabelle auszuführen ).

Hier ist die Quelle dieses Skripts:

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

Ich hoffe, jemand findet das hilfreich, es hat gut für mich funktioniert!. Wenn Sie Fragen haben, lassen Sie es mich bitte wissen.

HINWEIS: Dies ist möglicherweise nicht die beste Lösung, da ich gerade erst mit Django angefangen habe, aber trotzdem dieses "How-to" mit Ihnen allen teilen wollte, da ich beim Googeln nicht viele Informationen gefunden habe.

Antony Fuentes Artavia
quelle