In meinem letzten Artikel habe ich eine Serie über das Thema Freundliche Datenbanken begonnen, in der ich fortgeschrittene Datenbank-Themen wie Datenintegrität oder gleichzeitigen Zugriff behandle. Dort habe ich behauptet, dass ich für die Beispiele der Serie PHP oder pures SQL verwenden werde.
Diese Regel werde ich für diesen (und den nächsten) Artikel brechen. Da es gerade darum geht, wie die fertigen Validierungs-Funktionen eines Frameworks durch die Datenbank im Hintergrund unterstützt werden können, muss ich logischerweise ein Framework verwenden und habe mich hier für Ruby on Rails entschieden. Die Beispiele sind allerdings so gehalten, dass Ihr auch ohne Ruby/Rails-Kenntnisse verstehen solltet, was ich zeigen will.
Das heutige Thema könnte man auch so formulieren: Traue keinem Programmierer! Ja, ich weiß, ich bin selbst einer - deswegen ja der Titel. ;-)
Aber im Ernst: ein übliches Problem, gerade wenn man ein modernes Web- Framework einsetzt, ist unser Vertrauen in den Code. Dabei ist es egal, ob wir selbst oder ein Kollege ihn geschrieben hat oder wir fertigen Code aus einer Bibliothek verwenden.
Nehmen wir gerade das Thema Validierung. Es gibt hervorragende Bibliotheken für jede Programmiersprache für diesen Bereich. So hat z.B. Rails in ActiveRecord eine sehr umfangreiche Lösung eingebaut, mit der Ihr so ziemlich alles validieren könnt, was das Programmierer-Herz begehrt (ja, ich weiß, seit Rails3 liegt dieser Code in ActiveModel). Versteht mich nicht falsch, der Code ist wirklich klasse - nur trauen sollte man ihm nicht!
Nehmen wir ein typisches ActiveRecord-Modell mit einigen aktivierten Validierungen. Das Beispiel sollte auch für einen Nicht-Rails-Programmierer im Prinzip verständlich sein. Nachdem wir ein frisches Projekt angelegt haben, erzeugen wir das Modell Person mit den Attributen first_name, last_name, email, birthdate und size mit Hilfe des eingebauten Kommandozeilen- Tools:
1
| |
Als Referenz, so sieht die generierte Datenbank-Migration aus. Beachtet bitte, dass hier standardmäßig nichts von NULL-Constraints oder ähnlichen Dingen zu sehen ist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Dementsprechend sieht auch die erzeugte MySQL-Tabelle aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Der typische nächste Schritt eines Rails-Programmierers besteht darin, für die Attribute im Modell sinnvolle Validierungen festzulegen:
1 2 3 4 5 6 7 | |
Alle Attribute außer size sind Pflichtfelder, email muss eindeutig sein und size muss eine Zahl größer als 0 sein.
So weit so gut! Wenn wir den normalen Rails-Weg gehen, neue Datensätze anzulegen, funktionieren die Validierungen auch wie erwartet. Die nächsten Beispiele zeige ich über die Rails-Konsole:
1 2 3 4 5 6 7 8 9 10 11 | |
Wunderbar, bis auf den ersten hat ActiveRecord sich standhaft geweigert, ungültige Datensätze anzulegen. Wie gesagt, die Validierungs-Bibliothek ist tatsächlich hervorragend implementiert. Das einzige Problem ist, es gibt Möglichkeiten, sie zu umgehen.
Eine wenig bekannte Möglichkeit ist die Methode update_attribute. Diese aktualisiert ein einzelnes Attribut eines Datensatzes und umgeht dabei bewusst alle Validierungen:
1 2 3 4 5 | |
Der boolesche Wert true ist immer ein Zeichen für Erfolg - sehen wir doch direkt in der Datenbank nach, was mit dem Datensatz passiert ist:
1 2 3 4 5 6 | |
Tatsächlich, Rails hat die Werte in die Datenbank geschrieben! Eine weitere Möglichkeit, die Validierung zu umgehen, ist, mehrere Datensätze gleichzeitig zu aktualisieren. Dies macht man am besten mit der Methode update_all. Legen wir erst zwei neue Datensätze an:
1 2 3 4 | |
Versuchen wir nun, alle email-Attribute auf den gleichen Wert zu setzen. Das sollte unsere Unique-Validierung eigentlich verhindern:
1 2 | |
Oh oh, 3 Datensätze wurden aktualisiert! Sehen wir direkt in der Datenbank nach:
1 2 3 4 5 6 7 8 | |
Wie heißt es so schön: Operation gelungen, Daten kaputt!
Mit diesen Beispielen will ich nicht etwa zeigen, dass Rails schlecht programmiert ist. Im Gegenteil, ich bin in höchstem Maße begeistert von diesem Framework!
Es macht nur keinen Sinn, sich rein auf die Programmierung zu verlassen, wenn es um den Schutz der Daten geht. Das gilt für jede Programmiersprache und jedes Framework. Notfalls gehe ich direkt über manuelles SQL, damit kriege ich jedes Framework klein. Die Datenbank sollte, so weit wie möglich, im Hintergrund eingreifen, wenn die Validierung der Programmierung versagt.
Fügen wir nun in der Migration von vorhin die SQL-Entsprechungen der Validierungen ein und resetten die Datenbank:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Folgende Dinge habe ich geändert:
- alle Pflichtfelder (:presence => true) haben nun :null => false
- der Datentyp von size ist nun unsigned int, es sind also keine negativen Werte mehr möglich
- auf der Spalte email liegt nun ein UNIQUE-Index
Wiederholen wir nun die Tests von eben mit der neuen Datenbank.
1 2 3 4 5 6 7 8 9 10 11 | |
Nun weigert MySQL sich mit einer Fehlermeldung, die an Rails durchgereicht wird. Voraussetzung, dass dieser Effekt eintritt, ist die Einstellung sql- mode = “STRICT_TRANS_TABLES” aus meinem letzten Artikel. Praktischerweise sehen wir auch das SQL, dass durch Rails erzeugt wurde. Wenn wir versuchen, es direkt auszuführen, erhalten wir die gleiche Fehlermeldung.
Auch update_all, scheitert nun, so wie es sein soll:
1 2 | |
Egal was wir MySQL nun entgegenwerfen und egal welches Framework wir zu diesem Zweck verwenden, das Ergebnis ist immer das Gleiche: Die Datenbank weigert sich, ungültige Daten zu akzeptieren. Mit wenig Aufwand bei der Planung der Datenbankstruktur haben wir ein grundlegend anderes Ergebnis und deutlich mehr Datenintegrität erreicht.
Was ich Euch heute gezeigt habe, sind natürlich nur die Grundlagen zu diesem Thema. Es gibt noch viel mehr Möglichkeiten, Validierungen auf Seiten der Datenbank durchzuführen. Wenn nötig, kann man beinahe beliebige Tests durchführen, von der Prüfung eines Zahlenbereichs (zwischen 45 und 80) über den Test auf einen regulären Ausdruck bis hin zu Prüfungen auf dynamische Daten (in die Spalte darf nur ein Datum aus der Vergangenheit eingetragen werden) ist alles möglich.
Es ist nur eine Frage des Aufwands - und leider auch der verwendeten Datenbank. PostgreSQL bietet in diesem Bereich deutlich mehr als MySQL. Wenn Ihr Euch für das Thema interessiert, in der Dokumentation des Gems rein stehen ein paar Beispiele zu fortgeschrittenen Constrains mit PostgreSQL.
Von meiner Seite aus war es das für heute. Nächstes Mal beschäftigen wir uns mit Constraints auf Datenbank-Beziehungen, also den berühmten Fremdschlüsseln.
Bis zum nächsten Mal!
Gruß Marc