Sonntag, 14. Dezember 2008

Ein ungleiches Paar

Viele meinen, daß objekorientierte Programme und relationale Datenbanken nicht gut zusammenpassen. Da aber relationale Datenbanken da sind und die objektorientierte Programmierung auch der Standardansatz der letzten zehn Jahre ist, müssen sie irgendwie miteinander auskommen. Ich will hier ein paar Gedanken über die Probleme dabei beschreiben und auch ein wenig erläutern, warum ich der Meinung bin, daß es in den meisten Fällen derzeit keinen guten Grund zur Scheidung gibt.

Datentypen

Eins der wohl lästigsten Probleme sind die elementaren Datentypen. Insbesondere die numerischen Typen verhalten sich in den gängigen Programmiersprachen deutlich anders wie die in den üblichen relationnalen Datenbanken. Während es in den Programmiersprachen meist zwei oder drei ganzzahlige Typen mit unterschiedlichem Werteumfang gibt, können in den Datenbanken die ganzzahligen Werte bis zu einer herstellerabhängigen Obergrenze jede Länge besitzen. Die Obergrenze des Zahlenbereichs ist bei Programmiersprachen meist vom binären System bestimmt, in Datenbanken aber vom dezimalen. Da den kompatiblen Datentyp bei einer streng typisierten Sprache wie Java oder C# zu finden, erfordert einige Rechnerei. Einiges an Schwierigkeiten kann man sich auch einhandeln, wenn man in der Datenbank z.B. für Schlüssel unsinnig breite numerische Felder definiert, die dann in kein Standardganzzahltyp der Programmiersprache mehr passen.
Bei Gleitzahlwerten ist der Unterschied nicht ganz so gravierend. Aber man sollte beachten, daß die Datenbanken diese mit einer definierten Anzahl von dezimalen Nachkommastellen speichern. Diese eignen sich zum Speichern auch etwas besser wie die Fließkommazahlen, die für numerische Berechnungen optimiert sind, wie sie im Programm aber nicht in der Datenbank stattfinden.

Nullwerte

In Datenbanken können Felder als optional definiert werden. Sie können dann auch den Wert "Null" besitzen, also leer sein. Objektorientierte Sprachen haben meist Probleme mit solchen Werten umzugehen. Leicht bekommt man dann eine "NullPointerException" oder ähnliches. Das bedeutet, daß der Code mit großen Mengen von Sicherheitsabfragen gespickt ist, die ihn unübersichtlich machen. Wer einmal mit den ado.net datasets in der Version 1.0 zu tun hatte, kennt eine in dieser Beziehung besonders unelegante Lösung.
In funktionalen Sprachen kennt man die maybe-Monade. Sie bietet eine besonders elegante Lösung mit Null-Werten umzugehen. Z.B. Scala implementiert dieses Konzept mit Option.

Ressourcen

Die Kommunikation mit der Datenbank benötigt Ressourcen, die alloziert und wieder freigegeben werden wollen. Dies sind insbesondere Datenbankverbindungen und Cursoren. Für Entwickler, die sich daran gewöhnt haben, daß moderne Programmiersprachen sie von diesen Problemen bezüglich Speicherverwaltung entlasten, ist das eine ungewohnte Erfahrung. Man sollte darauf achten, daß die Anwendung diese Ressourcen immer in derselben Umgebung freigibt, wo sie sie alloziert hat. In C# hilft hier das using-Konstrukt, was in Python jetzt als with übernommen wurde. In C++ hilft der alte Trick, die Allozierung in einem Konstruktor vorzunehmen und im korrespondierenden Destruktor die Freigabe. Wird die Klasse als lokale Variable instanziert, erfolgt die Freigabe automatisch, wenn der Scope verlassen wird.

Transaktionen

Einer der stärksten Punkte der Datenbanken sind Transaktionen. Zusammenhängende Änderungen werden nur zusammen sichtbar. Wenn während einer Reihe von Änderungen ein Fehler auftritt, kann der alte Zustand sofort wieder hergestellt werden (rollback). Allerdings haben viele Leute am Anfang Probleme mit der Handhabung von Transaktionen. Wenn sie nicht sorgfältig implementiert sind, kann es hier zu vielen schwer zu verfolgenden Fehlern kommen. Eine große Hilfe hier ist es, wenn die Transaktionsbehandlung deklarativ erfolgt, wie es z.B. bie JEE der Fall ist.
Transaktionen sind aber nicht nur etwas trickreich in der Anwendung, sie sind auch der wohl beste Ansatz, um mit bestimmten Problemen der Nebenläufigkeit umzugehen. Das ist nämlich der Grund, warum Datenbanken das Transaktionskonzept eingeführt haben. Viele Anwender greifen parallel auf dieselben Daten zu. Weil das so gut funktioniert, wird in letzter Zeit auch häufiger diskutiert, Transaktionen auch auf andere Daten anzuwenden (transactional memory).

SQL

Der Zugriff auf relationale Daten erfolgt heute beinahe ausschließlich mit SQL. Ingres hatte zu Beginn eine andere Sprache namens QUEL, die z.B. Date in seinem Standardwerk "Database Systems" als SQL überlegen dargestellt hat, aber die Frage ist jetzt entschieden. Das besondere an SQL ist, daß man nicht beschreiben muß, wie der Computer etwas machen soll, sondern nur, was man haben will. Das ist vielen unheimlich, die nur an prozedurale Sprachen gewöhnt sind. In Wahrheit ist es aber viel flexibler und ausdrucksstärker als jede prozedurale Lösung. Ich kenne Fälle, wo 1000 Zeilen prozeduraler Code durch 40 Zeilen SQL ersetzt werden konnten. Im Gegensatz zu der prozeduralen Lösung war die SQL-Lösung sogar wartbar, zumindest für Leute, die die Sprache beherrschen. Da SQL völlig entkoppelt ist von der endgültigen physikalischen Speicherform, kann man in der Datenbank eine Menge Optimierungen vornehmen, die die Reaktionsgeschwindigkeit der Datenbank dramatisch verbessern, ohne das der SQL-Code angefaßt werden müßte.
Leider versuchen eine ganze Menge von Frameworks SQL zu verstecken. Das führt dann dazu, daß man gegen eine weniger ausdrucksvolle API programmiert und das Framework das dann in SQL umsetzt. Dies geschieht genauso in Hibenate wie auch in LINQ. Da ich ziemlich viel Erfahrung mit SQL habe, habe ich immer eine Abneigung dagegen, eine Zwischenschicht mit List und Tücke dazu zu bringen, das geeignete SQL zu produzieren. Insgesamt macht die Zwischenschicht das optimieren des Datenbankzugriffs schwieriger und machmal unmöglich (Stichwort: hints). Ich weiß, daß man in Hibernate auch direkt SQL verwenden kann. Die Anwendung wird aber nicht wartbarer dadurch, daß man mehrere unterschiedliche Zugriffsarten gleichzeitig verwendet (bei Hibernate: HQL, Criteria und SQL).
Ein Problem mit SQL ist, daß es erst zur Laufzeit der Programms ausgewertet wird und dazu noch von einem anderen System (der Datenbank). Hier sind Unit-Tests unbedingt notwendig, um ungültiges SQL zu vermeiden. Erfreulich wäre auch, wenn es Editoren gäbe, die mit eingebettetem SQL umgegehn könnten und Code-Vervollständigung und -Prüfung bieten würden und vielleicht sogar Unterstützung für Refactoring. Man wird ja noch träumen dürfen.

Datenübergabe

Die aus der Datenbank bezogenen Daten werden über einen sogenannten Cursor zurückgegeben. Der Zugriff darauf entspricht im wesentlichen einer Queue. Man holt sich einen Datensatz nach dem anderen. Für die Weiterverarbeitung ist aber meist eine Übernahme in andere Datenstrukturen wie Listen, Hashes oder Arrays günstiger. Wenn man alles in so eine Struktur auf einmal kopieren will, so hat man das Problem, daß das viel Speicher und viel Zeit kosten kann. Besser ist es mit "faulen" Datenstrukturen zu arbeiten, die intern erst die Daten laden, wenn sie benötigt werden. Sprachen, die sich an der funktionalen Programmierung orientieren haben dieses Konzept bereits integriert.

Architekturschichten

Eins der größten Probleme mit dem Datenbankzugriff sind die sich widersprechenden architekturellen Ziele, die es zu beachten gilt. Einmal möchte man die ganzen technologischen Aspekte, die ich bisher geschildert habe, in eine separate Schicht unterbringen. Zum anderen scheint es nahezuliegen, ein Objekt selber dafür verantwortlich zu machen, sich zu speichern und zu laden (z.B. durch Konstruktor). Die letzte Betrachtungsweise führt zu einem Muster, das Fowler als "Active Record" bezeichnet. Es ist z.B. in der Datenbankschicht von Ruby on Rails verwendet worden. Das funktioniert aber in komplizierteren Fällen nicht, da die Anforderungen an ein Objektmodell etwas andere sind wie die eines relationalen Datenmodells. Die Unterschiede zu erläutern würde heute hier zu weit führen und ich hebe sie mir für ein andermal auf. Jedenfalls wir dann eine Schicht benötigt, die die Abbildung zwischen relationalem und Objektmodell implementiert.

Lebenszyklen

Datenbanken lassen sich wesentlich einfacher ändern wie die auf sie zugreifenden Programme. Damit können diese Programme aber sehr leicht funktionsunfähig werden. In der Datenbank reicht ein einfacher "alter .."-Befehl, der - vorausgesetzt man besitzt die Rechte - jederzeit ausgeführt werden kann. Die Anwendung muß geändert, kompiliert und installiert werden. Häufig ist daher auch zu beobachten, daß Teile des Systems, die weniger stabil sind, in die Datenbank ausgelagert werden (z.B. als Stored Procedures). Das halte ich nicht für eine besonders gute Idee, wenn andere Designaspekte dagegen sprechen.
Andererseits sind die Datenbanken insgesamt wesentlich langlebiger wie die Anwendungen, die darauf zugreifen.

Anwendungsübergreifend

Viele Entwickler wünschen sich, daß statt relationalen Datenbanken objektorientiere Datenbanken eingeführt würden. Es gibt einige Gründe, warum das bislang nicht geschehen ist. Da sind sicher zuerst einmal die großen Investitionen, die in die bestehenden Datenbanken geflossen sind. Dazu kommt aber auch, daß das Objektmodell einer Anwendung nicht unbedingt kompatibel sein muß zu dem einer anderen, die auch auf dieselben Daten zugreift. In der Datenbank wird statisches Wissen abgelegt. Objekte sollen aber auch miteinander über Methoden agieren können. Für die Datenmodellierung spielt nur eine Rolle, was abgelegt, für die Objektmodellierung auch wie damit umgegangen werden soll. Wenn eine Datenbank nur einer einzelnen Anwendung dient, mag eine objektorientierte Datenbank eine gute Lösung sein. Für einen Datenbestand, der seinen Wert unabhängig von den Anwendungen hat, ist die relationale Darstellung immer noch die beste weil flexibelste und gleichzeitig stabilste. Das ist insbesondere in Unternehmensanwendung fast immer der Fall.

Montag, 1. Dezember 2008

Die Linke weiß, was die Rechte tut

Neulich habe ich bereits über IT-architektonischen Aspekte der Datensicherheit geschrieben. Diesmal will ich noch einen weiteren Aspekt ergänzen.

Gerade als Softwareentwickler erlebt man heutzutage immer wieder kuriose Auswüchse der Datensicherheit: "Ihr Software arbeitet fehlerhaft. Aus Datenschutzgründen kann ich Ihnen nichts genaueres sagen, aber ich erwarte, daß der Fehler bis morgen behoben ist." Dabei ist das meiner Ansicht nach größte Problem ganz wo anders.

Wer mit vertraulichen Daten umgeht, muß sich verpflichten, die geltenden Datenschutzbestimmungen einzuhalten. Wenn die Kenntnis dieser Bestimmungen abgefragt werden, so kommen dann Fragen vor wie folgende: "Eine Kollegin ruft sie an und will die private Telefonnummer eines Kunden wissen. Geben sie sie ihr?" Da der Mechanismus dieser Fragen leicht zu durchschauen ist, antwortet man sofort mit "nein". Was ist aber, wenn ich die Nummer selber gut gebrauchen könnte für etwas was nicht mit der Aufgabe zu tun hat, die mir Zugang zu den Daten gestattet. Es ist nicht zu erwarten, daß meine eine Gehirnhälfte diese Nummer kennt, die andere sie aber nicht benutzt.

Wenn also jemand vertrauliche Daten benutzt, so muß das nicht daran liegen, daß er Sicherheitslücken ausnutzt, er kann auch ganz legal, aber in anderem Zusammenhang, Zugang gehabt haben. Derartige Kompetenzüberschneidungen geschehen immer wieder fahrlässig. Es ist aber durchaus vorzustellen, daß jemand dies bewußt nutzt.

Kritisch kann so etwas auch werden, wenn ein Dienstleister Aufträge von verschiedenen Firmen gleichzeitig entgegennimmt. Wenn dasselbe Callcenter für konkurrierende Firmen arbeitet, so ist es wahrscheinlich, daß die Kundendaten der einen Firma benutzt werden, um Kunden für die andere zu werben.