11. JDBC • • • • • • • Grundsätzliche Nutzung von JDBC Verbindungsaufbau Anfragen Analyse des erhaltenen Ergebnisses Veränderungen des Ergebnisses Einführung JUnit Einführung DBUnit Ziel: Verständnis für Konzepte von JDBC aufbauen Datenbanken Prof. Dr. Stephan Kleuker 276 Überblick: Datenbankanfragen mit JDBC class DriverManager Datenbankverbindung Connection con= DriverManager.getConnection(...); herstellen Statement stmt= con.createStatement(); Datenbankanfrage Ergebnisse verarbeiten Verbindung zur DB schließen Datenbanken ResultSet rs = stmt.executeQuery(...); rs.next(); int n = rs.getInt("KNr"); con.close(); Prof. Dr. Stephan Kleuker 277 Verbindungsaufbau mit einer Datenbank • Laden des Datenbanktreibers DriverManager.registerDriver( new oracle.jdbc.driver.OracleDriver()); • Aufbau einer Verbindung Connection con = DriverManager.getConnection ("jdbc:postgresql://rechner:5712/eshop" ,"user", "password"); • JDBC-URL identifiziert Datenbank Aufbau: jdbc:subprotocol:subname • Treibermanager übergibt JDBC-URL der Reihe nach an registrierte Treiber Driver.acceptsURL(String url) liefert true, falls der Treiber den String akzeptiert Datenbanken Prof. Dr. Stephan Kleuker 278 Auslesen der Oracle-Verbindungsdaten • Oracle „versteckt“ irgendwo Verbindungsdaten in tnsnames.ora • benötigt wird IP-Adresse, Port (default 1521), Name der DBInstanz (z. B. in SID) + Nutzername und Passwort • Daraus abgeleiteter Connection-String für HS-DB (SID: Ora11) Connection con = DriverManager.getConnection( "jdbc:oracle:thin:" + "@oracle-srv.edvsz.hs-osnabrueck.de" + ":1521:Ora11", "username", "passwort"); • Connection-String für DB auf lokalem Rechner (Oracle XE) Connection con = DriverManager .getConnection( "jdbc:oracle:thin:@localhost:1521:xe" , "username", "passwort"); Datenbanken Prof. Dr. Stephan Kleuker 279 Connection • Ergebnis von DriverManager.getConnection() ist eine Connection con (oder eine SQLException) • Connection ist Verbindung zur Datenbank • Connection ist teure Ressource: – Aufbau der Verbindung kostet viel Zeit – Anzahl gleichzeitiger Verbindungen häufig beschränkt • Wichtigste Aufgaben: – Erzeugen von Statement-Objekten – Beschreibungen von Datenbank und DBMS erfragen – Transaktionen handhaben Datenbanken Prof. Dr. Stephan Kleuker 280 Verbindungsanalyse durch Metadaten (Ausschnitt) DatabaseMetaData dbmd = con.getMetaData(); System.out.println("DB-Name: " + dbmd.getDatabaseProductName() + "\nDB-Version: " + dbmd.getDatabaseMajorVersion() + "\nDB-Release: " + dbmd.getDriverMinorVersion() + "\nTransaktionen erlaubt: " + dbmd.supportsTransactions() + "\nbeachtet GroßKlein :" + dbmd.storesMixedCaseIdentifiers() + "\nunterstützt UNION :" + dbmd.supportsUnion() + "\nmax. Prozedurname: " + dbmd.getMaxProcedureNameLength()); DB-Name: Oracle DB-Version: 11 DB-Release: 2 Transaktionen erlaubt: true beachtet GroßKlein :false unterstützt UNION :true max. Prozedurname: 30 Datenbanken Prof. Dr. Stephan Kleuker 281 Statement / Anfrage ausführen • Statement stmt = con.createStatement(); • Wird immer aus bestehender Connection erzeugt • Aufgabe: Ausführen einer SQL-Anweisung über die Connection • Mehrere parallele Statements pro Connection möglich • SELECT-Anweisung ausführen: ResultSet rs = stmt.executeQuery("SELECT * FROM Kunde"); • Daten ändern: int updates = stmt.executeUpdate("DELETE FROM Kunde ..."); Datenbanken Prof. Dr. Stephan Kleuker 282 Metadaten des Anfrageergebnisses ResultSet rs = stmt.executeQuery("SELECT * FROM Continent"); ResultSetMetaData rsmd = rs.getMetaData(); int spalten = rsmd.getColumnCount(); for (int i = 1; i <= spalten; i++) { // nicht i=0 System.out.println(i + ". Name: " + rsmd.getColumnName(i) + " Typ: " + rsmd.getColumnTypeName(i) + " Javatyp: " + rsmd.getColumnClassName(i)); } 1. Name: NAME Typ: VARCHAR2 Javatyp: java.lang.String 2. Name: AREA Typ: NUMBER Javatyp: java.math.BigDecimal Datenbanken Prof. Dr. Stephan Kleuker 283 Analyse von ResultSet ResultSet rs = stmt.executeQuery("SELECT * FROM Kunde"); • Ergebnis einer Selektionsanweisung: Tabelle • ResultSet enthält einen Datensatz-Zeiger (Cursor) zum Durchlauf der Tabelle • Voreinstellung: sequenziell und lesend • ab JDBC 2: nichtsequenzielle und aktualisierbare ResultSets • Zeiger steht initial vor der ersten Tabellenzeile • rs.next() positioniert zur nächsten Zeile, liefert false, falls bereits auf letzter Zeile Datenbanken Prof. Dr. Stephan Kleuker 284 Attributwerte auslesen (1/2) • Spaltenwerte (Attribute) einer Zeile mit getXXX()-Methoden lesen • Treiber konvertiert Daten, falls möglich, deshalb (fast) immer getString() nutzbar • Beispiel: Lesen einer ganzen Zahl in DB-Spalte kundenNr: int n = rs.getInt("kundenNr"); • Effizientere Methode, falls Spaltenindex bekannt: int n = rs.getInt(4); • Spaltenindex zum Spaltennamen finden int findColumn(String columnName) • Strategie: Spaltenindex einmalig ermitteln und merken, Werte danach immer über den Index abrufen Datenbanken Prof. Dr. Stephan Kleuker 285 Attributwerte auslesen (2/2) • Methode getObject() – Liest jeden beliebigen SQL-Datentyp – Liefert Ergebnis als entsprechenden Java-Typ • Nullwerte – Spalte kann leere Zellen enthalten (Nullwert, SQL-NULL) – Bei leerer Zelle liefert getInt() ebenfalls das Ergebnis 0 – Unterscheidung zu echter 0 möglich durch wasNull() • true, falls zuletzt mit getXXX() gelesene Zelle SQLNULL enthielt • false sonst Datenbanken Prof. Dr. Stephan Kleuker 286 Beispiel: Ausgabe eines Anfrageergebnisses ResultSet rs = stmt.executeQuery("SELECT * FROM Continent"); ResultSetMetaData rsmd = rs.getMetaData(); int spalten = rsmd.getColumnCount(); while (rs.next()) { for (int i = 1; i <= spalten; i++) { System.out.print(rs.getString(i) + " "); } System.out.print("\n"); Europe 9562488 } Asia 45095292 Australia/Oceania 8503474 Africa 30254708 America 39872000 Datenbanken Prof. Dr. Stephan Kleuker 287 ResultSet: positionieren und ändern • Statement createStatement(int resultSetType, int resultSetConcurrency) Parameter (ResultSet-Konstanten) ermöglichen beliebiges Positionieren (Scrolling) und Ändern der Datensätze • TYPE_FORWARD_ONLY: sequentieller Durchlauf • TYPE_SCROLL_INSENSITIVE: positionierbar, Änderungen an Datenbank werden nicht bemerkt • TYPE_SCROLL_SENSITIVE: positionierbar, Änderungen an Datenbank werden bemerkt • • CONCUR_READ_ONLY: nur lesen • CONCUR_UPDATABLE: Änderungen möglich Datenbanken Prof. Dr. Stephan Kleuker 288 ResultSet: positionieren (1/3) • void beforeFirst() Positioniert vor den ersten Satz • boolean first() Positioniert auf den ersten Satz • boolean last() Positioniert auf den letzten Satz • void afterLast() Positioniert hinter den letzten Satz Datenbanken Prof. Dr. Stephan Kleuker 289 ResultSet: positionieren (2/3) • boolean absolute(int pos) Positioniert ausgehend vom Anfang (pos positiv) oder vom Ende (pos negativ) • boolean relative(int rows) Positioniert relativ zum aktuellen Satz vorwärts (rows positiv) oder rückwärts (rows negativ) • boolean next() Positioniert auf den nächsten Satz • boolean previous() Positioniert auf den vorigen Satz Datenbanken Prof. Dr. Stephan Kleuker 290 ResultSet: positionieren (3/3) • int getRow() Liefert aktuelle Satznummer • boolean isBeforeFirst() Liefert true, falls vor dem ersten Satz • boolean isFirst() Liefert true, falls auf dem ersten Satz • boolean isLast() Liefert true, falls auf dem letzten Satz • boolean isAfterLast() Liefert true, falls hinter dem letzten Satz Datenbanken Prof. Dr. Stephan Kleuker 291 ResultSet: Datensatz ändern • Methoden updateXXX() ändern Werte in aktueller Zeile. rs.absolute(5); rs.updateString("Name", "Heinz"); rs.updateInt(2, 42); rs.updateNull(3); rs.updateRow(); • void updateRow() Schreibt geänderte Zeile in die Datenbank (*) • void cancelRowUpdates() Macht Änderungen rückgängig – nur vor updateRow() • void deleteRow() Löscht aktuelle Zeile aus ResultSet (*) Ob Bearbeitung des ResultSet sich direkt auf die Datenbank auswirkt, hängt von Autocommit-Einstellung ab [später] Datenbanken Prof. Dr. Stephan Kleuker 292 ResultSet: Datensatz einfügen • Einfügezeile: im ResultSet, nicht in der Datenbank rs.moveToInsertRow(); rs.updateString("Name", "Callaghan"); rs.updateInt(2, 42); rs.insertRow(); rs.moveToCurrentRow(); • void moveToInsertRow() Positioniert auf die Einfügezeile • void insertRow() Schreibt Einfügezeile in ResultSet und Datenbank (abhängig von Autocommit-Einstellung) • void moveToCurrentRow() Positioniert von der Einfügezeile auf die aktuelle Zeile im ResultSet Datenbanken Prof. Dr. Stephan Kleuker 293 Daten ändern • ResultSet executeQuery(String) führt SELECT- Anweisung aus • int executeUpdate(String) führt INSERT-, UPDATE- oder DELETE-Anweisung aus – Liefert Anzahl betroffener Zeilen (Update count) – Auch für sonstige Befehle (CREATE ...) geeignet • boolean execute(String) führt beliebige SQLAnweisung aus – Liefert true, falls Ergebnis ein ResultSet ist, dann mit getResultSet() Ergebnis abrufbar – Liefert false, falls Ergebnis ein Update count ist Datenbanken Prof. Dr. Stephan Kleuker 294 PreparedStatement (1/2) • PreparedStatement Objekt enthält vorübersetzte SQLBefehle • geeignet, wenn Statement mehr als einmal ausgeführt werden muss • PreparedStatement kann Variablen enthalten, die jedesmal bei Ausführung definiert werden • Ansatz: Erzeuge PreparedStatement, identifiziere Variablen mit ? PreparedStatement pstmt = con.prepareStatement ("UPDATE Kunde " + "SET Status = ? where Umsatz > ?"); Datenbanken Prof. Dr. Stephan Kleuker 295 PreparedStatement (2/2) 1. Variablen-Werte übergeben pstmt.setXXX(index,value); 2. Statement ausführen pstmt.executeQuery(); pstmt.executeUpdate(); int goldenerKunde=42000; PreparedStatement pstmt = con.prepareStatement ("UPDATE Kunde " + "SET Status = ? where Umsatz > ?"); pstmt.setString(1, "Gold"); pstmt.setInt(2, goldenerKunde); pstmt.executeUpdate(); Datenbanken Prof. Dr. Stephan Kleuker 296 COMMIT • Für Connection con ist automatisch ein „Auto-COMMIT“ eingestellt, alle Aktionen direkt auf der Datenbank • Mit con.setAutoCommit(boolean) einstellbar • Starteinstellung über con.getAutoCommit() ermitteln (bei Oracle default: true !) • con.commit() zum erfolgreichen Abschließen • con.rollback() zum Verwerfen der Änderungen seit dem letzten Commit Datenbanken Prof. Dr. Stephan Kleuker 297 Verbindungsaufbau (Ausschnitt) import oracle.jdbc.*; // besser ausschreiben import java.sql.*; ... try { // Oracle JDBC-Treiber laden DriverManager. registerDriver(new oracle.jdbc.driver.OracleDriver()); Connection con = DriverManager. getConnection("jdbc:oracle:thin:@oracle-srv.edvsz" +".hs-osnabrueck.de:1521:Ora11", "kleuker", "passwort"); // Oracle DatabaseMetaData fuer Meta-Informationen DatabaseMetaData meta = con.getMetaData(); // Beispielmetainformation: Treiberversion System.out.println("JDBC driver version is "+ meta.getDriverVersion()+"\n"); befehl=con.createStatement(); //private Statement befehl } catch (SQLException e) {... } Datenbanken Prof. Dr. Stephan Kleuker 298 Einfache Verbindungsnutzung (Ausschnitt) ResultSet result; ResultSetMetaData metaresult; try { result=befehl.executeQuery("SELECT * FROM City"); metaresult=result.getMetaData(); int spalten = metaresult.getColumnCount(); while(result.next()){ for(int i=1; i<=spalten;i++) ausgabe.append(result.getString(i)+" "); ausgabe.append("\n"); } ausgabe.append("Bearbeitung abgeschlossen\n"); result.close(); } catch (SQLException es) { while (es!=null){ //evtl. mehr als eine Exception fehler.append("Fehlercode: "+es.getErrorCode()+"\n"); fehler.append("SQL State: "+es.getSQLState()+"\n"); fehler.append(es+"\n"); es= es.getNextException(); } } Datenbanken Prof. Dr. Stephan Kleuker 299 Einbindung von Oracle-JDBC in Eclipse Rechtsklick auf Projekt, dann Properies wählen, „Java Build Path“, dann Reiter Libraries, dann „Add External JARs...“ Quellen: Oracle Web-Seiten C:\sqldeveloper\jdbc\lib\ojdbc6.jar Dann passenden Treiber suchen, z. B. ojdbc6.jar in sqldeveloper\jdbc\lib Datenbanken Prof. Dr. Stephan Kleuker 300 Einbindung von Oracle-JDBC in NetBeans • Rechtsklick im Projekt auf Libraries -> Add JAR/Folder • manövrieren zum Verzeichnis mit JDBC-Treiber (sinnvoll Unterordner lib des Projekts) • markieren und „Öffnen“ Datenbanken Prof. Dr. Stephan Kleuker 301 Erinnerung: PL/SQL-Funktion CREATE OR REPLACE FUNCTION anzahlNasen(namePar Angestellte.Name%TYPE) RETURN INTEGER /* oder NUMBER */ IS ergebnis INTEGER; BEGIN IF namePar = 'Meier' THEN RAISE_APPLICATION_ERROR(-20300, 'Meiers nicht zählbar'); END IF; SELECT COUNT(*) INTO ergebnis FROM Angestellte WHERE Angestellte.Name=namePar; RETURN ergebnis; END; Datenbanken Prof. Dr. Stephan Kleuker 302 PL/SQL in Java • Grundsätzlich besteht bei der Ausführung große Ähnlichkeit zu PreparedStatements • benötigt wird Connection-Objekt con CallableStatement stmt=con.prepareCall( "{? = call anzahlNasen(?)}"); • man beachte geschweifte Klammern, bei Prozeduraufruf wird „ ? = “ am Anfang weggelassen • in Oracle auch möglich: CallableStatement stmt=con.prepareCall( "BEGIN ? := anzahlNasen(?); END;"); • Ausgabeparameter müssen registriert werden stmt.registerOutParameter(1,Types.INTEGER); Datenbanken Prof. Dr. Stephan Kleuker 303 Beispiel public int nasenZaehlen(String nachname){ int ergebnis = 0; try { CallableStatement stmt=con.prepareCall( "{? = call anzahlNasen(?)}"); stmt.registerOutParameter(1, Types.INTEGER); stmt.setString(2, nachname); stmt.execute(); ergebnis=stmt.getInt(1); } catch (SQLException e) { if(e.getErrorCode() == 20300) // von RAISE_APPLICATION_ERROR ergebnis = -1; else{ ausnahmeAusgeben(e); ergebnis = Integer.MIN_VALUE; } } return ergebnis; } Datenbanken Prof. Dr. Stephan Kleuker 304 Nutzung von Cursor als Ergebnis (1/3) import import import import import import java.sql.CallableStatement; java.sql.Connection; java.sql.DriverManager; java.sql.ResultSet; java.util.Scanner; oracle.jdbc.OracleTypes; /* in Oracle fuer das MondialBeispiel CREATE OR REPLACE PROCEDURE STAEDTEIN( Land IN VARCHAR, stadtcursor IN OUT SYS_REFCURSOR) AS BEGIN OPEN stadtcursor for SELECT * FROM City WHERE City.Country=Land; END STAEDTEIN; */ /* Hinweis: Cursor nur lesbar, nicht schreibbar! */ Datenbanken Prof. Dr. Stephan Kleuker 305 Nutzung von Cursor als Ergebnis (2/3) public class PLSQLCursorNutzungInJava { // public static void main(String[] s){ String nutzer = "kleuker"; String passwort = "kleuker"; try { DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver()); //Verbindung aufbauen in der HS zur DB Connection conn = DriverManager.getConnection ("jdbc:oracle:thin:@oracle-srv.edvsz.hs-osna" + "brueck.de:1521:Ora11", nutzer, passwort); //Vorbereitung der PL/SQL-Nutzung ResultSet rset = null; CallableStatement cstmt = conn. prepareCall("{call staedtein(?,?)}"); cstmt.registerOutParameter(2, OracleTypes.CURSOR); Datenbanken Prof. Dr. Stephan Kleuker 306 Nutzung von Cursor als Ergebnis (3/3) //Testprogramm String eingabe = ""; while(!eingabe.equalsIgnoreCase("Ende")){ System.out.print( "Laenderkuerzel (Ende mit \"Ende\"): "); eingabe=(new Scanner(System.in)).next(); cstmt.setString(1,eingabe); cstmt.execute(); rset = (ResultSet) cstmt.getObject(2); while(rset.next()) System.out.println("Stadt: " +rset.getString("name") +" Einwohner:" +rset.getInt("population")); } conn.close(); } } } catch(Exception e){ // ausarbeiten, wie immer } Datenbanken Prof. Dr. Stephan Kleuker 307 Beispieldialog Laenderkuerzel (Ende mit "Ende"): GR Stadt: Piraeus Einwohner:196389 Stadt: Patrai Einwohner:142163 Stadt: Larisa Einwohner:102426 Stadt: Iraklion Einwohner:102398 Stadt: Volos Einwohner:71378 Stadt: Kavalla Einwohner:56705 Stadt: Athens Einwohner:885737 Stadt: Thessaloniki Einwohner:406413 Laenderkuerzel (Ende mit "Ende"): LAR Stadt: Tripoli Einwohner:0 Stadt: Bengasi Einwohner:0 Laenderkuerzel (Ende mit "Ende"): AND Stadt: Andorra la Vella Einwohner:15600 Laenderkuerzel (Ende mit "Ende"): enDE Datenbanken Prof. Dr. Stephan Kleuker 308 Einschub: Testen mit JUnit • Framework, um den Unit-Test eines Java-Programms zu automatisieren • einfacher Aufbau • leicht erlernbar • geht auf SUnit (Smalltalk) zurück • mittlerweile für viele Sprachen verfügbar (NUnit, CPPUnit) S. Kleuker, Qualitätssicherung durch Softwaretests, Springer Vieweg, Wiesbaden, 2013 Software-Qualität Prof. Dr. Stephan Kleuker 309 Testfall • Vor dem Testen müssen Testfälle spezifiziert werden • Vorbedingungen – Zu testende Software in klar definierte Ausgangslage bringen (z. B. Objekte mit zu testenden Methoden erzeugen) – Angeschlossene Systeme in definierten Zustand bringen – Weitere Rahmenbedingungen sichern (z. B. HW) • Ausführung – Was muss wann gemacht werden (einfachster Fall: Methodenaufruf) • Nachbedingungen – Welche Ergebnisse sollen vorliegen (einfachster Fall: Rückgabewerte) – Zustände anderer Objekte / angeschlossener Systeme Software-Qualität Prof. Dr. Stephan Kleuker 310 Aufbau einer Testklasse (1/8) import import import import import import junit.framework.Assert; org.junit.After; org.junit.AfterClass; org.junit.Before; org.junit.BeforeClass; org.junit.Test; public class AnschauungTest { private int wert; private static int klasse; Komponentenbasierte SoftwareEntwicklung Prof. Dr. Stephan Kleuker Beliebiger Klassenname, Endung „Test“ üblich Nutzung von normalen Exemplarvariablen Klassenvariablen, außer als Konstanten, unüblich 311 Aufbau einer Testklasse (2/8) Verhaltensbeschreibung mit Annotationen Methode wird einmal vor vor allen Tests ausgeführt, z.B. Aufbau DB-Verbindung @BeforeClass public static void setUpBeforeClass() throws Exception { System.out.println("setUpBeforeClass"); klasse = 99; } einmal nach allen Tests (aufräumen) @AfterClass public static void tearDownAfterClass() throws Exception { System.out.println("tearDownAfterClass"); } Komponentenbasierte SoftwareEntwicklung Prof. Dr. Stephan Kleuker 312 Aufbau einer Testklasse (3/8) Methode wird vor jedem Test ausgeführt, Idee: einheitliche Ausgangssituation schaffen @Before public void setUp() throws Exception { System.out.println("setUp"); wert = 42; klasse = klasse + 1; System.out.println("klasse ist "+klasse); } einmal nach jeden Tests (lokal aufräumen) @After public void tearDown() throws Exception { System.out.println("tearDown"); } Komponentenbasierte SoftwareEntwicklung Prof. Dr. Stephan Kleuker 313 Aufbau einer Testklasse (4/8) Test ist beliebige mit @Test annotierte Methode Methodenname ist beliebig, beginnt typischerweise mit „test“ und beinhaltet Name der @Test Methode oder Sinn des Tests public void test1() { System.out.println("test1"); Experimente wert = wert + 1; Assert.assertTrue("Erwartet 43 gefunden: "+wert , wert == 43); } Prüfmethode, Parameter Text und Boolesche Bedingung; ist Bedingung „false“ wird Test als gescheitert festgehalten und Text ausgegeben Komponentenbasierte SoftwareEntwicklung Prof. Dr. Stephan Kleuker 314 Aufbau einer Testklasse (5/8) @Test public void test2() { System.out.println("test2"); wert = wert + 2; Assert.assertTrue(wert == 44); } Kurzform ohne Text (gibt assert-Varianten) wenn Testfall scheitert ist entweder das Programm oder der Test fehlerhaft @Test public void test3() { System.out.println("test3"); wert = wert + 3; Assert.assertTrue("Erwartet 44 gefunden: "+wert , wert == 44); } Komponentenbasierte SoftwareEntwicklung Prof. Dr. Stephan Kleuker 315 Aufbau einer Testklasse (6/8) @Test Test scheitert, wenn ausgeführt; public void test4() { markiert Stellen, die nicht System.out.println("test4"); erreicht werden sollen try{ if(42/0 == 0){ gewünschte Exception } Assert.fail(); } catch(ArithmeticException e){ ungewünschte Exception } catch(Exception e){ Assert.fail(); (kann weggelassen } werden) } @Test public void test5() { System.out.println("test5"); throw new IllegalArgumentException(); } } Komponentenbasierte SoftwareEntwicklung Prof. Dr. Stephan Kleuker 316 Aufbau einer Testklasse (7/8) setUpBeforeClass setUp klasse ist 100 test4 tearDown setUp klasse ist 101 test5 tearDown setUp klasse ist 102 test1 tearDown setUp klasse ist 103 test2 tearDown setUp klasse ist 104 test3 tearDown tearDownAfterClass Komponentenbasierte SoftwareEntwicklung Prof. Dr. Stephan Kleuker 317 Aufbau einer Testklasse (8/8) • Failure: Fehler durch Assert • Error: Fehler durch Abbruch Komponentenbasierte SoftwareEntwicklung Prof. Dr. Stephan Kleuker 318 Erinnerung: Ich muss mein Gebot erhoehen CREATE OR REPLACE TRIGGER GEBOTERHOEHEN BEFORE INSERT OR UPDATE ON GEBOT FOR EACH ROW CREATE TABLE Gebot( DECLARE mnr INTEGER, PRAGMA AUTONOMOUS_TRANSACTION; ware INTEGER, maxi Gebot.gebot%TYPE; gebot NUMBER(8, 2), BEGIN PRIMARY KEY(mnr,ware,gebot) SELECT MAX (Gebot.gebot) ); INTO maxi FROM Gebot WHERE Gebot.mnr = :NEW.mnr AND Gebot.ware = :NEW.ware; IF maxi IS NOT NULL AND maxi >= :NEW.Gebot THEN RAISE_APPLICATION_ERROR(-20900 ,'Gebot muss erhoeht werden'); END IF; END; / Datenbanken Prof. Dr. Stephan Kleuker 319 Basisideen zur Testerstellung • viele kleine Tests, da nachfolgende Asserts nicht geprüft, wenn eines vorher abbricht • erwartetes Verhalten kann zusammen geprüft werden • jede mögliche Ausnahme in getrenntem Testfall • Extremwerte prüfen • Testklassen können weitere Hilfsmethoden enthalten • typisch: am Anfang auf „leerem Feld“ neue Testausgangssituation (@SetUp) erstellen Datenbanken Prof. Dr. Stephan Kleuker 320 Test von Datenbanken • Nie, nie mit laufender Geschäftsdatenbank testen; es wird immer ein Testsystem benötigt • generell direkt mit JUnit machbar • Detailproblem: nach den Tests so aufräumen, dass Ausgangssituation wieder hergestellt (abhängige Daten !) • Detailproblem: aufwändiger Vergleich zwischen aktuellem und erwartetem Tabelleninhalt Vereinfachung mit DBUnit als Ergänzung von JUnit • einfaches Leeren und Neueinspielen von Datensätzen • einfacher Vergleich von Mengen von Tabelleneinträgen • Tabellen z. B. auf Basis von XML-Dateien definierbar • (hier nur zentrale Konzepte) http://www.dbunit.org/ Datenbanken Prof. Dr. Stephan Kleuker 321 Projektaufbau Datenbanken Prof. Dr. Stephan Kleuker 322 Konfiguration des Loggers (log4j.properties) log4j.rootLogger=WARN, console log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.conversionPattern=%5p [%t] (%F:%L) - %m%n • ermöglicht flexibles Schreiben von Meldungen in LogDateien • sehr einfach ein- und ausschaltbar (hier Level WARN) Datenbanken Prof. Dr. Stephan Kleuker 323 Verbindung public class Verbindung { private static String dbAdresse = "127.0.0.1"; private static String dbInstanz = "XE"; private static String nutzer = "ich"; private static String passwort = "ich"; public static Connection verbinden() throws Exception { DriverManager.registerDriver( new oracle.jdbc.driver.OracleDriver()); return DriverManager.getConnection("jdbc:oracle:thin:@" + dbAdresse + ":1521:" + dbInstanz, nutzer, passwort); } public static void verbindungTrennen(Connection con , IDatabaseConnection conDBU) throws Exception { if (con != null) { con.close(); } if (conDBU != null) { conDBU.close(); } Datenbanken Prof. Dr. 324 } Stephan Kleuker } public class GebotErhoehenTest { private static Connection con = null; // direkte DB-Verbindung private static IDatabaseConnection conDBU; // DBUnit-Verbindung @BeforeClass public static void setUpBeforeClass() throws Exception { con = Verbindung.verbinden(); conDBU = new DatabaseConnection(con, null, true); DatabaseConfig config = conDBU.getConfig(); config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new OracleDataTypeFactory()); } DBUnit hat DB- individuelle Einstellungen @AfterClass public static void tearDownAfterClass() throws Exception { Verbindung.verbindungTrennen(con, conDBU); } Datenbanken Prof. Dr. Stephan Kleuker 325 basisdaten.xml • Möglichkeit zur Spezifikation von Testdaten mit XML <?xml version="1.0" encoding="UTF-8"?> <dataset> <Gebot mnr="1" ware="100" gebot="1.00" <Gebot mnr="1" ware="101" gebot="1.00" <Gebot mnr="1" ware="100" gebot="2.00" <Gebot mnr="2" ware="100" gebot="2.01" <Gebot mnr="3" ware="101" gebot="1.01" </dataset> /> /> /> /> /> • Wird Spalte nicht angegeben, dann NULL-Wert • Daten werden in angegebener Reihenfolge eingefügt Datenbanken Prof. Dr. Stephan Kleuker 326 Varianten beim Einspielen von Daten @Before public void setUp() throws Exception { IDataSet dataSet = new FlatXmlDataSetBuilder() .build(new FileInputStream(".\\testdaten\\basisdaten.xml")); DatabaseOperation.CLEAN_INSERT.execute(conDBU, dataSet); } • • • • • CLEAN_INSERT: alle Daten zuerst löschen, dann einfügen DELETE_ALL: löscht alle Daten in den Tabellen DELETE : löscht die übergebenen Daten INSERT: fügt die übergebenen Daten in die Tabellen ein UPDATE: aktualisiert die vorhandenen Daten mit den übergebenen Daten • REFRESH: aktualisiert vorhandene Daten, fügt nicht vorhandene Daten hinzu Datenbanken Prof. Dr. Stephan Kleuker 327 Test: erlaubtes Insert @Test public void testGebotAufNeueWareOk() { try { int anzahl = con.createStatement().executeUpdate( "INSERT INTO Gebot VALUES (4, 102, 3.00)"); Assert.assertTrue("statt 1, " + anzahl + "Datensaetze geaendert", anzahl == 1); } catch (SQLException e) { Assert.fail("erlaubtes INSERT gescheitert: " + "VALUES (4, 102, 3.00)"); } } // Hinweis: Test mit erlaubten UPDATE, DELETE sinnvoll • Etwas kritisch: es wurde eine Zeile eingefügt; wird nicht überprüft, ob sie so im Ergebnis steht Datenbanken Prof. Dr. Stephan Kleuker 328 Erlaubtes Insert, präzise Prüfung (1/2) • einfachesInsert.xml <?xml version="1.0" encoding="UTF-8"?> <dataset> <Gebot mnr="1" ware="100" gebot="1.00" <Gebot mnr="1" ware="101" gebot="1.00" <Gebot mnr="1" ware="100" gebot="2.00" <Gebot mnr="4" ware="102" gebot="3.00" <Gebot mnr="2" ware="100" gebot="2.01" <Gebot mnr="3" ware="101" gebot="1.01" </dataset> /> /> /> /> /> /> • Erinnerung: SQL speichert ohne Reihenfolge, was beim Vergleich zu beachten ist • DBUnit ermöglicht lexikographische Sortierung Datenbanken Prof. Dr. Stephan Kleuker 329 Erlaubtes Insert, präzise Prüfung (2/2) @Test public void testGebotAufNeueWareVarianteOk() throws Exception { con.createStatement().executeUpdate( "INSERT INTO Gebot VALUES (4,102,3.00)"); IDataSet databaseDataSet = conDBU.createDataSet(); ITable actualTable = databaseDataSet.getTable("Gebot"); IDataSet expectedDataSet = new FlatXmlDataSetBuilder() .build(new File(".\\testdaten\\einfachesInsert.xml")); ITable expectedTable = expectedDataSet.getTable("Gebot"); Assertion.assertEquals(new SortedTable(expectedTable) , new SortedTable(actualTable)); } Datenbanken Prof. Dr. Stephan Kleuker 330 Test, ob Primary Key noch existiert @Test public void testPrimaryKeyVerstoss(){ try { con.createStatement().executeUpdate( "INSERT INTO Gebot VALUES (2,100,2.01)"); Assert.fail("verbotenes INSERT durchgefuehrt: " + "VALUES (2,100,2.01)"); } catch (SQLException e) { } } Datenbanken Prof. Dr. Stephan Kleuker 331 Test des Triggers (1/2) @Test public void testHoeheresGebotOk() { try { int anzahl = con.createStatement().executeUpdate( "INSERT INTO Gebot VALUES (3, 101, 1.02)"); Assert.assertTrue("statt 1, " + anzahl + "Datensaetze geaendert", anzahl == 1); } catch (SQLException e) { Assert.fail("erlaubtes INSERT gescheitert: " + "VALUES (3, 101, 1.02)"); } } Datenbanken Prof. Dr. Stephan Kleuker 332 Test des Triggers (2/2) @Test public void testGleichesGebotVerboten(){ try { con.createStatement().executeUpdate( "INSERT INTO Gebot VALUES (3, 101, 1.00)"); Assert.fail("verbotenes INSERT durchgefuehrt: " + "VALUES (3, 101, 1.00)"); } catch (SQLException e) { Assert.assertTrue("Fehler 20900 erwartet, gefunden" + e.getErrorCode(), e.getErrorCode() == 20900); } } // fehlen Tests für UPDATE Test auf passsenden Fehlercode } Datenbanken Prof. Dr. Stephan Kleuker 333