Praktikum aus Softwareentwicklung 2, Stunde 7 Lehrziele/Inhalt 1. Datenbanken Datenbanken Datenbanken werden in Java über JDBC (ist ein Eigenname, wird aber oft als Abkürzung von Java Database Connectivity API gesehen) angesprochen. JDBC ist eine Abstraktionsschicht über Datenbanktreibern. Ohne Abstraktion müsste man bei einem Datenbankwechsel die Anwendung umprogrammieren. Design von JDBC JDBC wird seit 1995 entwickelt, erste Überlegungen gingen in Richtung einer Spracherweiterung, diese Ideen wurden aber verworfen und eine Treiberschnittstelle für Drittanbieter gebaut. JDBC lehnt sich an ODBC an, aber im Stil von Java: ODBC hat wenige Befehle aber sehr viele Optionen, JDBC hat viele einfache Methoden. Außerdem benutzt ODBC void-Zeiger und Java kennt keine Zeiger. Befehle an die Datenbank werden als String übergeben. Das erlaubt es Programmieren SQL-Befehle für eine Datenbank zu optimieren, aber man muss sich bewusst sein, dass eine solche Optimierung wieder eine Bindung an eine Datenbank bedeutet. Treiberarten in JDBC Es gibt vier Treiberarten in JDBC. Sie sind historisch bedingt: der Typ 1 Treiber ist ein Brücke von JDBC nach ODBC, vom Typ 2 ist ein Treiber wenn er aufrufe an native Treiber weiterleitet, vom Typ 3 ist ein Treiber wenn er voll in Java implementiert ist und an eine Middleware bindet und vom Typ 4 ist ein Treiber der voll in Java implementiert ist und direkt eine Datenbank ansprechen kann. Typ 1: Brücke Typ 1 die Brücke von JDBC nach ODBC war ein pragmatischer Ansatz von Sun, um vom Start weg so viele Datenbanken wie möglich anbinden zu können. Für ODBC waren damals viele Datenbanktreiber verfügbar. Nachteile dieses Ansatzes sind: die zusätzliche ODBC-Schicht kostet Leistung; höhere Wartung, es muss am Zielrechner ein ODBC-Treiber installiert und gepflegt werden. Typ 2: Partial Java Driver Gibt Aufrufe direkt an eine native Implementierung weiter. Da für Datenbanken native Treiber vorhanden waren, war dies eine Möglichkeit für Datenbank-Hersteller schnell Java-Treiber anzubieten. Der Nachteil dieses Ansatzes ist die Betriebssystemabhängigkeit der Treiber. Typ 3: Reiner Java Treiber zu einer Middleware Der Treiber ist völlig in Java implementiert und damit Betriebssystemunabhängig. Durch die Middleware ist das Programm auch Datenbankunabhängig. Nachteile: es muss einen Server geben wo diese Middleware installiert ist. © Markus Löberbauer 2010 Seite 22 Typ 4: Reiner Java Treiber zu einer Datenbank Der Treiber ist völlig in Java implementiert und dadurch Betriebssystemunabhängig. Typ 4 Treiber verbinden direkt auf die Datenbank und sind damit schnell. Ein kleiner Nachteil gegenüber Typ 3 ist die Abhängigkeit von der Datenbank. Installation von JDBC-Treibern Datenbanktreiber für JDBC kann man auf http://developers.sun.com/product/jdbc/drivers oder bei den Datenbankherstellerseiten finden. Zur Installation muss man den Treiber in den Klassenpfad aufnehmen. Will man einen Treiber benutzen, muss man ihn laden, dazu hat man die Möglichkeiten: a. System Property: jdbc.drivers, zB: a. java -Djdbc.drivers=org.apache.derby.jdbc.EmbeddedDriver Xyz b. System.setProperty("jdbc.drivers", "org.apache.derby.jdbc.EmbeddedDriver"); b. Manuelles laden der Treiberklasse, zB: a. Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); c. Seit JDBC 4 (Java 6.0), Laden als Java Service durch DriverManager, dazu muss der Treiber das Java Service java.sql.Driver anbieten Aufbauen einer Verbindung Eine Verbindung (Connection) zu einer Datenbank kann man über DriverManager.getConnection aufbauen. Dazu muss man eine Datenbank-Url und optional einen Benutzernamen und Passwort angeben. DriverManager: Verwaltet registrierte Treiber, baut Verbindungen auf Connection getConnection(String url, String user, String password) Datenbank URL Aufbau: jdbc:<subprotrokoll>:<subname> jdbc:<Datenbanktreiber>:<treiberspezifische Angaben> Derby o jdbc:derby:/path/to/Database o jdbc:derby:Database MySQL o jdbc:mysql://<host>:<port>/<Database> Arten von Statements Über die Connection kann man die Verbindung zur Datenbank verwalten, zB schließen, Informationen über die Datenbank abfragen, Transaktionen verwalten und Statement-Objekte anfordern. JDBC unterscheidet die Statement-Arten: Statement, PreparedStatement und CallableStatement Statement (Connection.createStatement): Absetzen von beliebigen SQL-Befehlen, einsetzbar für Werkzeuge, bei denen der Benutzer den Befehl eingeben kann und für vordefinierte Befehle. © Markus Löberbauer 2010 Seite 23 Beispiel: Statement stat = con.createStatement(); stat.executeUpdate("INSERT INTO test VALUES ('Hallo')"); PreparedStatement (Connection.prepareStatement): Absetzen von SQL-Befehlen mit Parametern, einsetzbar wenn Benutzer Parameter eingeben können. Parameter werden mit einem „?“ in den SQL-String eingesetzt, und mit set-Methoden vor dem Ausführen über ihre Position gesetzt, Positionen werden von 1 ab gezählt. Verhindert SQL-Injection. Kann auf der Datenbank vorkompiliert werden und ist damit schneller bei der Ausführung. Beispiel: PreparedStatement stat; stat = con.prepareStatement("INSERT INTO test VALUES (?,?)"); stat.setString(1, "Hallo"); stat.setString(2, "Welt"); stat.executeUpdate(); stat.setString(2, "Jane"); stat.executeUpdate(); CallableStatement (Connection.prepareCall): Ausführen von Datenbank-Prozeduren. CallableStatements können wie PreparedStatements parametrisiert werden. Ausgangsparameter werden unterstützt, sie müssen aber registriert werden (registerOutParameter). Übergangsparameter müsse ebenfalls als Ausgangsparameter registriert werden. Beispiel: CallableStatement cs = con.prepareCall("{ CALL GET_NUMBER_FOR_NAME(?, ?) }"); cs.registerOutParameter(2, java.sql.Types.INTEGER); cs.setString(1, "Duke"); cs.execute(); int number = cs.getInt(2); Abfragen der Ergebnisse Statements liefern Abfrageergebnisse als ResultSet zurück. ResultSet ist ein Cursur, es funktioniert wie ein Iterator von dem man Werte abfragen kann. Am Anfang steht das ResultSet vor der ersten Zeile, mit boolean next() kann man die nächste Zeile anspringen, der Rückgabewert von next gibt an ob eine gültige Zeile erreicht wurde. Die Werte einer Zeile können mit Methoden der Art get<Typ>(int spalte) und get<Typ>(String spaltenName) abgefragt werden. Fragt man einen Wert ab, kann man mit wasNull abfragen ob der Wert in der Datenbank SQL-NULL ist. Folgende weitere Methoden stehen zur verfügung: int findColumn(String spaltenName): sucht die Spaltennummer zu einer Spalte mit gegebenem Spaltennamen. boolean first(): Springt in die erste Zeile im ResultSet, liefert true wenn gültige Zeile erreicht wird. void beforeFirst(): Springt vor die erste Zeile im ResultSet. © Markus Löberbauer 2010 Seite 24 boolean last(): Springt in die letzte Zeile im ResultSet, liefert true wenn gültige Zeile erreicht wird. void afterLast(): Springt hinter die letzte Zeile im ResultSet. boolean absolute(int row): Spring in die Zeile mit der gegebenen Nummer: o row > 0 ... von oben (1 erste Zeile, 2 zweite Zeile, ...) o row < 0 ... von unten (-1 letzte Zeile, -2 vorletzte Zeile, ...) o liefert true wenn gültige Zeile erreicht wird. int getRow(): Liefert die Nummer der aktuellen Zeile Abbildung der SQL-Typen auf Java-Typen SQL-Typ CHAR, VARCHAR, LONGVARCHAR NUMERIC, DECIMAL BIT TINYINT SMALLINT INTEGER BIGINT REAL FLOAT, DOUBLE Java-Typ String java.math.BigDecimal boolean byte short int long float double BINARY, VARBINARY, LONGVARBINARY byte[] DATE java.sql.Date TIME java.sql.Time TIMESTAMP java.sql.Timestamp … siehe JSR-221, Appendix B, Date Type Conversion Tables Metadaten einer Datenbank Über DatabaseMetaData Connection.getMetadata() kann man auf Informationen der Datenbank zugreifen. Das ist wichtig wenn man Programme entwickelt, die die Datenbank nicht kennen, zB: Administrationsoberflächen; oder wenn man die Datenbank bei der ersten Verwendung initialisieren will. Es kann auf Daten wie die Datenbank-Url (getURL), den Benutzernamen (getUserName) und Beschreibbarkeit (isReadOnly) zugegriffen werden. Man abfragen was eine Datenbank unterstützt, zB: Transaktionen (supportsTransactions), Gruppierung (supportsGroupBy). Welche Beschränkungen eine Datenbank hat, zB: Maximale Länge eines Statements (getMaxStatementLength), Maximale Anzahl der parallel absetzbaren Statements (getMaxStatements) und Maximale Anzahl geöffneter Verbindungen (getMaxConnections). Wird bei den Beschränkungen 0 geliefert, bedeutet das, dass keine Beschränkung gibt oder die Beschränkung unbekannt ist. Und man inhaltsbezogene Daten abfragen, zB: welche Tabellen in einer Datenbank liegen, welche Spalten in einer Tabelle existieren und welche Typen die Spalten haben. © Markus Löberbauer 2010 Seite 25 Daten über Ergebnistabellen kann man über ResultSetMetaData ResultSet.getMetaData() abfragen. Darüber kann man beispielsweise abfragen wie viele und welche Spalten geliefert werden; welche Typen und Namen die Spalten haben und ob man auf eine Spalte schreiben kann. Transaktionen Eine Transaktion wird in JDBC gestartet, sobald man ein Statement absetzt und noch keine Transaktion läuft. Standardmäßig wird in JDBC jedes Statement als eine Transaktion behandelt. Braucht man länger laufende Transaktionen, dann muss man die Eigenschaft autoCommit der Verbindung auf false setzen (Conncetion.setAutoCommit). Eine laufende Transaktion kann man mit Connection.commit abschließen und mit Connection.rollback rücksetzen. Zusätzlich kann man während einer Transaktion Sicherheitspunkte (Savepoints) angelegen auf die man mit einem Rollback zurückspringen kann. Unterstützte Transaktions-Isolation Welcher Transaktions-Isolations-Level von einer Datenbank unterstützt wird kann man über DatabaseMetaData. supportsTransactionIsolationLevel abfragen. Die in JDBC bekannten Transaktionslevels sind in der Klasse Connection definiert: NONE: Kein Transaktionssupport => kein JDBC Treiber READ_UNCOMMITTED: dirty reads, non-repeatable reads und phantom reads können auftreten READ_COMMITTED: dirty reads sind verhindert; non-repeatable reads und phantom reads können auftreten REPEATABLE_READ: dirty reads und non-repeatable reads sind verhindert; phantom reads können auftreten SERIALIZABLE: dirty reads, non-repeatable reads und phantom reads sind verhindert. Beispiel: Connection con; ... try { con.setAutoCommit(false); Statement stat = con.createStatement(); stat.executeUpdate("INSERT ..."); stat.executeUpdate("INSERT ..."); stat.executeUpdate("UPDATE ..."); con.commit(); } catch (SQLException e) { con.rollback(); } Ausnahmebehandlung Alle JDBC-bezogenen Exceptions erben von SQLException, seit Java 6.0 gibt es eine feingranulare Aufteilung in die Fehlerklassen: SQLNonTransientException, SQLTransientException und SQLRecoverableException. Nicht-transient bedeutet, dass ein erneuter Versuch wieder fehlschlagen wird; transient bedeutet, dass ein erneuter Versuch durchgehen kann; und recoverable bedeutet, dass ein erneuter Versuch mit geänderten Daten durchgehen kann. Java DB (Derby) Seit Java 6.0 wird die Datenbank Java DB mit dem JDK geliefert. Diese Datenbank ist auch unter dem Namen Derby oder Apache Derby bekannt. Derby ist eine kompakte (Kern: 2,5MB), in Java © Markus Löberbauer 2010 Seite 26 entwickelte, einfach zu nutzende (ohne Installation), standardkonforme (SQL 99) Datenbank. Interessant ist auch, dass die erzeugten Daten-Dateien betriebssystemunabhängig sind. Will man von der Konsole aus mit Derby Arbeiten muss man die Umgebungsvariablen JAVA_HOME, DERBY_HOME und PATH setzen: JAVA_HOME=Pfad zur Java JDK Installation DERBY_HOME=Pfad zur Derby Installation PATH um DERBY_HOME/bin erweitern In DERBY_HOME sind die jar-Dateien von Derby: derby.jar: Kern, genügt für embedded DB derbynet.jar: Netzzugriff, Serverseitig derbyclient.jar: Netzzugriff, Clientseitig derbytools.jar: Verwaltungs-Werkzeuge derbyrun.jar verweist auf: derby.jar, derbyclient.jar, derbytools.jar und derbynet.jar Auf die Verwaltungs-Werkzeuge in derbytools.jar kann man über Batch-Dateien zugreifen. Das Werkzeug sysinfo liefert Informationen über die Java-Installation auf dem System; mit dblook kann man das Datenbankschema exportieren. ij ist eine Konsole mit der man SQL-Befehle an eine Datenbank absetzen kann. © Markus Löberbauer 2010 Seite 27