Session D-CLAR

Klassen zu Relationen

Manfred Rätzmann
raezz@t-online.de


Der objektorientierte Datenbank-Entwickler:
Zwei Seelen wohnen, ach, in seiner Brust!

Es gab eine Zeit, da war jedem Datenbank-Entwickler klar, was das wichtigste Erfolgskriterium einer Datenbank gestützten Anwendung ist: die Korrektheit und Performance der in der Datenbank realisierten Datenstrukturen. Inzwischen haben wir den Siegeszug der objektorientierten Softwareentwicklung erlebt und müssen nun feststellen, dass die Vorherrschaft des Datenbankdesigns zunehmend durch die Priorität des objektorientierten Anwendungsentwurfs in Frage gestellt wird. In meiner TOP DOWN Session zur Vorgehensweise beim objektorientierten Anwendungsentwurf zeige ich, dass es  und warum es empfehlenswert ist, die bisherige Vorherrschaft des Datenmodells abzuschaffen und die Sache "anders herum" anzugehen: Die Oberfläche bestimmt die Struktur der Business-Objekte, die Business-Objekte bestimmen die Struktur der Datenbank. Wie sie das machen, wie also aus einem Objekt- oder Klassenmodell die dazu passende Datenbankstruktur abzuleiten ist, ist Thema dieser Session. Ich benutze dabei die gleiche Beispielanwendung wie in der TOP DOWN Session. Mein Beispiel ist ein Teil einer Anwendung für eine Kleintierpraxis. Das Geschäftsmodell des uns interessierenden Teils sieht so aus:

Die Kunden der Praxis sind die Patientenbesitzer. Ein Kunde kann eins oder mehrere Tiere besitzen, die als Patienten in der Praxis behandelt werden. Zu jedem Patienten existiert eine Karteikarte, die alle Einträge zu dem Patienten zusammenfasst. Ein Karteieintrag kann dabei eine erneute Vorstellung des Tieres, eine erbrachte Leistung, eine Medikamentenabgabe, eine Futtermittelabgabe, eine Impfung, ein Vermerk zu Anamnese oder Diagnose einer Erkrankung oder eine Medikamentenverschreibung sein.

Der Kunde erhält bei Bedarf Belege in Form von Rechnungen oder Rezepten.

Projektion von Klassen auf Tabellen

Bei der Erstellung des Geschäftsmodells habe ich bereits bekannte Kerndaten den einzelnen Klassen als Attribute zugeordnet. So haben Objekte der Klasse Kunde zum Beispiel die Attribute "Nachname", "Vorname" usw., ein Impferinnerungsflag und das Attribut "OffenerSaldo" auf das ich später noch einmal zurückkomme.

Die Projektion einer solchen Klasse auf eine Datenstruktur ist sehr einfach: Die Klasse Kunde wird auf eine Tabelle "Kunde" abgebildet, jedes persistente Attribut der Klasse (d.h. jedes Attribut, dessen Wert auch erhalten bleiben muß, wenn der Computer ausgeschaltet wird) wird zu einem Tabellenfeld.

Jeder Satz der Tabelle "Kunde" enthält demnach die Daten genau eines Kundenobjektes. Ich habe der Tabelle ein Feld "OID" als Objekt-ID hinzugefügt. Jedes Objekt der Beispielapplikation hat eine solche eindeutige ID die von der Methode CreateID der Klasse "PersistentObject" bei der Neuanlage eines Objektes vergeben wird (siehe dazu auch das Implementationsmodell in den Unterlagen zu meiner TOP DOWN Session). Diese Objekt-ID ist der Primärschlüssel der Tabelle.

Eine solche 1:1 Projektion einer Klasse auf eine Tabelle ist der einfachste Fall. Interessanter wird es bei der Frage, wie wir Vererbungshierarchien auf Tabellenstrukturen abbilden. Dabei stehen uns drei unterschiedliche Möglichkeiten zur Verfügung, die man auch als

Horizontale Partionierung

Die horizontale Partionierung wird bevorzugt eingesetzt, wenn die Elternklasse abstrakt ist und die abgeleiten Klassen so verschieden sind, dass sie selten bis nie z.B. in einer Liste zusammen aufgeführt werden müssen. In meinem Beispiel ist das bei der "Beleg"-Klasse gegeben. In diesem Fall werden nur die abgeleiteten konkreten Klassen auf Tabellen abgebildet, nicht jedoch die abstrakte Oberklasse. Die Tabellen für die konkreten Klassen enthalten dann neben den Attributen der konkreten Klasse auch alle Attribute der abstrakten Oberklasse, im Beispiel also die Kunden-ID, Belegnummer und das Belegdatum.

Vorteil der horizontalen Partionierung ist, dass direkt über alle Attribute der abgeleiteten Klasse auf ein einzelnes Objekt zugegriffen werden kann, z.B. mit einem

um die Daten der Rechnung Nr. 1002 einzulesen.

Eine Liste aller Belege eines Kunden unabhängig vom Typ wäre aber umständlich zu generieren, da hierbei mit UNION SELECTs gearbeitet werden muß:

Vertikale Partionierung

Wird häufiger über die abstrakte Elternklasse zugegriffen, d.h. eine Liste von Objekten verschiedener Unterklassen benötigt, empfiehlt sich die vertikale Partionierung. Dabei werden die Atttribute der Elternklasse in einer gesonderten Tabelle abgelegt. Die Tabellen zu den abgeleiteten Klassen enthalten dann nur noch die Attribute, die für die abgeleiteten Klassen spezifisch sind.

Das ist der Fall bei den von "Karteieintrag" abgeleiteten Klassen. Da hier sehr häufig eine Liste aller Einträge unabhängig von deren Typ gebraucht wird, werden die allen Einträgen gemeinsamen Attribute der Elternklasse in einer eigenen Tabelle zusammengefasst. Die Tabelle für die Objekte der Klassen "Leistung", "Vermerk", "Impfung" etc. enthalten nur die spezifischen Attribute der jeweiligen Klasse. Vorteil ist, wie gesagt, dass sehr einfach eine Liste aller Karteieinträge eines Patienten zu erzeugen ist mit dem SQL Statement:

 

Der Nachteil ist, dass die kompletten Daten eines einzelnen Eintrags, z.B. einer Impfung nur über einen JOIN zu erreichen sind:

Dabei wird gleichzeitig deutlich, wie die Tabellen bei einer vertikalen Partionierung verknüpft werden. Die Tabelle der Elternklasse erhält pro Satz eine Objekt-ID. Mit der gleichen ID werden in den Tabellen der abgeleiteten Klassen die Datensätze für die ergänzenden Attributwerte abgelegt.

Typisierte Partionierung

Bei der typisierten Partionierung schließlich werden alle Attribute der Elternklasse und der abgeleiteten Klassen in einem Datensatz einer Tabelle zusammengefasst. Ein Beispiel dafür sind die Positionen eines Belegs, hier einer Rechnung:

Die Tabelle "RechPos" enthält die Datensätze aller Positionen aus Rechnungen. Zur Unterscheidung der einzelnen Positionsarten wird ein Kennzeichen, hier PosArt eingeführt, dass bei den Attributen der aus BelegPosition abgeleiteten Klassen nicht auftaucht, da es die unterschiedlichen Klassen selbst repräsentiert. Alle Abrechnungspositionen enthalten in PosArt zum Beispiel ein "ABR", Rabattpositionen ein "RAB" und so weiter.

Der Vorteil der typisierten Partionierung liegt darin, dass auf ein einzelnes Objekt wie bei der horizontalen Partionierung direkt zugegriffen werden kann. Gleichzeitig kann, wie bei der vertikalen Partionierung, eine Liste aller Objekte der Unterklassen einfach erzeugt werden.

Der Nachteil besteht darin, dass je nach Objekttyp in den entsprechenden Datensätzen Felder ohne relevante Information mitgeführt werden müssen, also ein höherer Platzbedarf besteht. Dies kann man allerdings durch Mehrfachnutzung des selben Feldes für Attribute verschiedener Unterklassen zu optimieren versuchen. Wenn die abgeleiteten Klassen nicht gar zu disparat sind, sollte sich die Platzverschwendung in Grenzen halten.

Welche der möglichen Projektionen von Vererbungshierarchien auf Tabellenstrukturen schließlich gewählt wird, hängt hauptsächlich von den Zugriffsnotwendigkeiten der Anwendung ab. Es ist auch ohne weiteres möglich, die verwendete Strategie innerhalb eines Vererbungszweiges zu wechseln. Wenn in unserem Beispiel die Karteieinträge zu Impfungen sinnvollerweise weiter differenziert werden müssten, könnte dies etwa folgendes Klassenmodell ergeben:

Dann wäre es wahrscheinlich angebracht, zwischen der Klasse "Karteieintrag" und "Impfung" eine vertikale Partionierung vorzunehmen und zwischen den einzelnen Unterklassen von "Impfung" eine typisierte Partionierung. daraus würden dann die beiden Tabellen "Karteieintrag" und "Impfung" entstehen, wobei im Datensatz von "Impfung" ein Feld "Typ" enthalten wäre, das den Impftyp, also die konkrete Unterklasse bestimmt.

Dadurch, dass das Datenmodell erst aus dem fertigen Klassenmodell abgeleitet wird, kann es auf die Erfordernisse der jeweiligen Applikation hin optimiert werden. Der Zustand, dass sich Klassenmodell und Datenmodell widersprechen und sich damit gegenseitig das Leben schwer machen, tritt hier nicht ein.

OO Besonderheiten

Assoziation und Aggregation

Assoziations- und Aggregationsverknüpfungen innerhalb des Klassenmodells werden bei 1:N Verknüpfungen durch Übernahme der Objekt-ID der 1-Seite in die Attribute der N-Seite hergestellt. bei der Verknüpfung der Klassen Kunde zu Karteikarte

wird der Klasse "Karteikarte" also ein Attribut hinzugefügt, das die ID des jeweiligen Kunden aufnimmt. Diese Vorgehensweise entspricht exakt der Verknüpfung zweier Tabellen bei der Datenmodellierung durch Übernahme des Primärschlüssels der einen Tabelle als Fremdschlüssel in die zweite Tabelle.

M:N Beziehungen zwischen zwei Klassen werden im Datenmodell durch Verknüpfungstabellen realisiert. Ein gutes Beispiel für eine M:N Beziehung ist immer die Beziehung zwischen Artikel und Lieferant, die im Modul Bestellwesen auch in Programm für unsere Kleintierpraxis auftaucht:

Ein Artikel kann von mehreren Lieferanten bezogen werden, ein Lieferant liefert mehrere Artikel. Im Datenmodell wird diese Beziehung in einer zusätzlichen Tabelle abgebildet, die nur die jeweiligen Objekt-IDs von Artikel und Lieferanten enthält, zwischen denen diese Beziehung besteht.

Assoziationsklassen

Wenn eine Assoziation eigene Attribute besitzt, werden diese im Klassenmodell in einer Assoziationsklasse untergebracht. Im Artikel/Lieferantenbeispiel können die Lieferbedingungen des Lieferanten zum jeweiligen Artikel in einer Assoziationsklasse zusammengefasst werden.

Jedes Objekt der Klasse "Lieferbedingung" gibt also die Bedingungen wie Preis, Lieferzeit etc. an, zu denen ein spezieller Lieferant einen einzelnen Artikel liefert. Im Datenmodell werden die Attribute der Assoziationsklasse als Felder in die Datensatzstruktur der Verknüpfungstabelle aufgenommen.

Sammlungen (Collections)

Klassenattribute, die Sammlungen (Collections) darstellen, können nicht mit den einfachen Attributwerten zusammen in einem Datensatz abgelegt werden. Ein Beispiel dafür bilden noch einmal die Belegklasse mit der Sammlung von Belegpositionen:

Hierbei handelt es sich um eine Zusammensetzung (Composition), das heißt, die verknüpften Objekte, also die Positionen, können nicht außerhalb der Zusammensetzung, sprich des Belegs, existieren. In diesem Fall, der immer eine 1:N Verknüpfung darstellt, wird die ID des Hauptobjektes - des Belegs also als Attribut in die verknüpften Objekte übernommen. Wir brauchen dazu also im Datenmodell keine Verknüpfungstabelle sondern übernehmen den Primärschlüssel der Haupttabelle "Beleg" als Fremdschlüssel in die Sätze der verknüpften Tabelle "Position".

Anders sieht das aus, wenn die Sammlung Objekte enthält, die auch unabhängig von dieser Verbindung bestehen können, etwa bei einer Stückliste. Dann haben wir keine Zusammensetzung (Composition) vor uns sondern eine Anhäufung (Aggregation), oder besser eine Menge im Sinne der Mengenlehre von eigenständigen Objekten. In diesem Fall, der meistens eine N:M Beziehung ist jedes Objekt kann ja in mehreren Mengen vorkommen brauchen wir im Datenmodell eine Verknüpfungstabelle, die die ID der Menge und die ID des enthaltenen Objektes in jeweils einem Datensatz zusammenfasst.

Der objektorientierte Datenbankentwickler:
Das Beste aus zwei Welten!

Wenn nicht mehr das Datenmodell der Ausgangspunkt für die weitere Entwicklung einer neuen Applikation ist, sondern das Klassenmodell der Applikation, so stellt sich die Frage, ob die Techniken und Vorgehensweisen, die wir bei der Datenmodellierung erlernt haben, jetzt alle hinfällig geworden sind. Oder sind diese Techniken auf die Klassenmodellierung übertragbar bzw. können wir unsere bei der Datenmodellierung erworbenen Kenntnisse und Erfahrungen auch bei der Klassenmodellierung sinnvoll einsetzen?

Ein eindeutiges Ja auf diese Frage! Aber nicht nur das. Ich glaube, wenn jemand vor der Aufgabe steht, ein Klassenmodell für eine Anwendung zu erstellen, deren Daten in einer relationalen Datenbank gespeichert werden sollen, muß dieser Jemand auch fundierte Kenntnissein der Datenmodellierung haben. Ansonsten ist ein Scheitern ziemlich wahrscheinlich. Die geforderten Kenntnisse und Erfahrungen betreffen Dinge wie Redundanzfreiheit und die bei der Datenmodellierung angewandten Normalisierungsverfahren, die Bedeutung von Primärschlüsseln, Zugriffsoptimierung sowie Fragen der referentiellen Integrität der Datenbasis. Damit wollen wir uns im zweiten Teil dieser Session befassen.

Objekt-Identität, Primär- und Kandidatenschlüssel

Um in einer Menge von Objekten der gleichen Klasse ein bestimmtes Objekt zu finden, braucht man eine Eigenschaft, die mit Sicherheit bei jedem Objekt anders ist. Mit Datenbank-Worten: einen eindeutigen Schlüssel. Für die Modellierung ist ein solcher eindeutiger Schlüssel eigentlich nicht notwendig. Die UML setzt zum Beispiel einfach voraus, daß jedes Objekt eine eindeutige Identität hat und damit identifiziert werden kann.

Auch ER-Diagramme brauchen im Prinzip keine Schlüssel. Da die meisten ER-Modellierungswerkzeuge aber darauf ausgerichtet sind, das Modell anschließend in eine relationale Datenbank umzusetzen, wird bereits beim Modellieren der Datenbank mit Schlüsseln gearbeitet.

Die eindeutigen Schlüssel von Tabellen in relationalen Datenbanken nennt man Kandidatenschlüssel (Candidate Keys), einer von ihnen wird zum Primärschlüssel (Primary Key) bestimmt. Verbindungen zwischen zwei Tabellen werden immer durch Übernahme des Primärschlüsselfelds der einen Tabelle in die andere Tabelle hergestellt. Das übernommene Feld wird als Fremdschlüssel bezeichnet, da es ein Schlüssel einer Fremdtabelle ist.

Primärschlüssel können Felder der Tabelle sein (auch mehrere miteinander verknüpft), die einen für den Anwender sinnvollen Inhalt haben, zum Beispiel Kunden- und Artikelnummern. Solche Schlüssel nennt man natürliche Schlüssel, bei Objekten spricht von einer „value-based identity“ der Objekte.

Häufig sind Primärschlüsselfelder aber Zusatzfelder, die keinen anderen Zweck haben als eben den, Primärschlüssel zu sein. Die Werte, die in diesen Feldern stehen, haben aus der Sicht des Anwenders keinen Sinn. Das sollen sie auch nicht, sie sollen nur eindeutig sein. Solche Schlüssel nennt man künstliche Schlüssel oder Surrogatschlüssel, bei Objekten spricht man dann von einer „existence-based identity“ der Objekte.

Wenn wir zwei Objekte der gleichen Klasse im Hauptspeicher aufbauen, sind dies verschiedene Objekte, auch wenn sie in allen Attributwerten übereinstimmen. Da für alle Objekte eine solche Identität vorausgesetzt wird, die nicht von den Attributen des Objektes abhängt, sollten wir diese auch in die Struktur der Datenbank übernehmen. Deshalb hat jeder Datensatz einen künstlichen Primärschlüssel, die OID.

Redundanzfreiheit

Redundanzfreiheit besagt, dass eine Information nur an einer Stelle festgehalten werden soll. Im allgemeinen gilt diese Forderung auch bei der Modellierung von Klassen. So macht es wahrscheinlich wenig Sinn, die Telefon-Nr. des Kunden zusätzlich als Attribut der Karteikarte anzusehen. Der Stellenwert der Redundanzfreiheit reduziert sich bei der Klassenmodellierung allerdings von einem ehernen Prinzip auf eine normalerweise vernünftige Technik.

Ist das Attribut "offener Saldo" der Klasse "Kunde" zum Beispiel redundant, da sich der offene Saldo als Summe aller offenen Posten des Kunden jederzeit ermitteln läßt? Aus der Sicht der Datenmodellierung ja, aus der Sicht der Klassenmodellierung nicht unbedingt. Wieso? Der offene Saldo als Summe aller offenen Posten dieses Kunden ist zweifelsohne ein wichtiges Attribut des Kunden. Dabei ist es zunächst mal unerheblich, wie der Wert dieses Attributs intern gespeichert ist, ob er also als Datenfeld einer Tabelle vorliegt oder das Ergebnis einer Operation ist. Wenn ein Nutzer der Klasse "Kunde" auf dieses Attribut zugreift ist die Ermittlung des Wertes für ihn völlig transparent. Vielleicht greift der Sourcecode der Kunden-Klasse lediglich auf ein Feld der Tabelle "Kunde" zu um den Wert zu ermitteln. Vielleicht wird aber auch ein SQL-Statement abgesetzt, das den aktuellen Wert aus den Sätzen der Tabelle "OffenePosten" ermittelt.

Je nach Häufigkeit des Zugriffs, benötigter Performance und/oder anderen Aspekten der Realisierung wird demnach für die Bereitstellung des Attributwertes die eine oder andere Technik gewählt werden. Auswirkungen auf das Klassenmodell unserer Applikation hat das nicht.

Normalisierung

Kann man Klassenstrukturen normalisieren?

Um die Tabellen einer relationalen Datenbank in die erste Normalform zu bringen heißt es, Informationen zu separieren. Eine Spalte einer Tabelle darf nicht mehrere Angaben gleichzeitig enthalten. Auf Klassenattribute übertragen wäre das der Fall, wenn wir zum Beispiel alle mit einem Rezept verschriebenen Medikamente in einem Attribut als kommaseparierte Liste festhalten würden. Aus der Sicht der Klassenmodellierung spricht degegen zunächst mal nichts und niemand hindert den unerfahrenen Klassenmodellierer daran, sowas zu tun. Als Datenmodellierer wissen wir allerdings, dass nach Daten in solchen Listen viel weniger performant gesucht werden kann als nach separierten Werten. Aus dieser Erfahrung heraus werden wir also die mit einem Rezept verschriebenen Medikamente auf Positionen oder ähnliches aufteilen, wobei in einer Position des Rezepts nur ein Medikament angegeben werden kann.

Bei der 3. Normalform von Tabellen einer relationalen Datenbank geht es um funktionale Abhängigkeiten. (Die 2. Normalform beschäftigt sich ebenfalls mit funktionalen Abhängigkeiten, allerdings zu Teilen des Primärschlüssels. Da dieser bei uns immer aus der Objekt-ID besteht, also nicht zusammengesetzt ist, brauchen wir die 2. Normalform nicht gesondert zu betrachten) Eine funktionale Abhängigkeit liegt dann vor, wenn eine Spalte bzw. Attribut automatisch den Wert anderer Spalten/Attribute festlegt. Zum Beispiel sind alle Spalten mit Adressangaben in der Tabelle "Kunde" funktional von der Spalte OID (der Kunden-ID) abhängig. Wenn wir Adressangaben als Attribute auch in die Klasse "Karteikarte" aufnehmen, entsteht dadurch auf Tabellenebene eine Verletzung der 3. Normalform, da die Spalte "Kunde" kein Kandidatenschlüssel der Tabelle "Karteikarte" ist. Die 3. Normalform einer relationalen Datenbank verlangt nämlich, dass funktionale Abhängigkeiten nur noch zu Kandidatenschlüsseln bestehen dürfen.

Wenn die aus Ihrem Modell entstandene Datenstruktur nicht der dritten Normalform entspricht, deutet das mit hoher Wahrscheinlichkeit auf Fehler im Modell hin. Es kann natürlich sein, daß Ihr Modell bewußte Designentscheidungen enthält, die zu einer nichtnormalisierten Datenbank führen. Normalerweise ist der Test auf Vorliegen der dritten Normalform aber ein sehr nützlicher Qualitätscheck für Ihr Modell.

Mit Blick auf die entstehenden Datenstrukturen können wir also etwas über die Qualität unseres Klassenmodells aussagen. Aber auch im Designprozess selbst ist Übung im Normalisieren von Datenstrukturen sehr nützlich. Wir können überprüfen, ob funktionale Abhängigkeiten zwischen einzelnen Attributen bestehen. Es gibt auf der Klassenebene zwar keine Kandidatenschlüssel, wir können aber Attribute daraufhin untersuchen, ob sie alleine oder mit anderen zur Identifizierung eines Objektes ausreichen würden. Wenn funktionale Abhängigkeiten da sind, sollten diese nur zu solchen "identifizierenden Attributen" bestehen.

Referentielle Integrität

Referentielle Integrität bedeutet, daß alle Fremdschlüssel in verknüpften Tabellen auf gültige Sätze in der Haupttabelle verweisen, daß also keine der im Modell angelegten Beziehungen in’s Leere zeigt. Ein referentiell nicht integrer Zustand ist mit dem Vorliegen von Pointerfehlern vergleichbar und bildet ähnliche Absturzgefahren für Ihr System.

RI-Regeln gehören nicht zu den Geschäftsregeln (Business-Rules). Sie sind also keine Regeln, die inhaltliche Aspekte der Datenbank bestimmen. Wo liegt der Unterschied? Eine Geschäftsregel kann zum Beispiel lauten, daß kein Kunde gelöscht werden darf, für den im System noch offene Rechnungen vorliegen. Eine RI-Regel hingegen besagt, dass, wenn ein Kunde gelöscht wird, auch alle Rechnungen an ihn gelöscht werden müssen. Die RI-Regel entscheidet also nicht darüber, ob der Kunde gelöscht werden darf. Sie legt nur fest, was passieren muß, wenn dieser Kunde gelöscht wird. RI-Regeln und Geschäftsregeln sollten nicht miteinander vermischt werden.

In VFP wird die referentielle Integrität durch Code sichergestellt, der von den Update- Insert und Delete-Triggern aufgerufen wird. Dieser Code ist jedoch nicht von Hause aus da, sondern Sie müssen ihn erstellen. Entweder, indem Sie ihn selbst schreiben (was nicht ganz trivial ist) oder indem Sie ihn generieren lassen. RI-Code kann zum Beispiel mit dem in VFP integrierten RI-Assistenten generiert werden. Wenn Sie ein Datenbank-Modellierungstool einsetzen, sollte dieses ebenfalls dazu in der Lage sein.

Für die Generierung des RI-Codes müssen Sie entscheiden, was mit verknüpften Sätzen geschehen soll, wenn ein Hauptsatz gelöscht wird oder wenn der Primärschlüssel geändert wird. Außerdem müssen Sie angeben, ob beim Einfügen von Sätzen in die verknüpfte Tabelle immer ein passender Satz in der Haupttabelle vorhanden sein muß.

Weitergeben (cascade)

Die Option „Weitergeben“ bedeutet, dass eine Änderung des Primärschlüssels an die verknüpften Tabellen weitergegeben werden soll. Da wir mit Surrogatschlüssel arbeiten, erübrigt sich das, da diese nicht geändert werden können. Wenn ein natürlicher Primärschlüssel geändert wird, kann das Weitergeben erhebliche Zeiten in Anspruch nehmen.

Bei Löschen eines Satzes der Haupttabelle bedeutet Weitergeben, dass alle mit diesem Satz verknüpften Sätze ebenfalls gelöscht werden.

Verhindern (restrict)

Das Setzen dieser Option verhindert, dass ein Primärschlüssel eines Satzes der Haupttabelle geändert wird, wenn dieser Satz bereits mit einem anderen Satz verknüpft ist. Ebenso wird in diesem Fall das Löschen des Hauptsatzes verhindert.

Beim Einfügen von Sätzen in die verknüpfte Tabelle verhindert diese Option, dass ein Satz eingefügt werden kann, zu dem kein passender Satz in der Haupttabelle vorhanden ist.

Ignorieren (ignore)

Ignorieren bedeutet, dass Änderungen oder Löschvorgänge nicht weitergegeben werden und dass Sätze der verknüpften Tabelle ohne Bezug zu einem Satz in der Haupttabelle erlaubt sind. Ignorieren setzt also die RI-Regeln außer Kraft und ist von daher im allgemeinen zu vermeiden.

Auf .NULL. setzen (Nullify)

Die Nullify-Option wird vom VFP Assistenten für referentielle Integrität nicht angeboten. xCase hingegen stellt Nullify als RI-Option für die DELETE-Regel zur Verfügung. Nullify bedeutet, dass beim Löschen eines Satzes alle Fremdschlüssel in anderen Tabellen, die auf diesen Satz verweisen, auf .NULL. gesetzt werden sollen.

Auf Defaultwert setzen (set to default)

Auch das ist eine Option für die DELETE-Regel, die vom VFP Assistenten nicht angeboten wird. Die Option bedeutet, dass beim Löschen eines Hauptsatzes alle Fremdschlüsseleinträge, die sich auf diesen Satz beziehen, auf den als DefaultValue in der Datenbank hinterlegten Wert gesetzt werden. Das macht durchaus Sinn, wenn es in der Haupttabelle einen Satz gibt, dessen Primärschlüssel gleich dem DefaultValue der Fremdschlüsselfelder der zugehörigen verknüpften Tabellen ist. An einen solchen „Diverse“-Satz wird dann ein Satz verknüpft, wenn der eigentliche Satz der Haupttabelle gelöscht wird.

Was kommt wohin?

Soweit also die Theorie der RI-Regeln, die man in der Datenbank hinterlegen kann. Die Frage, die sich dem Klassenmodellierer stellt ist aber, sollen diese Regeln überhaupt in der Datenbank abgelegt werden oder sollen sich die Klassen selbst darum kümmern? Im Prinzip haben wir drei Möglichkeiten:

  1. Wir können die Sicherstellung der referentiellen Integrität komplett der Datenbank überlassen.

  2. Wir können andererseits auch alles in den Klassen abhandeln

  3. Oder wir entscheiden uns für ein teils/teils

Option 1

Option 1 bietet sich an, wenn die zu erstellende Anwendung ihre Daten immer in der selben Datenbank speichern wird, wir also keinen Wechsel zu einer anderen Datenbank berücksichtigen müssen. Bei einem Wechsel der Datenbank müssten natürlich alle RI-Regeln in der zweiten oder dritten Datenbank erneut erstellt werden. Diese Variante hat auch den Nachteil, dass Aktionen, die mit dem Löschen eines Objektes verknüpft sind, z.B. ein Eintrag in einer Log-Datei wer, wann und warum das Objekt gelöscht hat, ebenfalls in der Datenbank hinterlegt werden müssen. Wenn Objekte auf Grund von RI-Regeln gelöscht werden, hat der Code in der Klasse des Objektes darauf keinen Einfluss mehr und kann also auch nicht darauf reagieren.

Option 2

Wenn die Applikation auf den unterschiedlichsten Datenbanken arbeiten soll, tun wir gut daran, so wenig wie möglich an Logik in der Datenbank abzulegen, da sich das bald zu einem Wartungsalptraum auswachsen könnte. In diesem Fall ist also Option 2 angebracht, d.h. wir hinterlegen den Code, der die referentielle Integrität sicherstellt, in den Klassen unserer Anwendung. Dies erfordert allerdings einen nicht unerheblichen Aufwand auf Seiten der Klassenprogrammierung. Beim Löschen eines Objektes zum Beispiel müssen zunächst alle Objekte, die mit dem zu löschenden Objekt verknüpft sind, gefragt werden, ob das Objekt gelöscht werden darf. Das kann man sich ähnlich vorstellen, wie die QueryUnload Methode der VFP Forms, die darüber entscheiden können, ob die Form geschlossen werden darf oder nicht. Erst wenn alle "Ja" gesagt haben, darf das Objekt tatsächlich gelöscht werden. Anschließend müssen wiederum alle betroffenen Objekte darüber informiert werden, dass das Objekt tatsächlich gelöscht wurde.

Wie sieht sowas in der Praxis aus? Sagen wir, es liegt eine Geschäftsregel vor, die besagt, dass kein Kunde gelöscht werden darf, für den noch offene Rechnungen vorliegen. Außerdem sind mit dem Kunden zusammen auch alle Karteikarten zu löschen, die für diesen Kunden angelegt wurden. Wenn also ein Kunde gelöscht werden soll, müsste die Kundenverwaltung die OffenePostenVerwaltung fragen, ob das in Ordnung geht, z.B. durch Aufruf einer Methode QueryDelete(<KunID>). Die gleiche Frage wird an die Karteiverwaltung gestellt. Wenn von beiden ein OK kommt. wird der Kunde gelöscht und der OffenePostenVerwaltung sowie der Karteiverwaltung anschließend eine Nachricht zugestellt, dass der Kunde gelöscht wurde. Diese löschen dann alle Sätze (ausgeglichene Posten, Zahlungseingänge, Karteikarten, Karteieinträge etc.) die sich auf diesen Kunden beziehen. Eine ganze Menge Programmierarbeit, wie man sieht.

Option 3

Wenn die Gefahr besteht, dass ein Wechsel zu einer anderen Datenbank im Lebenslauf des zu erstellenden Programms ansteht, die Flexibilität aber nicht so weit geht, dass jeder Anwender seine Datenbank selbst festlegen kann, ist Option 3 eventuell die richtige. Hierbei wird unterschieden zwischen den Geschäftsregeln, die darüber entscheiden, ob ein Kunde gelöscht werden darf und den Regeln, die die referentielle Integrität sicherstellen.

Überlassen Sie die Entscheidung, ob ein Kunde gelöscht werden darf, einer Methode der Kundenklasse, stellen Sie in den RI-Regeln nur sicher, dass auch nach dem Löschen des Kunden die referentielle Integrität Ihrer Datenbank gesichert ist.

Auch hierbei besteht die Gefahr, dass das Löschen eines Objektes auf Grund von RI-Regeln unbemerkt vor sich geht und eventuell erforderliche Folgeaktionen unterbleiben.

Conclusio

Die Zeit der relationalen Datenbanken ist noch lange nicht abgelaufen, wie es scheint. Deshalb sollten wir unsere Erfahrungen aus der Modellierung von Datenbanken nicht über Bord werfen, auch wenn sich der Stellenwert der Datenmodellierung gegenüber der Klassenmodellierung geändert hat. Aus der Verbindung von beidem lassen sich heute Anwendungen erstellen die die Flexibilität, Performance und Sicherheit von relationaler Datenhaltung mit der Benutzerfreundlichkeit, Reichhaltigkeit und Wartbarkeit objektorientierter Software vereinen.