Serialisierte Migration: Von MySQL zu PostgreSQL

Bei meinem fehlgeschlagenen Versuch, eine MySQL-Datenbank in eine PostgreSQL-Datenbank zu überführen, hat es mir nicht nur an technischer Expertise, sondern auch an Vertrauen in ein bewährtes Werkzeug gemangelt: Zwar hatte ich syncdb bereits genutzt, um das Datenbankschema in PostgreSQL zu replizieren, war dann aber dazu übergegangen, die Ausgabe von mysqldump für die frische Datenbank zurechtzudengeln. Nochmal von vorn.

Die Installation von PostgreSQL auf meinem Mac und meinem CentOS-Server gestaltet sich recht einfach. Auf dem Mac müssen gemäß README lediglich einige Kernelparameter angepasst werden (PostgreSQL benötigt deutlich mehr shared memory als von Apple vorgesehen), anschließend installiert das Paket sich ohne Weiteres in /Library/PostgreSQL/9.0 und legt freundlicherweise auch noch einen LaunchDaemon an. Unter CentOS kann eine aktuelle PostgreSQL-Version wie üblich nur halbautomatisch installiert werden, weshalb man auch hier wieder einen entsprechenden exclude-Eintrag in /etc/yum.conf vornehmen sollte.

Den Komfort von .my.cnf – ein passwortloser Login mit dem Account des DB-Nutzers – erreicht man durch die Kombination der Datei .pgpass und der Umgebungsvariablen PGUSER. Für die ersten Schritte genügt dieses kleine Glossar:

MySQL PostgreSQL Funktion
mysql [dbname] psql dbname Aufbau des Datenbankterminals (in PostgreSQL ist die Angabe einer Datenbank obligatorisch, wenn keine Datenbank mit dem Namen des Datenbanknutzers existiert)
USE DATABASE dbname \c dbname Verbindung mit einer bestimmten Datenbank
SHOW TABLES\d Anzeige aller Tabellen der aktuellen Datenbank
DESCRIBE TABLE tablename \d tablename Anzeige der Struktur einer Tabelle

Weil Django bei den folgenden Schritten eine große Rolle spielen wird, muss auch psycopg, der PostgreSQL-Adapter für Python, auf beiden Systemen installiert werden. Auf dem Mac gestaltet sich die Kompilation unspektakulär, für CentOS steht eine vergleichsweise aktuelle Version (2.0.x) auch im offiziellen Repository bereit.

Der Transfer der Daten ist genau dann ganz einfach, wenn die Datenbank nicht allzu groß ist (für größere Datenmengen empfiehlt sich ein nur unwesentlich komplexerer iterativer Ansatz oder eine sehr aufwendige Kombination von Django, Bash und SQLAlchemy):

cd ~/Sites/projectdir mkdir fixtures python manage.py dumpdata appname1 [appname2] [appname3] > fixtures/initial_data.json vi settings.py DATABASE_ENGINE = 'mysql' → DATABASE_ENGINE = 'postgresql_psycopg2' FIXTURE_DIRS = ('/Users/jan/Sites/djangoproject/fixtures/',) ZZ python manage.py syncdb

Das funktioniert ganz großartig, wenn die ursprüngliche Datenbank keine Inkonsistenzen enthält. Andernfalls moniert Djangos Serializer fehlende Fremdschlüssel. Ist dieses Problem behoben, müssen nur noch einige NULL-Werte in '0' (für Boolean-Felder) bzw '' (für Textfelder) umgewandelt werden.

Ein weiteres kleines Problem kann entstehen, wenn eines der transferierten Modelle eine individuelle __init__-Methode enthält, die einzelne Datensätze basierend auf einem Feldwert als Instanz einer Unterklasse initialisiert:

def __init__(self, *args, **kwargs): super(Page, self).__init__(*args, **kwargs) subclasses = { 'B' : BlogPage, 'L' : LogPage, 'P' : Page, } self.__class__ = subclasses[self.page_type]

initial_data.json enthält zwar die entsprechenden Datensätze ('model' : 'appname.blogpage' bzw. 'model' : 'appname.logpage'), scheitert aber bei der korrekten Übertragung der Feldwerte. Kommentiert man die __init__-Methode in models.py vor dem Datendump aus, tritt dieses Problem nicht auf.

Geschafft. Im ungetunten Zustand ist nicht mal ein signifikanter Leistungsunterschied feststellbar:

ab -n1000 -c10 http://janeden.django:8080/mommsen

wird ohne Caching von PostgreSQL in 65,902 (66,350) Sekunden abgearbeitet, MySQL (5.1.45) benötigt 67,393 (67,999) Sekunden.