JDBC Udo Kelter 25.11.2016 Zusammenfassung dieses Lehrmoduls JDBC (Java Database Connectivity) ist eine Schnittstelle, über die von Java-Programmen aus auf die Inhalte relationaler Datenbanken zugegriffen werden kann. Dieses Lehrmodul stellt die wichtigsten Funktionen vor. Insb. wird die Frage beantwortet, wie mit einer typsicheren Sprache wie Java die Ergebnisse von Abfragen, deren Typ man nicht statisch bestimmen kann, verarbeitet werden können. Vorausgesetzte Lehrmodule: obligatorisch: – Einführung in SQL Stoffumfang in Vorlesungsdoppelstunden: 1.0 1 JDBC 2 Inhaltsverzeichnis 1 Motivation 3 2 Aufbau einer Verbindung zum Datenbankserver 2.1 JDBC-Treiber laden . . . . . . . . . . . . . . . . . . . . . . . 2.2 Connection-Objekt erzeugen . . . . . . . . . . . . . . . . . . . 4 4 5 3 SQL-Statements 3.1 SQL-Statement-Objekte erzeugen . . . . . . . . . . . . . . . . 3.2 executeUpdate . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 executeQuery . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 6 7 4 ResultSets 4.1 ResultSet als lineare Liste . . . . . . 4.2 Cursors (Positionsmarken) . . . . . . 4.3 Positionierbare ResultSets . . . . . . 4.4 Auslesen eines Tupels . . . . . . . . 4.5 Automatisch aktualisierte ResultSets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 8 9 10 5 Änderbare ResultSets 5.1 Motivation . . . . . 5.2 Ändern eines Tupels 5.3 Löschen von Tupeln 5.4 Einfügen von Tupeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 11 12 12 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Vorübersetzte Statements 12 7 Fehlerbehandlung Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 15 c 2016 Udo Kelter Stand: 25.11.2016 Dieser Text darf für nichtkommerzielle Nutzungen als Ganzes und unverändert in elektronischer oder gedruckter Form beliebig weitergegeben werden und in WWW-Seiten, CDs und Datenbanken aufgenommen werden. Jede andere Nutzung, insb. die Veränderung und Überführung in andere Formate, bedarf der expliziten Genehmigung. Die jeweils aktuellste Version ist über http://kltr.de erreichbar. JDBC 1 3 Motivation Viele einfache ad-hoc-Abfragen zu einer bestehenden relationalen Datenbank können mit wenigen Abfragen über einen interaktiven Kommandointerpreter, den jedes DBMS-Produkt mitliefert, erledigt werden. Für komplexere Abfragen und erst recht umfängliche Änderungen von Daten in einer Datenbank ist eine derartige interaktive, also manuelle Vorgehensweise nicht geeignet. Stattdessen sollte bzw. muß von einem (laufenden) Programm aus auf die Datenbank zugegriffen werden. Hierbei treten folgende Detailprobleme auf: – Aufbau und Überwachung einer Verbindung zum Datenbankserver – die Konversion von Daten in der Datenbank in den Laufzeitobjekten der jeweiligen Programmiersprache und umgekehrt. In der Regel weisen die Typsysteme der Datenbank und der Programmiersprache erhebliche Unterschiede auf. Besonders schwierig ist speziell für Abfragen die Entwicklung einer Laufzeit-Datenstruktur, die die zurückgelieferte (Ergebnis-) Relation implementieren kann. – die Gestaltung geeigneter und leicht nutzbarer APIs für sämtliche SQL-Kommandos – die Fehlerbehandlung. Im Prinzip kann z.B. bei der üblichen Prozeßarchitektur von DBMS-Laufzeitumgebungen jederzeit die Verbindung zum DBMS-Serverprozeß gestört sein. Fast alle vorstehenden Detailprobleme müssen spezifisch für die jeweilige Programmiersprache gelöst werden. Daher existieren für die gängigen Programmiersprachen jeweils eigene Anbindungen, um von Programmen in dieser Sprache auf eine relationale Datenbank zugreifen zu können. Man kann ganz grob zwei Arten von Anbindungen unterscheiden: – reine APIs bzw. Bibliotheken: die Bibliotheken werden zu dem Programm, das eine Datenbank benutzt, nach dessen Kompilierung hinzugebunden. Die Funktionen der Bibliothek werden durch manuell erstellte Aufrufe benutzt. Derartige APIs werden auch als c 2016 Udo Kelter Stand: 25.11.2016 JDBC 4 Treiber (driver) bezeichnet, in Analogie zum Anschluß von Plattenlaufwerken. Ein Entwickler hat hier viele Gestaltungsmöglichkeiten, muß sich aber auch um viele Details kümmern. – Präprozessoren und automatisierte Konversionen: Die Konversion von Daten und andere Details werden hier durch einen Präprozessor automatisiert, der insb. die Datenstrukturen analysiert, die persistent gemacht werden sollen, und entsprechende Tabellen definiert und API-Aufrufe generiert. Auf der einen Seite wird hier viele manuelle Programmierung vermieden, andererseits muß man i.d.R. zunächst ein komplexes Framework verstehen. In diesem Lehrmodul stellen wir ein API für die Sprache Java vor, JDBC. JDBC (Java Database Connectivity) ist “... the industry standard for database-independent connectivity between the Java programming language and a wide range of SQL databases and other tabular data sources, such as spreadsheets or flat files.”1 2 Aufbau einer Verbindung zum Datenbankserver 2.1 JDBC-Treiber laden Ein Datenbankprodukt, das JDBC unterstützt, muß eine eigene Implementierung des APIs mitliefern. Dieser Treiber wird identifiziert durch Paket- und Klassennamen, beispielsweise: org.postgresql.Driver. Der Treiber muß als erstes zum Benutzerprogramm hinzugebunden und geladen werden, und zwar mit einem Kommando der Form Class.forName(driver class);. Beispiel: Class.forName("org.postgresql.Driver"); 1 Quelle: http://www.oracle.com/technetwork/java/javase/jdbc/index.html. c 2016 Udo Kelter Stand: 25.11.2016 JDBC 2.2 5 Connection-Objekt erzeugen Damit das laufende Applikationsprogramm mit dem DBMS-Serverprozeß kommunizieren kann, muß eine Kommunikationsverbindung zwischen beiden Prozessen hergestellt werden. Zur Laufzeit werden solche Verbindungen durch Connection-Objekte repräsentiert. Deklariert wird eine derartiges Objekt wie folgt Connection con = null; Um die Verbindung tatsächlich aufzubauen, muß die Operation getConnection benutzt werden, die der DriverManager zur Verfügung stellt. Muster: con = DriverManager.getConnection(db, user, passw); Die Parameter von getConnection sind wie folgt anzugeben: db Die Datenbank ist in folgender Syntax anzugeben: protokoll://rechnername/dateipfad Beispiel: jdbc:postgresql://pi81.informatik.uni-siegen.de/dbs1 Dieses Beispiel zeigt, daß Protokolle aufeinander aufbauen können. Oft wird z.B. ein Java-API auf Basis eines schon existierenden C-APIs implementiert. user Benutzername passw Passwort Wenn die Operation getConnection erfolgreich ausgeführt wurde, kann anschließend auf die Datenbank zugegriffen werden. 3 3.1 SQL-Statements SQL-Statement-Objekte erzeugen Letztlich will man SQL-Kommandos ausführen. Hierzu müssen zunächst Objekte erzeugt werden, die die SQL-Kommandos repräsentieren. Diese Objekte haben den vordefinierten Typ Statement. Hierbei sind mehrere Schritte zu trennen: c 2016 Udo Kelter Stand: 25.11.2016 JDBC 6 1. ein Statement-Objekt erzeugen und initialisieren 2. eine Anweisung eintragen 3. die enthaltene Anweisung ausführen (ggf. mehrfach) Ein Statement-Objekt kann mit der Operation createStatement erzeugt und initialisiert werden: Statement stmt; stmt = con.createStatement(); Im Zustand des Statement-Objekts ist vermerkt, zu welcher Verbindung es gehört. Daher braucht die Verbindung nicht mehr jedesmal erneut angegeben zu werden. Mit einem initialisierten Statement-Objekt können nun beliebige SQL-Kommandos durchgeführt werden. Im einfachsten Fall wird das SQL-Kommando textuell als Argument übergeben und ausgeführt. Wegen der unterschiedlichen Rückgaben sind hier zwei Fälle zu unterscheiden: – Abfragen: Zurückgeliefert wird hier eine Tabelle. Dementsprechend gibt es eine Operation zur Ausführung einer Abfrage, die eine Tabelle liefert (executeQuery). – Alle anderen SQL-Kommandos: Diese sind mit der Operation executeUpdate durchzuführen. 3.2 executeUpdate executeUpdate ist für alle SQL-Kommandos zu benutzen, die eine Datenbank ändern (insb. create table, insert, delete, ...). Die Syntax ist: int executeUpdate(String sql) Beispiel: int num = stmt.executeUpdate("INSERT ..."); executeUpdate liefert einen Integer-Wert zurück, der bestimmte Informationen über das Ausführungsergebnis darstellt: c 2016 Udo Kelter Stand: 25.11.2016 JDBC 7 – ggf. die Zahl der eingefügten / gelöschten / geänderten Tupel – 0 bei create table und u.ä. Kommandos 3.3 executeQuery In JDBC ist eine Tabelle durch die Klasse ResultSet implementiert. executeQuery liefert daher einen solchen ResultSet. Beispiel: ResultSet result = stmt.executeQuery("SELECT..."); Es gibt mehrere Arten von ResultSets, die wir i.f. vorstellen. 4 4.1 ResultSets ResultSet als lineare Liste Eine Tabelle ist konzeptuell betrachtet eine Kollektion von Tupeln. Es gibt unterschiedliche Methoden, Kollektionen zu implementieren. Die einfachste Form ist eine lineare Liste, dies ist auch das Standardverhalten von executeQuery. D.h. das Java-Kommando ResultSet result = stmt.executeQuery("SELECT..."); erzeugt eine lineare Liste (wozu der Name ResultSet nicht so ganz paßt) von Tupeln (rows). Lineare Listen kann man jeweils nur sequentiell von vorne bis zum Ende durchlaufen. Die Klasse ResultSet bietet hierzu die Operation next an. Beispiel: while(result.next()) { verarbeite aktuelles Tupel } 4.2 Cursors (Positionsmarken) Jeder ResultSet hat als Teil seines Zustands einen “Cursor”, also eine aktuelle Position in der Liste. Gedanklich besteht die Menge der Cursorpositionen aus den Positionen zwischen zwei aufeinanderfolgenden Tupeln und der Position vor dem ersten und nach dem letzten Tupel. Bei n Tupeln im ResultSet gibt es also n + 1 Cursorpositionen; diese sind von 0 bis n durchnumeriert. c 2016 Udo Kelter Stand: 25.11.2016 JDBC 8 Initial steht der Cursor vor dem ersten Tupel. Die Operation next bezieht sich implizit auf den Cursor des ResultSets. Der Haupteffekt von next besteht darin, den Cursor eine Position weiter zu verschieben. “Aktuelles Tupel”. Für jede Cursorposition p > 0 ist ein aktuelles Tupel definiert, und war das Tupel, das sozusagen zwischen den Cursorpositionen p − 1 und p steht. Die Tupel eines ResultSets sind auf diese Weise von 1 bis n durchnumeriert. Nur auf das aktuelle Tupel kann zugegriffen werden, hierauf gehen wir später genauer ein. 4.3 Positionierbare ResultSets Wenn man ein Abfrageergebnis nur unverändert ausgibt, reicht eine lineare Liste völlig aus. Wenn man hingegen den ResultSet wiederholt durchsuchen muß oder einzelne Tupel z.B. anhand ihrer Positionsnummer direkt lokalisieren will, ist eine wiederholte lineare Suche ineffizient. Statt als lineare Liste können ResultSets daher als beliebig positionierbare (“scrollbare”) Kollektion erzeugt werden; hierzu ist in der Operation createStatement das Argument ResultSet. TYPE SCROLL INSENSITIVE anzugeben. Beispiel: stmt = con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE); result = stmt.executeQuery("SELECT..."); Für einen solchen positionierbaren ResultSet stehen diverse Positionierungsoperationen, und zwar2 : – relative Veränderungen der aktuellen Cursorposition: next(), previous(), relative(int) 2 Details s. JDBC-Tutorial, Abschnitt “Cursors” http://docs.oracle.com/javase /tutorial/jdbc/basics/retrieving.html#cursors bzw. Spezifikation der Klasse ResultSet http://docs.oracle.com/javase/6/docs/api/java/sql/ResultSet.html. c 2016 Udo Kelter Stand: 25.11.2016 JDBC 9 – absolute Veränderungen: first(), beforeFirst(), last(), afterLast(), absolute(int) Mit der Operation int getRow() kann die Nummer der aktuellen Cursorposition bestimmt werden. Mit absolute(int) kann man später zu dieser Cursorposition zurückkehren. 4.4 Auslesen eines Tupels Man steht beim Zugriff von Programmen auf Datenbanken vor dem generellen Problem, daß die Typwelten, insb. die elementaren Datentypen und die Typkonstruktoren, nicht übereinstimmen. Im Fall von Java ist hier zusätzlich relevant, daß Java grundsätzlich als typsichere Sprache gestaltet ist, d.h. der Typ von Objekten kann bereits beim Compilieren bestimmt werden, was extreme softwaretechnische Vorteile bei der Identifikation von Fehlern bietet. Im krassen Widerspruch dazu steht die geringe Typsicherheit von SQL. Die Abfrage SELECT * FROM r liefert eine Tabelle eines Typs, der erst zur Laufzeit bestimmt werden kann. Weiterhin sind diverse Detailinformationen über die Datentypen der Spalten einer Tabelle aus den Abfragen nicht erkennbar. Beispielsweise kann der Compiler bei einer Abfrage executeQuery("SELECT A FROM r") statisch nicht wissen, welchen Typ die Spalte A hat. Noch weniger weiß der Compiler, wenn der String, der die Abfrage definiert, dynamisch erzeugt wird und von vorherigen interaktiven Eingaben abhängt. Daher muß letzten Endes ein Entwickler explizit angeben, welche Typen die Spalten eines zurückgelieferten ResultSets haben. Technisch wird dieses Problem dadurch gelöst, daß die Attribute des aktuellen Tupels einzeln durch typspezifische Kopieroperationen gelesen und z.B. auf Laufzeitvariablen übertragen werden. Die Operationsnamen dieser Kopieroperationen haben die Form getXXX, worin XXX ein Typname ist, z.B. getInt, getString usw. Eigenschaften dieser Operationen: – Sie beziehen sich auf das aktuelle Tupel. c 2016 Udo Kelter Stand: 25.11.2016 JDBC 10 – Sie lesen den Wert eines Attributs des aktuellen Tupels. – Sie liefern einen Wert vom Typ XXX zurück. Hierbei werden ggf. Werte implizit konvertiert(!), was u.U. nicht beabsichtigt war. Als Entwickler muß man also den Typ des Attributs kennen und die passende get-Operation benutzen. Für alle get-Operationen existieren jeweils zwei Varianten, wie das gewünschte Attribut identifiziert wird: – getXXX(string): anhand des Namens – getXXX(int): anhand der Position (1,...) Beispiele: name = result.getString("Vorname"); plz = result.getInt(5); Alle get-Operationen sind in der Spezifikation der Klasse ResultSet http://docs.oracle.com/javase/6/docs/api/java/sql/ResultSet.html genauer beschrieben. 4.5 Automatisch aktualisierte ResultSets Wenn ein ResultSet nach den bisherigen Verfahren durch ein executeQuery erzeugt wird, ist sein Inhalt eine Kopie der Daten aus der DB zum Zeitpunkt der Abfrage. Wenn sich die Datenbank danach ändert, bleibt der ResultSet unverändert, d.h. der ResultSet gibt ggf. den aktuellen Stand der Daten nicht mehr richtig wieder. In vielen Fällen ist es wünschenswert, daß der ResultSet sich automatisch an Änderungen in der Datenbank anpaßt. Die Lösung dieses Problems sind “sensitive” ResultSets, die mit dem Argument ResultSet. TYPE SCROLL SENSITIVE in der Operation createStatement erzeugt werden. Beispiel: stmt = con.createStatement( ResultSet.TYPE_SCROLL_SENSITIVE); result = stmt.executeQuery("SELECT..."); c 2016 Udo Kelter Stand: 25.11.2016 JDBC 5 11 Änderbare ResultSets 5.1 Motivation Die bisher eingeführten ResultSets waren unveränderlich, es sind nur lesende Operationen erlaubt. Oft wäre es aber praktisch, einen ResultSet zu durchlaufen und dabei bestimmte Attribute einzelner oder aller Tupel sofort nach dem Lesen zu verändern. Theoretisch kann man für jede einzelne Änderung ein update-Kommando mit executeUpdate durchführen. Dies wäre allerdings sehr umständlich und ineffizient. Eine elegantere Lösung sind änderbare ResultSets3 . Um einen änderbaren ResultSet zu erzeugen, ist die Operation createStatement mit dem Argument ResultSet.CONCUR UPDATABLE aufzurufen. Beispiel: stmt = con.createStatement( ResultSet.CONCUR_UPDATABLE); result = stmt.executeQuery("SELECT..."); 5.2 Ändern eines Tupels In einem änderbaren ResultSet können die Attribute des aktuellen Tupels mit updateXXX-Operationen verändert werden, z.B. updateInt, updateString usw. Diese sind analog zu den getXXX-Operationen konzipiert. Eigenschaften der updateXXX- Operationen: – Sie beziehen sich implizit auf das aktuelle Tupel. – XXX ist ein Typname. – Der 1. Parameter ist der Name (Typ: string) oder die Nummer (Typ: integer) des gewünschten Attributs. – Der 2. Parameter ist vom Typ XXX, er enthält den neuen Wert des Attributs. Auch hier werden ggf. Werte implizit konvertiert, was u.U. nicht beabsichtigt war. – Der Rückgabewert ist void. 3 Nach einer Änderung ist der Begriff ResultSet eigentlich nicht mehr korrekt. c 2016 Udo Kelter Stand: 25.11.2016 JDBC 12 Beispiele: result.updateString("Vorname", "Hans"); result.updateInt(5,57076); Alle update-Operationen sind in der Spezifikation der Klasse ResultSet beschrieben. Die Änderungen einzelner Attribute werden nicht sofort in die Datenbank übertragen, sondern nur gesammelt pro Tupel mit der Operation updateRow(). 5.3 Löschen von Tupeln Die Operation deleteRow() löscht das aktuelle Tupel im ResultSet und in der Datenbank. 5.4 Einfügen von Tupeln Das Einfügen von Tupeln in einen ResultSet wird durch eine spezielle zusätzliche Cursorposition und ein zugehöriges virtuelles “EinfügeTupel” (“insert row”) unterstützt. Zunächst muß man mit der Operation moveToInsertRow() den Cursor auf das Einfüge-Tupel setzen: result.moveToInsertRow(); Danach kann man mit den update-Operationen Attribute dieses Tupels ändern. Wenn man damit fertig ist, fügt man mit der Operation insertRow() das neue Tupel in den ResultSet und in die Datenbank ein: result.insertRow(); Danach kann man mit der Operation moveToCurrentRow() den Cursor wieder an die Stelle zurücksetzen, den er vor Ausführung des moveToInsertRow() hatte. 6 Vorübersetzte Statements Wenn man viele Daten ändert, sind änderbare ResultSets i.d.R. die beste Methode. Änderbare ResultSets werden aber nicht von allen Systemen unterstützt, dann bleibt nichts anderes übrig als mit einzelnen c 2016 Udo Kelter Stand: 25.11.2016 JDBC 13 Änderungskommandos zu arbeiten. Dies ist relativ rechenaufwendig, weil bei der Übersetzung des Kommandotextes jedesmal erneut alle üblichen Prüfungen auf korrekte Syntax, Vorhandensein der Bezeichner usw. durchgeführt werden müssen. Dieser Aufwand ist vermeidbar, wenn immer wieder fast das gleiche SQL-Kommando ausgeführt wird, und zwar mithilfe vorübersetzter Statements (Prepared Statements). Ein vorübersetzbares Statement ist ein Kommando-Text, der an den Stellen, wo ein Eingabeparameter (ein Bezeichner oder eine Konstante) erwartet wird, ein Fragezeichen enthält. Man kann dies auch als eine Funktionsdeklaration betrachten, jedes Fragezeichen steht für einen Parameter. Beispiel: String insertcmd = "INSERT INTO lieferungen " + "(Kundennummer, Lieferadresse, Betrag, Datum) " + "VALUES ( ?, ?, ?, 31.12.2016 )"; In diesem Beispiel wird die Eintragung mehrerer Lieferungen am 31.12.2016 in die Relation lieferungen vorbereitet. Vorübersetztende Statements werden in 3 Phasen definiert und ausgeführt: 1. Der vorzuübersetzende Text wird mit der Operation prepareStatement übersetzt, zurückgeliefert wird ein Objekt vom Typ PreparedStatement. Beispiel: PreparedStatement psInsertLfg; psInsertLfg = con.prepareStatement(insertcmd); 2. Die offenen Stellen, sozusagen die Parameter des PreparedStatements, werden gefüllt, s.u. 3. Nach Setzen der Werte aller Parameter wird das vervollständigte PreparedStatement mit der Operation executeUpdate ausgeführt. Beispiel: psInsertLfg.executeUpdate(); Setzen der Parameter eines PreparedStatements. Hierzu dienen setXXX-Kommandos, die für Objekte des Typs Preparedc 2016 Udo Kelter Stand: 25.11.2016 JDBC 14 Statement definiert sind. Analog wie bei den getXXX-Kommandos steht XXX auch hier wieder für einen elementaren Datentyp. Die setXXX-Kommandos haben 2 Parameter: 1. die Nummer des offenen Eingabeparameters 2. den zu benutzenden Wert, der vom Typ XXX sein muß Beispiele: psInsertLfg.setInt (1, 181917); psInsertLfg.setString (2, "Bahnhofstr. 55"); Weitere Details s. http://docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html 7 . Fehlerbehandlung Wie schon früher erwähnt sind bei jedem Datenbankzugriff prinzipiell immer Fehler möglich, z.B. Syntaxfehler oder der Verlust der Verbindung zum DBMS-Serverprozeß. Alle Datenbankzugriffe müssen daher selbst bei ersten Übungsaufgaben in try/catch-Blöcken gekapselt werden. Eine detaillierte Behandlung aller Fehlerursachen ist relativ aufwendig und kann ggf. Teile der Applikationslogik beinhalten. Bei ersten Übungen kann man auf eine detaillierte Fehlerbehandlung zunächst verzichten. Alle SQL-Zugriffsoperationen werfen Exceptions vom Typ SQLException. Über diese exception-Objekte können diverse Informationen über die Art des Fehlers herausgefunden werden. Die Klasse SQLException bietet hierzu folgende Operationen an: getMessage() liefert eine verbale Beschreibung des Fehlers getSQLState() liefert einen Fehlercode gemäß dem ANSI- bzw. ISOStandard getErrorCode() liefert einen herstellerspezifischen Fehlercode, dessen Bedeutung in der Produktdokumentation definiert sein muß. Bei einen Datenbankzugriff können i.a. mehrere SQLExceptions c 2016 Udo Kelter Stand: 25.11.2016 JDBC 15 auftreten. Deren Abhängigkeitsstruktur kann mit Hilfe der Operation getCause() abgefragt werden. Warnungen. Neben Fehlern können Datenbankzugriffe auch Warnungen produzieren. Während ein Fehler die nicht erfolgreiche Ausführung eines SQL-Kommandos anzeigt und ggf. zu einem Programmabbruch führt, treten Warnungen auch bei einer erfolgreichen Ausführung des Kommandos auf. Beispielsweise kann bei einer Konversion von Datenwerten ein Rundungsfehler aufgetreten sein. Warnungen müssen mit der Operation getWarnings abgerufen werden. Diese Operation ist u.a. für die Klassen Connection, Statement, PreparedStatement und ResultSet definiert, also bei allen Objekten aufrufbar, die Ergebnis von Datenbankzugriffen sind. Analog zu Fehlern sind Informationen über die Warnung abrufbar, und ein Datenbankzugriff kann mehrere Warnungen verursachen. Weitergehende Informationen u.a. zum Auslesen der Liste der SQLExceptions bzw. Warnungen s. http://docs.oracle.com/javase/tutorial/jdbc/basics/sqlexception.html c 2016 Udo Kelter Stand: 25.11.2016