Wirtschaftsinformatik: Komplexe Datenbank-Anwendungen

Sie sind hier: StartseiteWirtschaftsinformatikKomplexe Datenbank-Anwendungen (Grundlagen)

IC / CM, Kurs vom 01.10.2005 - 31.03.2006

Komplexe Datenbank-Anwendungen (Grundlagen): Java (Klassen, Objekte und Methoden; Typkompatibilität; Schnittstellen contra Ober-/Unterklassen; Ausnahmebehandlung; Protokollierung), JDBC (Schritte eines JDBC-Programmes, Aufbau eines JDBC-Programmes, Treibertypen, Datenbankzugriff, Datenbankverbindungen), Systemstrukturen (Grobe Architektur eines Datenbanksystems; Blöcke, Dateien, Seiten; Indizes; Implementierung in kommerziellen Datenbanksystemen)

  1. Java
  2. JDBC
  3. Systemstrukturen

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:

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:

  1. Die Ableitung von Exception (checked exception: Benutzender Code muss den Konstruktor in einen try-catch-Block einschließen, erzwingt eine explizite Behandlung, macht den Code aber unübersichtlicher) oder
  2. 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.

Zum Menü Wirtschaftsinformatik | Zum Seitenanfang

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.

Zum Menü Wirtschaftsinformatik | Zum Seitenanfang

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.