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.