Wirtschaftsinformatik: Komplexe Datenbank-Anwendungen
Sie sind hier: Startseite › Wirtschaftsinformatik › Komplexe Datenbank-Anwendungen (Grundlagen)
IC / CM, Kurs vom 01.10.2005 - 31.03.2006
Java
Klassen, Objekte und Methoden
Klassen bestehen aus Feldern (Instanzvariablen, Attribute) und Methoden (Funktionen, Prozeduren), Muster für die Erzeugung von Objekten.
Objekte sind Instanzen einer Klasse, haben einen
fest definierten Zustand (Werte der Felder) und
eine Identität (Speicheradresse). Der Begriff
Gleichheit bezieht sich auf die Zustände
verschiedener Identitäten (equals
).
Methoden stellen Dienste auf einem Objekt bereit.
Typkompatibilität
Zu unterscheiden sind verschiedene Typen:
- Primitive Typen (Wertbeschreibung mit
boolean, byte, short, int, long, char, float, double
), - Referenztypen (Zeiger auf Objekte: Klassen, Interfaces, Arrays, Null) und
- generische Typen (
List<Integer>, Map<String, Kunde>
).
Bei typisierten Sprachen ist besonders vorteilhaft, dass Fehler nicht erst zur Laufzeit angezeigt werden. Schnittstellen werden durch Klassen implementiert und stellen die Typkompatibilität sicher.
Ein Beispiel für Typkompatibilität:
list l = new LinkedList()
. Der Bereich
LinkedList
ist austauschbar,
z.B. durch ArrayList
;
beide sind typkompatibel zu list
! Mit LinkedList
ist ein sehr schnelles
Einfügen möglich, mit ArrayList
ein sehr schnelles
Indizieren.
Schnittstellen contra Ober-/Unterklassen
Eine Entwicklung auf Grundlage von Schnittstellen schafft Unabhängigkeit von konkreten Implementierungen.
Ober-/Unterklassen dienen der Vererbung von Feldern und Methoden, auch hier wird dadurch eine Typkompatibilität gewährleistet. Ein weiterer Vorteil ist die Code-Wiederverwendung durch die einmalige Implementierung gemeinsamer Funktionalität.
Über Ausnahmen (exceptions
)
wird die Verletzung semantischer Einschränkungen behandelt.
Sobald Programme größer werden, dienen innere
Klassen zur Strukturierung und verringern die allgemeine
Komplexität; sie können Standard-Klassen, anonyme Klassen
oder statische Klassen sein.
Pakete sind ein weiteres Strukturierungsmittel für Anwendungen, organisieren Klassen und ermöglichen eine hierachische Struktur.
Schnittstellen beschreiben gemeinsame Funktionen, erlauben jedoch keine Code-Wiederverwendung. Obertypen erlauben Code-Wiederverwendung, legen jedoch Implementierungsdetails durch Vorgabe der Vererbungsstruktur fest. Delegation verbindet die Vorteile von Schnittstellen und Obertypen.
Ausnahmebehandlung
Für die Ausnahmebehandlung gibt es zwei Möglichkeiten:
- Die Ableitung von
Exception
(checked exception: Benutzender Code muss den Konstruktor in einentry-catch
-Block einschließen, erzwingt eine explizite Behandlung, macht den Code aber unübersichtlicher) oder - die Ableitung von
RuntimeException
(unchecked exception: wenn in der Ausnahmebehandlung nicht auf den Fehler reagiert werden kann und nur ein Programmabbruch möglich ist, wird seit einiger Zeit als Standardvariante favorisiert).
Wird von Exception
abgeleitet, muss eine
throws
-Klausel im Quelltext stehen.
Protokollierung
Die Protokollierung (Logging) dient der Ausgabe von
Protokollinformationen zur Laufzeit des Programms und ist eine
Alternative zu Print-Anweisungen
im Code (System.out.println(...)
).
Logging kann ohne
Änderung des Codes extern konfiguriert werden und
ermöglicht ein außerordentlich schnelles Finden von
Fehlern. Es gibt verschiedene Protokollierungsstufen
(debug, info, warn, error, fatal
). Der Nutzer muss nur
noch entscheiden, wo die Ausgabe stattfindet.
JDBC
Schritte eines JDBC-Programmes
Relationale Datenbanken arbeiten mengenorientiert (Mengen), Programme jedoch satzorientiert (Variablen). Um diese Sichtweisen zu verbinden, sind verschiedene Schritte erforderlich.
Zuerst wird ein Verbindungsobjekt als Platzhalter (Connection
)
für eine Datenbankverbindung erzeugt (Treiber laden, Verbindung herstellen).
Auf Serverseite erfolgt die Bindung von Ressourcen (Prozesse). Daraufhin werden
SQL-Befehle
an das Datenbanksystem über ein Befehlsobjekt abgesetzt (Befehl
erzeugen und ausführen). Über ein Ergebnismengenobjekt
(ResultSet
) werden die Abfrageergebnisse bereitgestellt.
Abschließend erfolgt zum Zugriff auf die Ergebnisdatensätze
eine Iteration über das Ergebnismengenobjekt (Ergebnismenge verarbeiten).
JDBC-Schnittstellen
sind Connection
, Statement/PreparedStatement
und ResultSet
. Am Ende müssen die Verbindung
geschlossen und die Ressourcen wieder freigegeben werden.
Das waren die wesentlichen Schritte, die Funktionen, eines JDBC-Programmes. Doch wie ist ein solches generell aufgebaut?
Aufbau eines JDBC-Programmes
Einerseits wird ein Datenbanksystem gebraucht
(z.B. Derby). Dieses ist frei
verfügbar, komplett in Java geschrieben, hat einen geringen
Ressourcenverbrauch und ist ideal für Entwicklungszwecke
geeignet. Andererseits ist noch die Hilfsklasse
DBUtils
notwendig. Sie stellt Funktionen zum
Öffnen und Schließen von Verbindungen zur Verfügung,
wandelt Exceptions
in RuntimeExceptions
und hat ein eingebautes Logging.
In JDBC
erfolgt eine individuelle Implementierung der
Schnittstellen durch Treiber, zugeschnitten auf das jeweilige
Datenbanksystem. Die Verwaltung der Treiber wird über einen
Treibermanager (Schnittstelle DriverManager
)
realisiert. Das schafft eine relative Unabhängigkeit
(durch syntaktische Unterschiede sind Anpassungen notwendig)
der Anwendungen vom eingesetzten Datenbanksystem, da diese nur mit
den Schnittstellen arbeiten. Der Treibermanager dient als
Fabrik zur Erzeugung von Datenbankverbindungen und
stellt eine Service-Provider-Schnittstelle
bereit, in die sich die Treiber einklinken. Die Auswahl der
richtigen Treiber erfolgt über URLs
(z.B. jdbc:hsqldb:...
)
Treibertypen
Es gibt vier verschiedene Treibertypen: Mit einem JDBC-ODBC-Bridge-Treiber werden JDBC-Aufrufe in ODBC-Aufrufe übersetzt (z.B. Nutzung für Zugriff auf Access-Datenbanken). Diese Treiber sind jedoch sehr langsam.
Native Treiber übersetzen JDBC-Aufrufe in native API-Aufrufe, allerdings erfordert das native Bibliotheken auf dem Klienten. Die Spezifikationen des Datenbanksystems können zwar ausgenutzt werden, zum Laufen sind jedoch proprietäre Bibliotheken erforderlich.
Generische Protokoll-Treiber sind komplett in Java und JDBC-Aufrufe werden in ein generisches Datenbankprotokoll übersetzt. Der Client kommuniziert mit einer Middleware, native Bibliotheken sind auf dem Klienten nicht erforderlich.
Native Protokoll-Treiber sind ebenfalls komplett in Java, der Treiber wird bei Bedarf gezogen und JDBC-Aufrufe werden in das native Datenbankprotokoll übersetzt. Auch hier sind keine nativen Bibliotheken auf dem Klienten erforderlich.
Datenbankzugriff
Der Datenbankzugriff kann mit Statement
(Erzeugen der Verbindung, Erzeugen des Datenbankbefehls,
Ausführen des Datenbankbefehls mit Angabe des
SQL-Textes
und Erzeugung der Ergebnismenge, Iteration durch die Ergebnismenge)
oder mit PreparedStatement
(Erzeugung
der Verbindung wie vorher, Angabe des SQL-Textes bei
Befehlserzeugung, Setzen des ersten Parameters auf einen Wert,
Ausführen des Datenbankbefehls und Erzeugung der Ergebnismenge,
ohne SQL-Text,
da dieser bereits vorbereitet wurde) erfolgen. Die mit
PreparedStatement
gegebene Form der Befehlsvorbereitung
bietet Vorteile bei der mehrfachen Benutzung eines
Befehls. Es wird nur einmal ein Anfrageplan erzeugt und
gespeichert. Bei jeder Ausführung wird nur noch der erzeugte
Anfrageplan ausgeführt.
Mit Batch Updates können mehrere SQL-Anweisungen in einer Datenbankkommunikation ausgeführt werden. Folge ist eine Performanzsteigerung durch weniger Netzwerkverkehr.
Datenbankverbindungen
Datenbankverbindungen sind aufwändig in der Erzeugung und verbrauchen Ressourcen auf dem Datenbankserver. Daher sollten Datenbankverbindungen selten erzeugt werden und nur wenige Datenbankverbindungen gleichzeitig geöffnet sein.
Die direkte Erzeugung von Datenbankverbindungen mit
DriverManager.getConnection(dbUrl, uname, passwd)
ist
ungünstig, da die Verbindungsverwaltung durch das
Anwendungsprogramm durchgeführt werden muss.
Daher sollte eine indirekte Erzeugung über
die Schnittstelle DataSource
erfolgen, eine Fabrik zur
Erzeugung von Verbindungen. Die Operation getConnection()
liefert Verbindungen. Diese werden aber möglicherweise nicht
direkt auf der Datenbank erzeugt, sondern aus einer Menge
vorgehaltener Verbindungen "geborgt"
(connection pooling).
Diese Schnittstelle wird z.B. durch Applikationsserver bereitgestellt. Die Konfiguration der Datenquelle erfolgt außerhalb des Java-Codes. Änderungen an der Konfiguration (z.B. die Verlegung der Datenbank auf einen anderen Server) wirken sich somit nicht auf das Programm aus.
Systemstrukturen
Grobe Architektur eines Datenbanksystems
Die Architektur eines Datenbanksystems setzt sich im Groben aus den Systembestandteilen Datensystem (oberste Ebene), Zugriffssystem und Speichersystem (unterste Ebene) zusammen. Parallel dazu greifen sowohl die Transaktionsverwaltung als auch die Metadaten auf alle drei Ebenen zu. Zu unterscheiden auf den einzelnen Systemebenen sind die entsprechenden Objekte und Operationen.
Das Datensystem ist gekennzeichnet durch Objekte wie Relationen und Tupel sowie Operationen wie Relationenoperationen.
Das Zugriffssystem enthält Objekte wie Sätze und Zugriffspfade sowie Operationen wie Sätze einfügen, löschen, ändern oder Zugriffspfade verwalten.
Das Speichersystem besteht aus Objekten wie Datenbankpuffer, Dateien und Blöcke sowie Operationen wie Datenbankpuffer verwalten, Dateien verwalten oder Blöcke lesen/schreiben.
Blöcke, Dateien, Seiten
Ein Block ist die kleinste adressierbare Einheit. Dateien sind eine Menge von Blöcken. Eine Seite ist ein bestimmter Block innerhalb einer Datei. Das relationale Datenbanksystem arbeitet mit Seiten. Datenbankpuffer verbinden Extern- und Hauptspeicher. Mehrere Blöcke einer Datei werden in einen Pufferbereich im Hauptspeicher gelegt und dieser Pufferbereich anschließend von verschiedenen Prozessen genutzt (shared memory). Der gemeinsam genutzte Puffer ermöglicht den beteiligten Prozessen einen schnellen Zugriff auf ein und dieselbe Seite.
Die Verwaltung von Sätzen in Seiten wird
über den Tuple Identifier
(TID)
realisiert, einem eindeutigen Identifikator des Datensatzes. Dieser
setzt sich aus Seitennummer und Index im Seitenverzeichnis zusammen.
TID=(1001,3)
liefert demnach folgende Anweisungen: Gib
mir Seite 1001. Nimm den dritten Index und folge dem
Pointer. Der
Pointer verweist auf den
Beginn des jeweiligen Datensatzes innerhalb der angegebenen Seite.
Der TID
ist eine physische Identifikation (Wo...?),
während ein Primärschlüssel eine logische
Identifikation darstellt.
Die Speicherstruktur für Sätze ist wie folgt zu beschreiben: Zu Beginn steht der Relationen-Identifier, gefolgt vom Tuple-Identifier. Anschließend folgt die Anzahl der Felder mit fester Länge (Speicherort steht im Data Dictionary) sowie die Anzahl der Felder mit variabler Länge. Dann folgen die Felder mit fester Länge (Feld 1 bis Feld n), ein Zeiger-Array (zeigt Speicherort der variablen Felder) sowie die Felder mit variabler Länge (Feld 1 bis Feld n).
Indizes
Indizes sind Zugriffspfade, bilden logische Identifizierer auf physischen ab und müssen schnell berechenbar sein. Der interne Zugriff auf Sätze erfolgt per TID, der externe Zugriff über benutzerdefinierte Attribute. Die Abbildung von Attributen auf TID wird über Hilfsstrukturen realisiert. Die geläufigste Variante schneller Berechnung sind B-Bäume (Tiefensuche: Knoten und Blätter sind Seiten). Indizes sollten auf Primärschlüssel, referentielle Integrität (Fremdschlüssel) und eventuell häufige Suchstellen gesetzt werden.
Beim geclusterten Index erfolgt die physische Speicherung der Datensätze entsprechend der Indexsortierung. Datensätze, deren Indexwerte nahe beieinander liegen, werden physisch nahe beieinander gespeichert (auf der gleichen Seite). Es ist nur ein geclusterter Index pro Tabelle möglich. Beim nichtgeclusterten Index erfolgt die physische Speicherung der Datensätze unabhängig von der Indexsortierung und es kann beliebig viele pro Tabelle geben. Nichtgeclusterte Indizes müssen dicht sein. Mit einem dichten Index (dense) erfolgt ein direkter Zugriff über den TID auf den Datensatz. Ein dünn gestreuter Index (sparse) ist ein Zeiger auf Seiten, ermöglicht die Suche nach einem Datensatz innerhalb der Seite und benötigt wenig Speicher.
Abschließend noch ein paar Worte zu weiteren Aspekten von Indizes. Überdeckende Indizes (covering) ermöglichen die Speicherung weiterer Attributwerte im Index. Das führt zu einer Vermeidung von Zugriffen auf Datenseiten, da alle benötigten Daten im Index vorliegen. Weiterhin sei auf die Kosten für Einfüge-, Lösch- und Änderungsoperationen hingewiesen: Bei Datenänderungen in den Tabellen müssen die Indizes angepasst werden, es muss also eine Wartung von Indizes erfolgen. Indizes sind kein Allheilmittel. Sie bieten keine Vorteile bei kleinen Tabellen. Hier kann ein Tabellen-Scan schneller sein (abhängig von der Selektivität der Indexattribute).
Implementierung in kommerziellen Datenbanksystemen
Wie genau sieht es nun mit der Implementierung in kommerziellen Datenbanksystemen aus? SQL Server arbeitet mit B-Bäumen, geclusterte Indizes sind dünn gestreut und es ist eine Indexwartung bei Einfüge-, Änderungs- und Löschoperationen notwendig. In DB2 werden ebenfalls B-Bäume verwendet, geclusterte Indizes sind dicht und eine explizite Indexreorganisation ist erforderlich. Oracle nutzt B-Bäume, Hash und Bitmap, geclusterte Indizes werden in einer indexorganisierten Tabelle (unique/clustered) abgebildet.