Probleme bei der Versionierung von Migrationen

Bei Arbeiten im Team bzw. bei einem Workflow mit sogenannten Feature-Branches mit einem Versionierungsystem wie git kommt es immer wieder zu Problemen mit den Migrationen, die von Django erstellt werden. Wenn zum Beispiel in einem Feature-Branch die Datenbank durch eine Migrationen verändert wurde, ist diese Veränderung nicht in einem anderen Branch, zum Beispiel dem Main-Branch verfügbar.

Ähnliche Probleme ergeben sich, wenn zwei Personen an der gleichen App arbeiten und Model-Felder ändern. Dann kann es zum Beispiel plötzlich zwei Migrationsdateien mit dem Präfix 0002 haben.

Strategien mit Migrationsdateien

Das Problem ist nicht neu und die Lösung nicht trivial. Es gibt eigentlich auch keine gute, allgemeingültige Lösung dafür, nur ein paar Regeln, wie man mit dem Problem umgeht.

Vermeidet, gleichzeitig an einer App zu arbeiten

oder zumindest sollte nur einer von den Beteiligten Änderungen an einem Model bzw. einer View vornehmen. Gute Absprachen sind wichtig, um später Komplikationen zu vermeiden.

Umbennen der Migrationsdateien

Falls doch mal zwei Migrationsdateien mit dem selben numerischen Präfix im Code landen, kann umbenannt werden. Das Dependencies-Attribut nicht vergessen, anzupassen.

Migration Rollback

Falls man in den wichtigen Main-Branch wechselt (checkout), könnte man auf dem Feature-Branch einen Rollback der Migration machen. Das geht in Django relativ einfach, in dem man einfach die Nummer der Migration bei migrate angibt.

python manage.py migrate events 0001

Hier migrieren wir gezielt die Migration 0001, die Datenbank wird daraufhin in den Zustand zurückgesetzt. Jetzt könnte man in den Main-branch wechseln, und dort mit dem Zustand der Datenbank wie vorher, weiterarbeiten.

Branch Datenbanken

Falls im Feature-Branch Arbeiten anstehen, die das Datenbank-Schema massiv verändern und vieleicht sogar Daten-Migrationen durchgeführt werden müssen, ist es zu überlgen, ob man nicht eine eigene Datenbank im Feature-Branch anlegt. Mehr zu dem Thema hier: http://ses4j.github.io/2019/05/31/django-maintaining-database-per-branch-git-hook/

Daten-Migration

Falls ein eindeutiger Slug in schon bestehende Daten eingefügt werden muss, aber die Daten nicht gelöscht werden können, können wir nach folgendem Muster vorgehen. Es sind drei Makemigrations und eine Migration nötig, um das Ziel zu erreichen.

1) Beispiel: Ein Slug-Field erstellen mit null=True

Erstmal setzen wir das Slugfield weder auf unique noch machen wir es mandatory. Das etwas seltsam anmutende Konstrukt macht aber was es soll, und zwar Null Values und doppelte Vorkommen erstmal zu erlauben.

slug = models.SlugField(null=True)

Dann führen wir die Migration für die App pets aus:

python manage.py makemigrations pets

An dieser Stelle sollte nichts schiefgehen, da wir einfach eine neue Spalte in die Tabelle eingetragen haben. Es wurde eine Migrationsdatei erstellt.

2. Erstelle eine Daten-Migration

und sorge dafür, dass die Daten eindeutig eingetragen werden.

Daten-Migration

Django Datenmigrationen sind eine bequeme Möglichkeit, Daten in einer Datenbank bei Änderung des Datenbank-Schemas gleich mit zu ändern. Sie funktionieren im Grunde wie normale Migrationen, verändern aber statt des DB-Schemas Daten. So könnte man neue Daten auf Basis schon bestehender Daten in ein Feld einfügen.

Ein häufiger Anwendungsfall für Datenmigrationen ist die Implementierung neuer Felder, die nicht optional sind und mit eindeutigen Werten gefüllt werden müssen, wie zum Beispiel ein eindeutiges Pflicht-Slug-Feld in einer bereits bestehenden Tabelle. Man könnte das zwar mühselig und händisch selber machen, aber die Team-Kollegen stehen dann vor dem gleichen Problem.

Zum Erstellen von Datenmigrationen wird eine leere Migrationsdatei mit dem Argument --empty erstellt. Über den RunPython-Befehl wird Django mitgeteilt, welche Funktion beim Migrieren ausgeführt werden soll. Hier hat man Zugriff auf das Model und kann wie gewohnt mit dem ORM arbeiten.

Die zweite Funktionsreferenz in RunPython gibt an, was bei einem Rollback passieren soll. Wird hier nichts angegeben, scheitert die Migration mit einer Exception. migrations.RunPython.noop verhindert das Scheitern und macht einfach gar nichts.

Mehr zum Thema Datenmigrationen in der Doku: https://docs.djangoproject.com/en/stable/topics/migrations/#data-migrations

Dafür erstellen wir eine leere Migrationsdatei mit empty

python manage.py makemigrations pets --empty

und tragen in die neu erstelle Migrationsdatei folgenden Code ein:

from django.db import migrations
from django.utils.text import slugify


def populate_slug_field(apps, schema_editor):

    petmodel = apps.get_model("pets", "Pet")
    for pet in petmodel.objects.all():
        pet.slug = slugify(pet)
        pet.save()


class Migration(migrations.Migration):

    dependencies = [
        ("pets", "0002_pet_slug"),
    ]

    operations = [
        migrations.RunPython(
            populate_slug_field,
            migrations.RunPython.noop
        ),
    ]

Die Operations-Liste sorgt mit dem Befehl migrations.RunPython dafür, dass auf der Datenbank später beim Migrieren das Skript populate_slug_field ausgeführt wird. Ansonsten macht diese Migration nichts weiter. migrations.RunPython.noop ist die Funktion, die bei Rollback ausgeführt würde, sie macht einfach nichts.

populate_slug_field macht aus den schon vorhandenen Einträgen jeweils einen Slug und füllt damit die entsprechende Spalte. Hier sollte darauf geachtet werden, dass alle Werte eindeutig sind, da es ansonsten später zu einem Fehler kommt. Schließlich soll das Slug-Feld unique=True sein.

3. der letzte Schritt

Verändere das Model und erstelle die dritte Migration. Dann erst können wir migrieren.

Wir nehmen also die null=True - Eigenschaft wieder aus dem Slugfield und setzen das Feld jetzt endlich auf unique=True.

slug = models.SlugField(unique=True)

und führen die dritte MakeMigration für diese App aus:

python manage.py makemigrations pets

Django merkt, dass wir ein Pflichtfeld im Model haben (slug), und will nun wissen, wie die schon vorhandenen Objekte in der Tabelle damit umgehen sollen. Da wir eine Datenmigration geschrieben haben, die unsere Datenbank-Tabelle füllen wird, ignorieren wir diese Nachricht und lassen RunPython den Rest erledigen. Deshalb wählen wir 2) Ignore for now

It is impossible to change a nullable field 'slug' on tag to non-nullable without providing a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Ignore for now. Existing rows that contain NULL values will have to be handled manually, for example with a RunPython or RunSQL operation.
3) Quit and manually define a default value in models.py.
Select an option: 2


python manage.py migrate pets

Wenn die Migrationen anstandslos durchgelaufen sind, prüfen wir das Ergebnis gleich mal in der Datenbank.