Databases

Leerdoelen

Na het bestuderen van dit hoofdstuk wordt van je verwacht dat je:

  • met behulp van Java, JSP, CSS, een database en eventueel JavaScript een web applicatie met een database kunt ontwikkelen

Introductie

Bij dit onderdeel ga je een web-applicatie maken voor een sportvereniging waarmee hun ledenadministratie op een webpagina wordt beheerd. Je maakt gebruik van Java, JSP, CSS, een database en eventueel JavaScript.

Voorbeeld uitwerking

Demo app sportvereniging (under construction). Kies een tab voor overzicht en beheer van leden en teams. Probeer gerust uit!

Ontwikkelomgeving installeren

Zet je ontwikkel- en produktieomgeving op in App Engine. App Engine heeft een gratis (NoSQL) datastore die we gebruiken om de database te implementeren.

Voor deze opdracht gebruik je:

Opzetten van het project

Maak in je App Engine console een nieuwe applicatie en geef het een ID. Start in Eclipse een nieuw app engine project en koppel het aan je gekozen ID.

Om je project overzichtelijk te houden en "spaghetti code" te vermijden, gaan we het opzetten volgens het veelgebruikte Model-View-Controller ontwerp patroon.

mvc patroon

Maak in de map "src" 3 packages, volgens onderstaand voorbeeld.

Model laag

De "model" laag bestaat uit klassen die een afspiegeling zijn van iets uit de werkelijkheid. In ons project hebben we te maken met een vereniging met spelers en teams.

Maak de klasse "Lid", geef hem een aantal attributen zoals roepnaam, tussenvoegsels en achternaam. Ook hebben we een unieke sleutel nodig om het lid straks gemakkelijk te kunnen identificeren. Hiervoor zou je bijvoorbeeld een e-mail adres kunnen gebruiken, aangezien deze altijd uniek is. Tot slot maken we een standaard constructor, get en set methoden (getters en setters) en een methode die de gehele naam van het lid teruggeeft.

public class Lid {
    private String spelerscode, roepnaam, 
        tussenvoegsels, achternaam, email;
    
    //lege constructor
    public Lid() {
        this.spelerscode = "";
        this.roepnaam = "";
        this.tussenvoegsels = "";
        this.achternaam = "";
        this.email = "";
    }

    /**
     * constructor maakt lid-object
     * @param roepnaam
     * @param tussenvoegsels
     * @param achternaam
     * @param email
     */
    public Lid (String roepnaam,
                String tussenvoegsels, 
                String achternaam,
                String email) {
        
        this.roepnaam = roepnaam;
        this.tussenvoegsels = tussenvoegsels;
        this.achternaam = achternaam;
        this.email = email;
        this.spelerscode = email;  
    }
    
    /********* GETTERS EN SETTERS ************/
    
    public String getSpelerscode() {
        return spelerscode;
    }
    
    public void setSpelerscode(String spelerscode) {
        this.spelerscode = spelerscode;
    }
    
    public String getRoepnaam() {
        return roepnaam;
    }

    public void setRoepnaam(String roepnaam) {
        this.roepnaam = roepnaam;
    }

    public String getTussenvoegsels() {
        return tussenvoegsels;
    }

    public void setTussenvoegsels(String tussenvoegsels) {
        this.tussenvoegsels = tussenvoegsels;
    }

    public String getAchternaam() {
        return achternaam;
    }

    public void setAchternaam(String achternaam) {
        this.achternaam = achternaam;
    }
    
    /**
     *@return samengevoegde naam
     *
     */
    public String getNaam() {
        String naam;
        if (tussenvoegsels.equals("")) {
            naam = roepnaam + " " + achternaam;
        } else {
            naam = roepnaam + " " + tussenvoegsels + " " + achternaam;
        }
        return naam;
    }
}

Het eerste wat onze app moet kunnen is nieuwe leden aanmaken, ze opslaan in de database en ze daaruit weer ophalen. Maak een jsp pagina (bijvoorbeeld index.jsp) met een formulier en de benodigde invoervelden met gegevens waarmee een nieuw lid kan worden aangemaakt.

(roepnaam)
(tussenvoegsels)
(achternaam)
(e-mail)

Servlets

Als je een html formulier naar de server stuurt moet die ergens worden opgevangen en verwerkt. Hiervoor gebruiken we een Servlet.

Maak in je package "control" een java-klasse bijvoorbeeld genaamd "SportServlet". Zorg dat je klasse een uitbreiding van HttpServlet wordt volgens onderstaand voorbeeld.

public class SportServlet extends HttpServlet {
}
	

Klik op de foutmeldingen die Eclipse geeft en importeer de benodigde klassen uit de "javax.servlet" packages

In de servlet gaan we twee methoden uit de superklasse "overriden". Klik in Eclipse op het menu-item "Source" en kies de optie "Override/Implement methods..." Zorg dat de methoden doGet en doPost worden geïmplementeerd in je klasse. Zie voorbeeld.

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        //deze methode gaat alle requests afhandelen    
    }
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        //stuur door naar doGet
        doGet(req, resp);
    }
    

Om het jezelf gemakkelijk te maken hoef je maar één van de twee methoden uit te werken en de ander naar deze methode door te sturen. Je hoeft je dan geen zorgen meer te maken over wanneer get en post worden gebruikt.

Om requests naar een servlet te sturen heeft de servlet een adres (url) nodig. Je kunt de url van een servlet "mappen" in het web-xml bestand dat zich in de map WEB-INF van je war directory bevindt.

<servlet>
    <servlet-name>Sport</servlet-name>
    <servlet-class>control.SportServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Sport</servlet-name>
    <url-pattern>/sport</url-pattern>
</servlet-mapping>

Nu kun je het html formulier naar de servlet sturen als gebruiker op de verzend knop klikt.

<form action="/sport" method="get">
    <input type="text" name="roepnaam" placeholder="roepnaam">(roepnaam)<br>
    <input type="text" name="tussenvoegsels" placeholder="tussenvoegsels">(tussenvoegsels)<br>
    <input type="text" name="achternaam" placeholder="achternaam">(achternaam)<br>
    <input type="text" name="email" placeholder="e-mail">(e-mail)<br>
    <input type="submit" name="verzend_nieuw_lid_knop" value="verstuur">
</form>

In je servlet gebruik je de meegestuurde parameters om een object van de klasse Lid aan te maken. Eerst check je of er daadwerkelijk op de knop is geklikt, vervolgens vraag je de parameters op en maak je een lid aan.

public void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
    if (req.getParameter("verzend_nieuw_lid_knop") != null) {
        String roepnaam = req.getParameter("roepnaam");
        String tussenvoegsels = req.getParameter("tussenvoegsels");
        String achternaam = req.getParameter("achternaam");
        String email = req.getParameter("email");
        Lid lid = new Lid(roepnaam, tussenvoegsels, achternaam, email);
    }
}

Datastore

Het lid-object willen we opslaan in de database. In je package sportIO maak je een klasse die verantwoordelijk wordt voor alle database transacties. In dit geval gebruiken we de datastore van App Engine. Om toegang tot de datastore te krijgen heb je een object van DatastoreService nodig. Objecten worden in datastore opgeslagen als Entities. Als je een entity aanmaakt geef je de naam van de tabel (met hoofdletter) waarin hij wordt opgeslagen en een unieke sleutel waarmee je hem kunt identificeren. In ons geval gebruiken we daarvoor het attribuut spelerscode. Nadat je de entity hebt aangemaakt kun je hem properties meegeven. Dit zijn de attributen van je lid-object die bewaard moeten worden.

public class SportIO {
    private DatastoreService datastore;
	
    //constructor
    public SportIO() {
        datastore = DatastoreServiceFactory.getDatastoreService();
    }
    
    //methode om lid toe te voegen
    public void voegLidToe(Lid lid) {
        //Maak entity mbv tabelnaam en unieke sleutel
        Entity ent = new Entity("Lid", lid.getSpelerscode());
        ent.setProperty("roepnaam", lid.getRoepnaam());
        ent.setProperty("tussenvoegsels", lid.getTussenvoegsels());
        ent.setProperty("achternaam", lid.getAchternaam());
        ent.setProperty("email", lid.getEmail());
        ent.setProperty("spelerscode", lid.getSpelerscode());
        datastore.put(ent);
    }
}

In je servlet maak je een object van de SportIO klasse en gebruikt de methode die we net hebben gemaakt om het lid op te slaan. Nadat het lid is toegevoegd wordt gebruiker naar de index.jsp pagina terug geleid (zie code). Later gaan we dit veranderen.

/*  SportServlet.java */
public void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
    SportIO io = new SportIO();
    if (req.getParameter("verzend_nieuw_lid_knop") != null) {
        String roepnaam = req.getParameter("roepnaam");
        String tussenvoegsels = req.getParameter("tussenvoegsels");
        String achternaam = req.getParameter("achternaam");
        String email = req.getParameter("email");
        Lid lid = new Lid(roepnaam, tussenvoegsels, achternaam, email);
        io.voegLidToe(lid);
        resp.sendRedirect("/index.jsp");
    }
}

Test

Als het goed is kun je nu met behulp van het html formulier leden aan je datastore toevoegen. Om te testen of het werkt moet je de app deployen en kun je een aantal leden toevoegen. In de app engine console kun je controleren of er Entities zijn toegevoegd in de Datastore Viewer.

Alle entities ophalen en vertonen

De volgende stap is om je opgeslagen leden uit datastore op te vragen en op een jsp pagina te vertonen. Hiervoor moet je in SportIO een nieuwe methode maken die middels een query alle entities uit de tabel Lid ophaalt, hun properties opvraagt (let op de typecasting!), ze omzet naar java objecten en ze in een lijst - bijvoorbeeld een ArrayList - stopt die de methode teruggeeft.

/*  SportIO.java */
public ArrayList<Lid> getAlleLeden() {
    ArrayList<Lid> leden = new ArrayList<Lid>();
    Query q = new Query("Lid");
    PreparedQuery resultaten = datastore.prepare(q);
    for (Entity ent: resultaten.asIterable()) {
        Lid lid = new Lid();
        lid.setRoepnaam( (String) ent.getProperty("roepnaam") );
        lid.setTussenvoegsels( (String) ent.getProperty("tussenvoegsels") );
        lid.setAchternaam( (String) ent.getProperty("achternaam") );
        lid.setEmail( (String) ent.getProperty("email") );
        lid.setSpelerscode( (String) ent.getProperty("spelerscode") );
        leden.add(lid);
    }
    return leden;
}

In je servlet kun je de methode aanroepen op het io object.

/*  SportServlet.java */
public void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
    SportIO io = new SportIO();
    if (req.getParameter("verzend_nieuw_lid_knop") != null) {
        String roepnaam = req.getParameter("roepnaam");
        String tussenvoegsels = req.getParameter("tussenvoegsels");
        String achternaam = req.getParameter("achternaam");
        String email = req.getParameter("email");
        Lid lid = new Lid(roepnaam, tussenvoegsels, achternaam, email);
        io.voegLidToe(lid);
        resp.sendRedirect("/sport");
    }
    else {
    	ArrayList<Lid> leden = io.getAlleLeden();
    	req.setAttribute("leden", leden);
    	RequestDispatcher disp = req.getRequestDispatcher("/overzicht_leden.jsp");
        disp.forward(req, resp);
    }
}

Uitleg

Nadat de servlet het lid aan de datastore heeft toegevoegd, leidt hij de gebruiker terug naar zichzelf, maar dit keer worden er geen parameters meegestuurd zodat het if statement wordt overgeslagen en hij dus in het else statement komt. Hier wordt de ArrayList met leden opgevraagd en als attribuut aan het req object gekoppeld met de methode setAttribute(). De servlet stuurt vervolgens een jsp pagina met het attribuut "leden" terug naar de browser van de gebruiker met behulp van een RequestDispatcher object. In de jsp pagina kun je het attribuut nu gebruiken (zie code). Let op dat je de nodige java klassen in de pagina importeert.

<!-- overzicht_leden.jsp -->
<%@ page import="jspcursus.sport.vereniging.*" %>
<%@ page import="java.util.ArrayList" %>

<%
if (request.getAttribute("leden") == null) {
    response.sendRedirect("/sport")
} else { 
     ArrayList<Lid> leden = (ArrayList<Lid>) request.getAttribute("leden");
%>

<h1>Ledenoverzicht</h1>
<%  for (Lid lid: leden) { %>
        <%= lid.getNaam() %><br>
<%  }
}   %>

Met bovenstaande code zorg je er voor dat gebruiker altijd via de servlet op de pagina komt en er dus altijd een attribuut leden is dat nodig is om de lijst met leden op het scherm te zetten.

Test

Als het goed is kan je app nu de ingevoerde leden tonen.

Een enkele entity ophalen

Om een enkele entity uit de datastore te halen kun je z'n key gebruiken, in ons geval de spelerscode. Eclipse zal aangeven dat je een EntityNotFoundException moet afvangen (try/catch block) of doorgeven (throws declaratie) aan de klasse die de methode aanroept. In onderstaande uitwerking is er voor gekozen om de Exception af te vangen. Mocht de entity, om wat voor reden dan ook, niet (meer) beschikbaar zijn dan geeft de methode null terug.

/*  SportIO.java */
public Lid getLid(String spelerscode)  {
    Lid lid = null;
    Key k = KeyFactory.createKey("Lid", spelerscode);
    try {
        Entity ent = datastore.get(k);
        lid = new Lid();
        lid.setRoepnaam( (String) ent.getProperty("roepnaam") );
        lid.setTussenvoegsels( (String) ent.getProperty("tussenvoegsels") );
        lid.setAchternaam( (String) ent.getProperty("achternaam") );
        lid.setEmail( (String) ent.getProperty("email") );
        lid.setSpelerscode( (String) ent.getProperty("spelerscode") );
    } catch (EntityNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return lid;
}

De methode kan gebruikt worden om de gegevens van een enkel lid op het scherm te tonen. Verander je jsp pagina zodat achter ieder lid een link komt die naar onze servlet leidt. Om een enkel lid op te vagen hebben we z'n spelerscode nodig dus die sturen we mee als parameter. Geef ook een parameter mee zodat in de servlet kan worden bepaald wat de bedoeling is.

<!-- overzicht_leden.jsp -->

<% ArrayList<Lid> leden = (ArrayList<Lid>) request.getAttribute("leden");
    for (Lid lid: leden) { %>
        <%= lid.getNaam() %>
        <a href="/sport?haal_lid=&spelerscode=<%= lid.getSpelerscode() %>">naar lid</a><br>
<%  }
}   %>

In je servlet vang je het request op, haalt het lid uit de database en stuur hem door naar een nieuwe jsp pagina (die we nog moeten maken).

/*  SportServlet.java */
public void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
    SportIO io = new SportIO();
    if (req.getParameter("verzend_nieuw_lid_knop") != null) {
        ...
    }
    else if (req.getParameter("haal_lid") != null) {
        String spelerscode = req.getParameter("spelerscode");
        Lid lid = io.getLid(spelerscode);
        req.setAttribute("lid", lid);
    	RequestDispatcher disp = req.getRequestDispatcher("/lid.jsp");
        disp.forward(req, resp);
    }
    else {
    	...
    }
}

In bovenstaande code wordt een pagina genaamd lid.jsp naar gebruiker gestuurd. Op deze pagina worden de gegevens van het meegestuurde lid getoond. Maak deze pagina. Later gaan we deze pagina gebruiken om gegevens van een lid te kunnen wijzigen of het lid te verwijderen, dus maken we een formulier met inputvelden waarin we de gegevens van het lid vertonen en twee knoppen met toepasselijke namen. Om deze pagina te kunnen maken heeft hij het attribuut "lid" nodig. Als dit attribuut ontbreekt (== null) kun je de gebruiker terug sturen naar het ledenoverzicht.

<!--  lid.jsp  -->
<% 
if (request.getAttribute("lid") == null) {
    response.sendRedirect("/sport");
} else {  
     Lid lid = (Lid) request.getAttribute("lid"); 
%> 
    
<form action="/sport"  method="get">
    <input type="text" 
       name="roepnaam" 
       value="<%= lid.getRoepnaam() %>">(roepnaam)<br>
    <input type="text" 
       name="tussenvoegsels" 
       value="<%= lid.getTussenvoegsels() %>">(tussenvoegsels)<br>
    <input type="text" 
       name="achternaam" 
       value="<%= lid.getAchternaam() %>">(achternaam)<br>
    <input type="text" 
       name="email" 
       value="<%= lid.getEmail() %>">(email)<br>
    <input type="hidden" 
       name="spelerscode" 
       value="<%= lid.getSpelerscode() %>">
    <input type="submit" 
       name="wijzig_lid_knop" 
       value="wijzig">
    <input type="submit" 
       name="verwijder_lid_knop" 
       value="verwijder">
</form>
< } %>

Test

Je kunt nu de gegevens van een lid in een apart venster tonen.

Een lid verwijderen en wijzigen

In datastore kun je een lid als volgt verwijderen en wijzigen.

/*  SportIO.java */
public void verwijderLid(String spelerscode) {
    Key k = KeyFactory.createKey("Lid", spelerscode );
    datastore.delete(k);
}

//wijzigen == toevoegen
public void wijzigLid(Lid lid) {
    this.voegLidToe(lid);
}

Maak deze methodes aan in je SportIO klasse. Gebruik de methodes in je servlet zodat een lid wordt gewijzigd of verwijderd als gebruiker op één van de knoppen klikt die we aan het formulier hebben toegevoegd. Nadat een lid is gewijzigd of verwijderd kun je de bezoeker terug sturen naar het ledenoverzicht.

Test

Zorg dat je gegevens van een speler kunt wijzigen en een lid kunt verwijderen.

Gegevens types

Tot nu toe hebben we alleen strings opgeslagen, maar je kunt uiteraard ook andere types opslaan. Zie overzicht.

De klasse Date

Met de klasse Date kun je in Java een datum/tijdstip bewaren. Datum en tijd zijn vrij ingewikkeld omdat er nogal wat verschillende notaties zijn en we op aarde verschillende tijdzones kennen. De klasse Date heeft 2 constructors:

// huidige tijd in milliseconden vanaf 1-1-1970
Date date1 = new Date();
long ms = date1.getTime();

// milliseconden op 20 oktober 1964 00:00 uur gerekend vanaf 1-1-1970
long ms = -164073600000;
Date date2 = new Date(ms);

Als je bovengenoemd date2 object op het scherm wilt wilt vertonen, met andere woorden als je de methode toString op een date object aanroept, krijg je het volgende te zien:

Tue Oct 20 00:00:00 UTC 1964

Dat is meestal niet wat je wilt. Ook valt het niet mee om het aantal milliseconden vanaf 1 januari 1970 van een datum te berekenen als je een specifieke datum wilt maken (zoals een geboortedatum van een speler van onze vereniging).

SimpleDateFormat

Om van een date object een String te maken en omgekeerd kun je de klasse SimpleDateFormat gebruiken. Bij het maken van een object kun je aangeven welk patroon de datum string heeft/moet hebben. Zie voorbeeld. Je kunt verschillende string-patronen gebruiken. Zie documentatie.

SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy");
// Maak datum string met bovenstaand patroon
String geboortedatum = "20-10-1964"
// maak date object mbv een string
Date date = sdf.parse(geboortedatum);
// maak van date object een string
String geboortedatum = sdf.format(date);

Uiteraard kun je er voor kiezen om een string in de database op te slaan en deze te converteren naar een Date object mocht dat nodig zijn. Je moet er dan wel voor zorgen dat de String het juiste patroon heeft. Voordeel van het opslaan van het type date is dat je er op kunt sorteren en selecteren. In onze sport app is het voorstelbaar dat je alleen leden in een junioren team kunt plaatsen die jonger zijn dan een bepaalde leeftijd.

Test

Geef je leden een attribuut "geboortedatum" van het type Date. en zorg dat het als Date wordt opgeslagen in datastore. Ook kun je een attribuut "spelersnummer" maken en opslaan. Let op dat integers in datastore als long worden opgeslagen!

Resultaten filteren

Om alleen entities met een bepaalde property te selecteren kun je (een) filter(s) maken en aan je query meegeven. Onze app moet een overzicht kunnen geven van spelers van een bepaald team en teams van een bepaalde speler. Om dit mogelijk te maken maak je in datastore een tabel "Teamspeler".

public void setTeamspeler(Team team, Lid lid) {
    Entity e = new Entity("Teamspeler", team.getTeamcode() + lid.getSpelerscode());
    e.setProperty("teamcode", team.getTeamcode());
    e.setProperty("spelerscode", lid.getSpelerscode());
    datastore.put(e);
}

Nu kun je met behulp van een filter alle spelers van een bepaald team opvragen. Lees meer over datastore queries en filters.

public ArrayList<Lid> getTeamspelers(Team team)  {
    ArrayList<Lid> teamleden = new ArrayList<Lid>();
    Filter teamcodeFilter =  new FilterPredicate(
                       "teamcode", //naam van property in datastore 
                       FilterOperator.EQUAL, //gelijk aan
                       team.getTeamcode()); //attribuut van team
                       
    Query q = new Query("Teamspeler").setFilter(teamcodeFilter);
    PreparedQuery pq = datastore.prepare(q);
		
    for (Entity result: pq.asIterable()) {
        Lid lid = null;
        try {
            String spelerscode = (String) result.getProperty("spelerscode")
            lid = this.getLid(spelerscode);
            teamleden.add(lid);
            
        } catch (EntityNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }		
    return teamleden;
}

Test

Maak in je app een overzicht van teams, met dezelfde functionaliteit als je overzicht van spelers. Zorg er voor dat je spelers aan een team kan toevoegen en verwijderen en per team een overzicht van spelers kunt opvragen. Ook moet je per speler een overzicht kunnen maken van teams waar de speler in zit.

Resultaten sorteren

Met de volgende code wordt het overzicht van spelers oplopend gesorteerd op roepnaam en aflopend gesorteerd op geboortedatum. Let op dat het even kan duren voor datastore deze properties geïndexeerd heeft en de resultaten correct gesorteerd terug geeft.

public ArrayList<Lid> getLedenLijst() {
    ArrayList<Lid> leden = new ArrayList<Lid>();
    Query q = new Query("Lid")
        .addSort("achternaam", SortDirection.ASCENDING)
        .addSort("geboortedatum", SortDirection.DESCENDING);
    PreparedQuery pq = datastore.prepare(q);
    //zie eerdere code
    ...
}		

Test

Sorteer de resultaten van je ledenlijst op verschillende properties zoals naam, geboortedatum etc.