Refactoring von Datenbanken
Scott Ambler hat eine Menge für die Verbreitung agiler Vorgehensweisen bei der Softwareentwicklung getan. Es gibt aber einen Punkt, in dem ich jedes Mal, wenn ich in Konferenzen mit ihm zusammentreffe, erhebliche Meinungsunterschiede feststelle. Dabei handelt es sich um den agilen Umgang mit Datenbanken. Bislang sind wir noch nicht zu einer gemeinsamen Sichtweise zu gekommen. In einem Punkt stimme ich jedoch zu. Wenn ein Datenbankschema ein Problem zeigt, so sollte man versuchen, es zu beheben. Und es ist sicher Zeit, die hierzu notwendigen Techniken zu katalogisieren, wie es Fowler für das Refactoring von Software gemacht hat.
Was ist speziell an Datenbanken?
Nicht speziell ist, daß es häufig zahlreiche Nutzer einer Datenbank gibt, die zum Teil noch nicht einmal bekannt sind. Dieses Problem gibt es auch in der Softwareentwicklung, wenn man sich auf das heutzutage so populäre Paradigma der Serviceorientierung einläßt.Was aber besonders ist, ist daß die Datenbank Daten enthält, die wesentlich länger Bestand haben als alle Programme, die damit umgehen. Diese Daten sind ebenso wertvoll wie empfindlich. Hier gilt das Zweite Gesetz der Thermodynamik. Wenn nicht Energie hineingesteckt wird, nimmt die Entropie unweigerlich zu, was bedeutet, daß Information unwiderbringlich verloren geht. Daher lassen sich viel Änderungen in der Datenbank im Gegensatz zu Softwarerefactorings nicht rückgängig machen. Zum Beispiel kann man auf die Idee kommen, zwei Felder "Vorname" und "Nachname" zu einem Feld "Name" zusammenzufassen. Versucht man, nach diesen Vorgang wieder rückgängig zu machen, wird man feststellen, daß es keinen zuverlässigen Algorithmus gibt, für beliebige Namen Vor- und Nachnamen zu trennen. Es gibt Menschen mit nur einem Namen. In China wird gewöhnlich der Nachname zuerst genannt ...
Ein weiteres Problem ist, daß manche wünschenswerten Änderungen kaum durchführbar sind, da eine zu große Menge an Daten zu reorganisieren wäre, daß das nicht in dem verfügbaren Wartungsfenster durchführbar ist.
Diese Probleme gelten natürlich nur, wenn die Datenbank bereits in Produktion ist. Während der Entwicklung an einem Neusystem darf man fröhlich mit der Datenbank experimentieren.
Testen von Datenbanken
Wie immer bei Refactorings empfiehlt es sich, einen guten Satz von Tests zu haben, um sicherzustellen, daß nach der Änderung noch alles funktioniert. Wie kann ich nun ein Testset erstellen, um die Funktion einer Datenbank sicherzustellen?
Scott Ambler empfiehlt hier Tools wie DbUnit. Dabei handelt es sich um eine Erweiterung von JUnit speziell für Datenbanken. Dies ist sicher der richtige Weg, um die korrekte Funktion von Triggern und Stored Procedures sicherzustellen. Viel wichtiger für die Datenintegrität sind aber korrekte, durch Constraints abgesicherte, Relationen zwischen den Tabellen. Wenn ich aber hier Unittests schreibe, die z.B. versuchen, einen abhängigen Datensatz zu löschen und überprüfen, ob eine Exception geworfen wird, so teste ich überwiegend die Datenbanksoftware und kaum mein Datenbankschema. Die Frage ist, ob das Constraint richtig gesetzt ist, und nicht, ob Oracle die Überprüfung des Constraints richtig implementiert hat.
Hier scheint mir die einzige richtige Vorgehensweise zu sein, das Datenbankschema in einem Datenbankentwurfstool zu entwerfen, das in der Lage ist, diesen Entwurf automatisch mit der Implementierung abzugleichen.
Wann riecht die Datenbank komisch?
Bei der Software hat sich der Begriff "Codesmell" eingebürgert für eine Implementierung, die nicht optimal ist und durch Refactoring verbessert werden sollte. Hier einige Beispiele für Datenbankgerüche:
Feld- und Tabellennamen: Ähnlich wie in der Software sollten alle Datenbankelemente einen Namen haben, der klar ausdrückt, worum es sich handelt. Bei der Datenmodellierung sollten nur Begriffe verwendet werden aus dem mit dem Kunden abgestimmten Sprachgebrauch. Dazu kommt noch die Notwendigkeit einer einheitlichen Namenskonvention für Schlüssel- und Referenzfelder. Wenn hier gesündigt wird, wird die Datenbank unlesbar.
Normalisierung: In aller Regel sollte eine relationale Datenbank normalisiert sein. Das heißt zunächst einmal, daß eine Information immer nur genau einmal abgelegt wird. Es ist ein leider weitverbreiterter Irrglaube, daß Normalisierung zu schlechterer Performance führt. Dies ist mag bei Lesezugriffen sogar manchmal zutreffen, ist aber auch hier meist beherrschbar. Die Ausnahme bilden nur Datawarehouses, bei denen viele Ad-Hoc-Abfragen gemacht werden, die sich vorher nicht tunen lassen. Das Schreiben in eine richtig normalisierte Datenbank ist aber häufig dramatisch schneller. Ich habe schon zahlreiche Perfomanceengpässe in Datenbankanwendungen durch zusätzliche Normalisierung beheben können. Das wichtigste aber ist, daß bei unzureichender Normalisierung sich rasch Widersprüche in den Daten ergeben, wenn irgendeine auf die Datenbank zugreifende Anwendung nicht sauber programmiert ist.
Nicht zusammengehörende Daten in einer Tabelle: Manchmal werden in einer Tabelle Daten aus zwei verschiedenen Kontexten zusammen abgespeichert, nur weil ihre Struktur ähnlich ist. Dies macht die Datenbank nicht nur schwerer verständlich sondern erschwert auch die richtige Setzung von Constraints.
Zusammengehörende Daten in unterschiedlichen Tabellen: In Datenbanken, die über eine lange Zeit gewachsen sind, finden sich häufig identische Konzepte an unterschiedlichen Stellen. Diese sollten zusammengeführt werden.
Beispiel: Umbenennung einer Spalte
Scott Ambler schlägt hier vor, eine zusätzliche Spalte mit dem neuen Namen einzuführen, die Daten zu kopieren und mit Triggern sicherzustellen, daß alle Änderungen in einer Spalte auch auf die andere Spalte angewandt werden. Wenn nach einiger Zeit alle Anwendungen umgestellt sind, kann die alte Spalte entfernt werden.
Diese Lösung mag funktionieren, mir erscheint sie allerdings alles andere als optimal. Ich sehe folgende Probleme:
Das Kopieren der Daten ist bei großen Tabellen sehr aufwendig.
Durch die zusätzlichen Daten kann es zumindest bei Oracle zu einem Überlauf der Datenblöcke kommen, was zu den berüchtigten "chained rows" führt - sehr schlecht für die Datenbankperformance.
Alle Anwendungen, die mit "select *" arbeiten (schlechter Stil, kommt aber vor) bekommen sofort Probleme.
Die Trigger, auch wenn sie so geschrieben sind, daß sie nicht zu einer Endlosschleife führen (weil sie sich gegenseitig triggern), stellen einen vermeidbaren Overhead dar.
Mein Vorschlag hier ist, die Spalte nur umzubennen (geht bei neueren Oracle-Versionen zum Glück ohne großen Aufwand) und Altanwendungen eine View zur Verfügung zu stellen, in der die Spalte ihren alten Namen besitzt. Wenn alle Anwendungen die gleichen Datenbankaccounts verwenden, muß ich allerdings die Tabelle umbenennen, da View und Tabelle nicht den gleichen Namen haben könnnen. Das ist aber kein großes Problem, da die Zugrffe auf die Tabelle eh angepaßt werden. Wenn ich die Accounts unterschieden kann, kann ich die View auch in ein anderes Datenbankschema legen und mit privaten Synonymen arbeiten. Je nachdem wie die Datenbank aufgesetzt ist, muß hier geringfügig variiert werden.
Kapselung der Datenbank
Wenn niemand direkt Zugriff auf die Daten hat, ist man natürlich wesentlich freier in den Änderungsmöglichkeiten. Dies kann z.B. dadurch erreicht werden, daß nur Zugriff über Stored Procedures gestattet wird. Dies ist allerdings in vielen Fällen so nicht durchsetzbar, da dann alle neuen Datenbankzugriffe über die Datenbankgruppe implementiert werden müssen. Außerdem funktionieren die schicken O/R-Mapper nicht mehr so schön.
2 Kommentare:
Danke für die Tipps bezüglich DB-Refaktoring.
Im Rahmen der Unit Testing-Funktionen im Visual Studio Team Suite gibt es auch sogenannte DB Unit Tests. Hast Du schon einmal die Chance gehabt, die Dir anzuschauen?
Ich habe mich mit dem Tool noch nicht beschäftigt, da es nach MS-Angaben wohl in der Hauptsache für T-SQL-Programmierer gedacht ist und ich bisher auch ohne gut zurechtgekommen bin.
Was muß ich für einen Datenbanktest machen?
1. Datenbank in eine definierten Zustand bringen.
2. Test ausführen.
3. Ausgangszustand wiederherstellen.
Den ersten Punkt löse ich, indem ich von einer leeren Datenbank ausgehe und Testdaten gezielt lade. Diese lassen sich in der MS-Welt sehr gut als Datasets pflegen. Tipp: Die serialisierten Datasets sollten möglichst sprechende Dateinamen haben.
Der ganze Test wird in einer Transaktion ausgeführt, so daß am Ende durch ein Rollback der Ausgangszustand wiederhergestellt werden kann. Transaktionsgrenzen in Setup- und Teardown-Methoden des Unittests.
Kommentar veröffentlichen