Info-books
HO40e
Toegepaste Informatica Objectgeoriënteerd Programmeren en Webapplicaties
Deel 40e:
Java voor het internet: Persistentie en Threads K. Behiels - J. Gils
Hoofdstuk 2
2.1
Andere vormen van Persistentie
XML
2.1.1 Hoe werk je met XML? XML (eXtensible Markup Language) is een universele taal om gegevens te beschrijven. Om XML-documenten te lezen zijn er vele libraries beschikbaar. Om zoveel mogelijk uitwisselbaar te zijn moet XML flexibel zijn. Daardoor worden enkele problemen van serialization opgelost. In XML kunnen de opgeslagen gegevens extern aangepast worden en kan in bepaalde gevallen zelfs de structuur gewijzigd worden. Door gebruik te maken van tools zoals Castor (http://castor.exolab.org) kan je op een eenvoudige manier Java-objecten naar XML omzetten en omgekeerd. In het geval van Castor dien je twee libraries in de vorm van jar-bestanden voor je programma beschikbaar te maken (castor-0.9.5.3.jar en xerces.jar). De Castor API (zie www.castor.org/api) bevat onder andere de package org.exolab.castor.xml. De belangrijkste klassen hierin zijn de Marshaller en Unmarshaller klassen. Met een Marshaller zet je een Java-object om naar een XML-document. Dit gaat het eenvoudigst met een FileWriter. Om een XML-document terug om te zetten naar een Java-object gebruik je een Unmarshaller in combinatie met een FileReader: FileWriter writer = new FileWriter(filename); Marshaller.marshal(object, writer); FileReader reader = new FileReader(filename); object = (class) Unmarshaller.unmarshal(class.class, reader);
Belangrijk is dat, om een object succesvol te unmarshallen, er in de klasse naast een default constructor voor elk attribuut een getter en een setter moet aanwezig zijn! Opmerkingen: Het op een eenvoudige manier lezen en schrijven komt door Castor, niet door XML. Met XML kan je, in tegenstelling tot met serialization, gegevens van een oude klasse in een nieuwe versie van die klasse inlezen. Als je een attribuut toevoegt, verlies je de oude gegevens niet. Bij het terug inlezen zullen de niet aanwezige attributen genegeerd worden. Voor het overige geldt hetzelfde als bij serialiazation, met name de noodzaak om alle objecten terug in te lezen. (Je kan de objecten en hun attributen niet afzonderlijk benaderen).
K. Behiels – J. Gils
Persistentie en Threads
13
2.1.2 Een voorbeeld We hernemen het voorbeeld uit het vorige hoofdstuk en passen het aan voor het werken met Castor. In dit voorbeeld schrijf je één object (een object dat een ArrayList bevat) weg in de vorm van een XML-bestand, en lees je het terug in. Eerst de klasse Address: package be.kdg.xmldemo; public class Address { private String street; private String zipCode; private String city; public Address() { } public Address(String street, String zipCode, String city) { this.street = street; this.zipCode = zipCode; this.city = city; } public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } public String getZipCode() { return zipCode; } public void setZipCode(String zipCode) { this.zipCode = zipCode; } public String getCity() { return city; } public void setCity(String city) { this.city = city; }
public String toString() { return "Address{" + "street='" + street + "'" + ", zipCode='" + zipCode + "'" + ", city='" + city + "'" + "}"; } }
14
Persistentie en Threads
K. Behiels – J. Gils
De klasse Address gebruik je in de klasse Person: package be.kdg.xmldemo; public class Person { private int id; private String name; private Address address; public Person() { } public Person(int id, String name, Address address) { this.id = id; this.name = name; this.address = address; } public Person(int id, String name, String street, String zipCode, String city) { this.id = id; this.name = name; address = new Address(street, zipCode, city); } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } public boolean equals(Object object) { if (this == object) return true; if (!(object instanceof Person)) return false; final Person person = (Person) object; return id != person.id; } public int hashCode() { return id; }
K. Behiels – J. Gils
Persistentie en Threads
15
public String toString() { return "Person{" + "id=" + id + ", name='" + name + "'" + ", address=" + address + "}"; } }
Een People-object bestaat uit een verzameling Person-objecten: package be.kdg.xmldemo; import java.util.ArrayList; import java.util.List; public class People { private List list; public People() { list = new ArrayList(); } public People(List list) { this.list = list; } public List getList() { return list; } public void setList(List list) { this.list = list; } public void add(Person person) { list.add(person); } public boolean remove(Person person) { return list.remove(person); } public void printPeople() { for (int i = 0; i < list.size(); i++) { Person person = (Person) list.get(i); System.out.println(person); } } }
Voor het lezen en schrijven van de objecten gebruik je een afzonderlijke klasse XmlTool: package be.kdg.xmldemo; import org.exolab.castor.xml.Marshaller; import org.exolab.castor.xml.Unmarshaller; import org.exolab.castor.xml.ValidationException;
16
Persistentie en Threads
K. Behiels – J. Gils
import import import import
java.io.FileNotFoundException; java.io.FileReader; java.io.FileWriter; java.io.IOException;
public class XmlTool { private People people; public XmlTool(People people) { this.people = people; } public void createXml() { FileWriter writer = null; try { writer = new FileWriter("people.xml"); Marshaller.marshal(people, writer); } catch (ValidationException e) { e.printStackTrace(); } catch (org.exolab.castor.xml.MarshalException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } } public People retrieveXml() { people = null; // To be sure it works! FileReader reader = null; try { reader = new FileReader("people.xml"); people = (People) Unmarshaller.unmarshal( People.class, reader); } catch (ValidationException e) { e.printStackTrace(); } catch (org.exolab.castor.xml.MarshalException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return people; } }
K. Behiels – J. Gils
Persistentie en Threads
17
Ten slotte het hoofdprogramma: package be.kdg.xmldemo; public class RunXmlDemo { public static void main(String[] args) { People people = new People(); people.add(new Person(100, "Baert, Peter", "Hoogstraat 16", "9000", "Gent")); people.add(new Person(102, "Coveliers, Anka", "Heirbrug 23", "9160", "Lokeren")); people.add(new Person(103, "De Maeght, Maria", "Plezantstraat 7", "9100", "Sint Niklaas")); XmlTool tool = new XmlTool(people); tool.createXml(); people = tool.retrieveXml(); people.printPeople(); } }
2.2
JDBC
2.2.1 Inleiding De technologie die het mogelijk maakt om vanuit Java met databases te werken is Java Database Connectivity (JDBC). JDBC is voor een database wat een Java-interface is voor een Java-klasse. Met behulp van JDBC kan je vanuit Java vrijwel altijd op dezelfde manier werken, onafhankelijk van de database (Access, MySQL, Oracle, …). Een Java-interface belooft de aanwezigheid van
zijn methoden in de klassen waarin hij geïmplementeerd wordt. Op gelijkaardige wijze belooft de JDBC-specificatie functionaliteiten van de databases en de drivers die aan de standaard voldoen. Vroeger bestond JDBC als een afzonderlijke library, ondertussen is ze volledig in de Java standaard editie geïntegreerd. Er is ook een JDBC-driver voorzien, namelijk een die een brug vormt naar om het even welke geïnstalleerde ODBC-driver. Voor de andere drivers ben je aangewezen op de fabrikant van de database. Er zijn 4 types van JDBC-drivers:
18
•
Type 1: JDBC-ODBC bridge Koppelt een aanwezige ODBC-driver aan de JDBC API.
•
Type 2: Native API partly Java driver Koppelt een aanwezige database aan de JDBC API.
•
Type 3: Net protocol full Java driver Zet JDBC-oproepen om naar een database-onafhankelijk netprotocol. De server zet dit dan om naar het databaseprotocol. Persistentie en Threads
K. Behiels – J. Gils
•
Type 4: Native protocol Java driver Een pure Java-driver die direct met de database kan communiceren (niet mogelijk met databases zoals Access).
De JDBC API bestaat uit twee delen: • •
de JDBC core API die deel uitmaakt van J2SE. Je vindt ze in de java.sqlpackage; de JDBC optional API die zich in de javax.sql-package bevindt.
Een volledige studie van JDBC is een cursus op zichzelf. We beperken ons hier tot het strikte minimum. Opmerking: Gegevens opslaan in een database is een zuivere vorm van persistentie. Via JDBC kan je de gegevens tot op attribuutniveau benaderen. 2.2.2 De java.sql-package De belangrijkste onderdelen van de java.sql-package zijn: • • • • • • • •
de DriverManage-klasse; de Driver-interface; de Connection-interface; de Statement-interface; de ResultSet-interface; de PreparedStatement-interface; de ResultSetMetaData-interface.
DriverManager Elke JDBC-driver die je wenst te gebruiken moet geregistreerd zijn bij de DriverManager. Deze drivers verschillen naargelang het type database dat je gebruikt. Om de JDBC-driver vanuit een servlet of een JSP-pagina te laden kopieer je het driverbestand (meestal een *.jar bestand) naar de WEB-INF\lib directory van je toepassing. Om de driver te laden voer je de volgende code uit: try { Class.forName("JDBC.driver"); } catch(ClassNotFoundException e) { // driver niet gevonden }
Hierin is JDBC.driver de volledige naam van de JDBC-driver klasse. Voor de open source 100% Java database hsqldb is dit bijvoorbeeld "org.hsqldb.jdbcDriver". Als je
K. Behiels – J. Gils
Persistentie en Threads
19
met Access werkt is dit "sun.jdbc.odbc.JdbcOdbcDriver", de standaard JDBCODBC-driver van Sun Microsystems. De belangrijkste methode van deze klasse is de getConnection-methode, die bestaat in drie verschillende versies (telkens public static): Connection getconnection(String url); Connection getconnection(String url, Properties info); Connection getconnection(String url, String user, String password);
•
Driver Deze interface is geïmplementeerd door elke JDBC-driver-klasse. De driver-klasse zelf is geladen en geregistreerd bij de DriverManager. Er kan meer dan één driver tegelijkertijd geregistreerd zijn. De DriverManager vraagt elke driver om te connecteren met de database op de bijbehorende URL.
•
Connection De connection-interface stelt een verbinding met een database voor. Een instantie hiervan wordt bekomen met de getConnection-methode van de DriverManager-klasse. Hij bevat de volgende methoden die allemaal een throw van een SQLException kunnen doen: public void close();
Met deze methode sluit je de verbinding onmiddellijk af in plaats van te wachten tot ze automatisch wordt afgesloten. public boolean isClosed();
Test of de verbinding al of niet is afgesloten. public Statement createStatement(); public PreparedStatement prepareStatement(String sql); Met de createStatement-methode maak je een Statement-object om SQLuitdrukkingen naar een database te zenden. Wanneer een SQL-uitdrukking meer dan eenmaal wordt uitgevoerd is het beter een PreparedStatement te gebruiken. public boolean getAutoCommit(); public void setAutoCommit(boolean autoCommit); Met de eerste methode kan je de toestand van autocommit opvragen, met de tweede kan je ze wijzigen. Standaard staat autocommit op true. Dit wil zeggen dat de JDBC-driver de transactie automatisch zal beëindigen (starten gaat altijd automatisch). Als je autocommit op false zet moet je zelf de transactie eindigen met commit of rollback. public void commit(); public void rollback(); Met de commit-methode maak je alle wijzigingen in de database (sinds de laatste commit of rollback) permanent. Met de rollback-methode breng je de database
terug in de toestand na de laatste succesvolle commit.
20
Persistentie en Threads
K. Behiels – J. Gils
•
Statement Je gebruikt een statement-methode om een SQL-uitdrukking uit te voeren en de geproduceerde resultaten te bekomen. De twee belangrijkste methoden zijn de volgende (ze kunnen ook een SQLException veroorzaken): public ResultSet executeQuery(String sql); public int executeUpdate(String sql); De executeQuery-methode voert een SQL-uitdrukking uit en geeft een ResultSetobject terug. Met een executeUpdate kan je insert-, update- of delete <SQL-uitdrukking> uitvoeren. De return-waarde is het aantal records dat door de
transactie gewijzigd is. •
ResultSet Deze interface bevat het resultaat van een query in de vorm van een soort tabel. Hij bevat een soort pointer waarmee je naar de verschillende gegevensrijen kunt verwijzen. De drie belangrijkste methoden (die ook allemaal een SQLException kunnen veroorzaken) zijn: public boolean isFirst(); public boolean isLast();
Deze methoden geven aan of de pointer respectievelijk naar de eerste of naar de laatste record in de ResultSet verwijst. public boolean next();
Deze methode verplaatst de pointer naar de volgende record. Ze geeft true terug zolang er nog records zijn. public ResultMetaData getMetaData(); Deze methode geeft de meta data van de ResultSet. public XXX getXXX(int kolomIndex); De getXXX-methoden, waarbij XXX voor een datatype staat, gebruik je om de waarde van een kolom in een bepaalde rij van de ResultSet op te vragen.
Kolomindex 1 geeft de eerste kolom enzovoort. •
PreparedStatement Deze interface is een uitbreiding van de Statement-interface en wordt gebruikt om een SQL-uitdrukking vooraf te compileren zodat ze bij meervoudig gebruik efficiënter uitgevoerd wordt.
•
ResultSetMetaData Deze interface stelt de meta data van een ResultSet voor. Ze bevat onder andere de volgende methoden (ook hier is een SQLException mogelijk): public int getColumnCount();
Geeft het aantal kolommen terug. public String getColumnName(int kolomIndex);
Geeft de naam van de kolom terug, de indexwaarde van de eerste kolom is 1.
K. Behiels – J. Gils
Persistentie en Threads
21
2.2.3 Toegang krijgen tot een database Om toegang te krijgen tot een tabel in een database moet je 4 stappen doorlopen: •
Stap 1: Laad de JDBC-database-driver Voor bijna alle populaire databases zijn er tegenwoordig JDBC-drivers op de markt, bijvoorbeeld voor Oracle, Sybase, DB2, SQL Server, … Er zijn ook heel wat open source databases zoals MySQL, hsqldb, Apache Derby, … Ook voor ODBC zijn er drivers beschikbaar zodat je, indien nodig, via een JDBC-ODBC bridge kan connecteren. Een goede bron om drivers te zoeken is de volgende URL: http://industry.java.sun.com/products/jdbc/drivers Omdat JDBC-drivers meestal in de vorm van een *.jar-bestand geleverd worden is het eerste wat je moet doen ervoor zorgen dat de driver gevonden kan worden. Kopieer daarom de driver (het *.jar bestand) in de lib-directory van je toepassing (WEB-INF\lib voor een webapplicatie). Wanneer je met een JDBC-ODBC-bridge werkt is dit niet noodzakelijk omdat die in je Java-omgeving geïntegreerd is. Wanneer je van plan bent om met verschillende databases te werken moet je alle benodigde drivers laden. Als je bijvoorbeeld vanuit een programma met Access en MySQL-bestanden wilt werken moet je in je programma de volgende regels opnemen: // voor Access Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); // voor MySQL (www.mysql.com) Class.forName("com.mysql.jdbc.driver");
Voor MySQL (www.mysql.org) moet je programma de beschikking hebben een aangepaste driver in de vorm van een jar-bestand. Je vindt die op de url http://www.mysql.com/products/connector-j Een interessante database om je programma’s te testen is hsqldb (zie http://hsqldb.org/). Het volstaat om het bestand hsqldb.jar als library in je programma op te nemen om een compleet databasesysteem ter beschikking te hebben. Het laden van de driver hiervoor gaat via: // voor hsqldb Class.forName("org.hsqldb.jdbcDriver");
•
Stap 2: Maak een verbinding (Connection) Nadat de JDBC-driver bij de DriverManager geregistreerd is kan je een verbinding met de database maken. Het eerste argument bij de verschillende versies van de getConnection-methodes is altijd de URL. Deze URL-string heeft de volgende syntaxis: "jdbc:subprotocol:subname"
22
Persistentie en Threads
K. Behiels – J. Gils
De subprotocol- en de subname-delen zijn afhankelijk van de database die je gebruikt. Als je met Access werkt is het subprotocol odbc en de subname de DSN (Data Source Name) voor de database (normaal de naam van het databasebestand). Als je Access-bestand de naam JavaWeb.mdb heeft dan wordt de URL-string als volgt: "jdbc:odbc:JavaWeb"
Bij MySQL is het subprotocol mysql en de subnaam de naam van de machine en de database, de URL-string voor een database met de naam JavaWeb wordt dan: "jdbc:mysql://JavaWeb"
en als de naam van de server 'linux' is wordt het: "jdbc:mysql///linux/JavaWeb"
Voor hsqldb is het subprotocol hsqldb en de subnaam een verwijzing naar de database, je kan hier met een relatief pad werken. De volledige uitdrukkingen zijn dan respectievelijk bijvoorbeeld: Connection connection = DriverManager.getConnection("jdbc:odbc:JavaWeb"); Connection connection = DriverManager.getConnection("jdbc:mysql://JavaWeb"); Connection connection = DriverManager.getConnection("jdbc:mysql://linux/JavaWeb", "admin", "secret"); Connection connection = DriverManager.getConnection("jdbc:hsqldb:dbData/JavaWeb", "as", "");
In de laatste gevallen worden ook gebruikersnaam en wachtwoord meegegeven. Bij hsqldb is de standaard user “as” en het standaard password leeg. •
Stap 3: Maak een SQL-uitdrukking (Statement) Nadat je een Connection-object ter beschikking gekregen hebt kan je een SQLuitdrukking maken. Welke SQL-onderdelen begrepen worden is afhankelijk van je database server. Verder is de uitvoering van een SQL-uitdrukking afhankelijk van het toegankelijkheidsniveau, bijvoorbeeld of je al of niet de toelating hebt om records te wijzigen. Je creëert eerst een Statement-object (uitgaande van een open verbinding met de naam connection). Daarna kan je een executeQuery- of een executeUpdate-methode uitvoeren. Bijvoorbeeld met een tabel met de naam Gebruikers:
K. Behiels – J. Gils
Persistentie en Threads
23
Statement statement = connection.createStatement(); String sqlString = "INSERT INTO Gebruikers " + "VALUES ('Don', 'Johnson', 'Miami', 'Vice')"; statement.executeUpdate(sqlString); ResultSet result = statement.executeQuery("SELECT * FROM Gebruikers");
•
Stap 4: Verwerk het resultaat (ResultSet) Als je gebruik maakte van een executeQuery-statement dan staat het resultaat daarvan in een ResultSet-object. Om de gegevens hieruit op te halen kan je gebruik maken van de getXXX-methoden waarvan de getString-methode de meest gebruikte is. Deze methode bestaat in twee versies: public String getString(int kolomIndex) ; public String getString(String kolomNaam);
De verschillende records doorlopen kan met de volgende lus: while(result.next()) { // . . . }
2.2.4 Een praktisch MVC voorbeeld Voor de het programmavoorbeeld maken we gebruik van de volgende eenvoudige tabel Gebruikers. De tabel wordt (indien hij nog niet bestaat) in het programma zelf aangemaakt: Gebruikers Sleutel Voornaam Familienaam Gebruikersnaam Wachtwoord
•
1 Jos
Gils
gils
swordfish
2 Erik
Goossens
goose
matrix
3 Theo
Jansens
jans
sleeples
4 Luc
Lambrecht
lam
stigmata
5 Ronny
Verlinden
ronny
silence
6 Kris
Van Gompel
gompy
outbreak
7 Els
D'Heer
d'heer
o'cool
8 Tom
Van den Eede tom
nightowl
Model Voor het model maak je gebruik van de klassen Gebruiker en Gebruikers.
24
Persistentie en Threads
K. Behiels – J. Gils
Besluit Naast serialisatie zijn er nog andere manieren om Java-objecten op te slaan. De opslag in een XML-bestand heeft grote voordelen op gebied van overdraagbaarheid. Communicatie met databases gaat in Java onder andere met behulp van JDBC.
Wat je moet kennen en kunnen: • •
objecten in XML-bestanden opslaan gebruik makende van de Castor tool; een verbinding met een database maken en er gegeven uit lezen.
Opdracht De klasse EuroLand bevat de volgende velden: private private private private
String naam; int toetredingsJaar; String muntEenheid; String extraInfo;
De klasse EuroLanden bevat een verzameling van EuroLand-objecten. De klasse EuroLandenData (bijgevoegd) bevat de eigenlijke gegevens die je in EuroLandApplicatie (ook bijgevoegd) eerst in een EuroLanden-object moet invoeren Vervolgens moet je het EuroLanden-object met behulp van Castor naar een XML-bestand wegschrijven en dat bestand daarna terug inlezen in datzelfde EuroLanden-object. Zorg er wel voor dat het object voorafgaand aan het teug inlezen eerst op null gezet wordt, anders weet je niet zeker of het omzetten naar en van XML wel goed werkt. Bij het opstarten van het programma (na omzetten naar XML en terug inlezen) moet je de volgende lay-out bekomen:
Uiteraard moet bij het selecteren van een ander land de bijbehorende informatie in de overeenstemmende groene velden wijzigen.
36
Persistentie en Threads
K. Behiels – J. Gils
Verder vind je de lay-out bij het uitvergroten (met GridBagLayout gewerkt) en ook het hoofdprogramma. Let op! Voor deze opgave heb je drie libraries in de vorm van een jar-bestand nodig, met name castor-1.1.x.x-xml.jar, commons-logging-1.1.jar (commons.apache.org/logging) en xerces.jar. De laatste vind je in de lib directory van IntelliJ, de eerste kan je downloaden bij http://www.castor.org. public class EuroLandApplicatie { public static void main(String[] args) { List data = new EuroLandenData().getLanden(); EuroLanden landen = new EuroLanden(data); XmlTool tool = new XmlTool(landen); tool.createXml(); landen = tool.retrieveXml(); new EuroLandFrame("EuroLanden", landen); } }
K. Behiels – J. Gils
Persistentie en Threads
37