Skip to content

3. Inleiding tot API JDBC

3.1. Het opzetten van de werkomgeving

We gaan werken met een MySQL5-database.

U moet beschikken over:

  • een JDK (Java Development Kit) geïnstalleerd (paragraaf 23.1);
  • de afhankelijkheidsbeheerder Maven geïnstalleerd (paragraaf 23.2);
  • de IDE Spring Tool Suite (STS) geïnstalleerd (paragraaf 23.3);
  • de SGBD MySQL5 (paragraaf 23.4) en de bijbehorende client EMS MyManager (paragraaf 23.5) geïnstalleerd;
  • de codes van het document [http://tahe.developpez.com/java/spring-database] gedownload;

In het vervolg gaan we ervan uit dat de beheerder van MySQL5 de root-gebruiker is met het root-wachtwoord. Start SGBD, MySQL5 en de bijbehorende client [MyManager]. Met behulp van [MyManager] maken we de database [dbproduits] [1-34] aan:

  • in [3] moet de database [dbproduits] heten;
  • in [8-9], root met het wachtwoord root (wat niet te zien is op de bovenstaande schermafbeelding);
  • in [14a], het wachtwoord is opnieuw root (wat niet te zien is op de schermafbeelding);
  • in [15] is de database [dbproduits] aangemaakt;
  • bij [20]: let op de geselecteerde database. Dit moet de database [dbproduits] zijn;
  • in [22]; de map is <voorbeelden>/spring-database-config/mysql/databases, waarbij <voorbeelden> de map is met de gedownloade voorbeelden;
  • in [23] selecteert u het script SQL [dbproduits.sql]. Dit genereert de tabel [PRODUITS] in de database [dbproduits];
  • in [30] is de tabel [produits] aangemaakt;
  • in [33], de kolommen van de tabel [produits];
  • in [34], deze is aanvankelijk leeg;

Importeer nu met STS de volgende projecten (volg de procedure die is gebruikt voor de projecten in de map <exemples>/spring-core):

  • in [2]; het project [mysql-config-jdbc] is te vinden in de map [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];

Dit project configureert de laag JDBC van de onderstaande architectuur:

Importeer vervolgens de volgende drie projecten opnieuw:

  • in [2]; de projecten zijn te vinden in de map [<exemples>/spring-database-config/spring-jdbc] [1];

Deze drie projecten zijn Maven-projecten die gebruikmaken van het Maven-project [mysql-config-jdbc]. Dit laatste project genereert het volgende Maven-artefact (zie pom.xml):


    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>

Hetzelfde artefact wordt gegenereerd door het project [oracle-config-jdbc, db2-config-jdbc, ...]. Om er zeker van te zijn dat de projecten [spring-generic-jdbc-*] die momenteel in STS zijn geladen, daadwerkelijk gebruikmaken van het project [mysql-config-jdbc]:

  • zorg ervoor dat er niet tegelijkertijd een ander project [sgbd-config-jdbc] is geladen. Dit zou tot moeilijk te begrijpen fouten kunnen leiden;
  • werk de Maven-configuratie van de geladen projecten als volgt bij:

Om uw configuratie te controleren, voert u de uitvoerconfiguratie [spring-jdbc-generic-01.IntroJdbc01] [1-3] uit:

U zou de volgende console-uitvoer moeten krijgen:

------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.00000000000001,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.10000000000001,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.2,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.30000000000001,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé

In de volgende voorbeelden kan de lezer:

  • ofwel direct met de eerder geladen projecten werken;
  • of de projecten zelf samenstellen;

3.2. De stappen voor het gebruik van een database

In de bovenstaande architectuur omvat het gebruik van een database door het consoleprogramma de volgende stappen:

  1. het laden van de database-driver JDBC;
  1. het openen van een verbinding met de database;
  2. het uitvoeren van een opdracht SQL op de database en het verwerken van de resultaten van de opdracht SQL;
  3. het sluiten van de verbinding;

Stap 1 wordt slechts één keer uitgevoerd. De stappen 2-4 worden herhaaldelijk uitgevoerd. Merk op dat er geen verbinding open blijft staan. Deze wordt gesloten zodra deze niet meer nodig is.

3.2.1. stap 1 - het laden van de driver JDBC in het geheugen

De code


        // stuurprogramma laden JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // de uitzondering afhandelen
}

De bewerking op regel 3 heeft tot doel de driver JDBC uit de database in het geheugen te laden. Deze bewerking hoeft slechts één keer te worden uitgevoerd. Herhaling leidt echter niet tot een fout. Er wordt gezocht naar de klasse van de driver JDBC in het classpath van het project. Het is dus noodzakelijk dat in het Eclipse-project het bestand [jar], dat de klasse van de driver JDBC bevat, is opgenomen in het classpath van het project.

3.2.2. stap 2 – een verbinding tot stand brengen

Zodra de driver JDBC is geïnstalleerd, wordt deze gevraagd een verbinding te openen met de BD:

De code


package spring.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class IntroJdbc01 {

...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // verbinding openen
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            ...
        } finally {
         // verbinding sluiten
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // de uitzondering afhandelen
                ...
            }
         }
}
  • regels 3-7: de implementatieklassen van de interface JDBC bevinden zich allemaal in het pakket [java.sql]. Bovendien genereren ze bij een fout allemaal een uitzondering van het type [SQLException] (regel 19, 27). Deze uitzondering is afgeleid van de klasse [Exception] en is een zogenaamde gecontroleerde uitzondering: men is verplicht een try/catch-blok te gebruiken om deze af te vangen, of als alternatief deze niet af te vangen en aan te geven dat de methode de uitzondering doorlaat door de signatuur van de methode aan te vullen met [throws SQLException];
  • regel 17, [DriverManager.getConnection] is een statische methode die drie parameters verwacht:
    • [url]: de URL uit de database. Dit is een tekenreeks die afhankelijk is van de gebruikte BD. Voor MySQL heeft deze de vorm [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: de eigenaar van de verbinding;
    • [passwd]: zijn wachtwoord;
  • regels 24-30: de verbinding moet in de clausule [finally] worden gesloten, zodat deze wordt gesloten, ongeacht of er een uitzondering optreedt of niet.

3.2.3. stap 3 - het verzenden van opdrachten SQL en [SELECT]

Zodra er een verbinding tot stand is gebracht, kunnen SQL-opdrachten worden verzonden. De manier waarop leesopdrachten [SELECT] worden afgehandeld, verschilt van die voor bijwerkingsbewerkingen [UPDATE, INSERT, DELETE]. We beginnen met de opdrachten SQL en [SELECT]:

De code


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // verbinding openen
            connexion = DriverManager.getConnection(url, user, passwd);
            // transactie starten
            connexion.setAutoCommit(false);
            // in alleen-lezenmodus
            connexion.setReadOnly(true);
            // de tabel [PRODUITS] wordt gelezen
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // transactie vastleggen
            connexion.commit();
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
             doCatchException(connexion,e1);
        } finally {
            // de finally-blok wordt verwerkt
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • regels 8, 10: het openen van een transactie (regel 8) in de alleen-lezenmodus (regel 10). Een transactie is een reeks opdrachten SQL die ofwel allemaal slagen, ofwel allemaal mislukken. Dus in een transactie met N opdrachten SQL: als opdracht I+1 mislukt, worden de voorgaande I opdrachten geannuleerd. Voor een leesbewerking is een transactie niet nodig. Het aanmaken van een alleen-lezen-transactie kan echter bepaalde SGBD in staat stellen om bepaalde optimalisaties door te voeren;
  • regel 12: gebruik van een [PreparedStatement]. Een [PreparedStatement] heeft normaal gesproken parameters die worden aangeduid met het teken ?. Hier heeft het die niet. Een [PreparedStatement] is een opdracht die wordt voorbereid door de SGBD. Deze voorbereiding brengt kosten met zich mee en wordt slechts één keer uitgevoerd. Vervolgens wordt deze voorbereide opdracht uitgevoerd door de SGBD met verschillende effectieve parameters die de formele parameters ? zullen vervangen. Merk op dat het beter is om de gewenste kolommen bij naam te noemen in plaats van de notatie * te gebruiken om alle kolommen op te halen. Door de namen van de kolommen te specificeren, kunnen we vervolgens hun waarden ophalen op basis van hun positie in de query SELECT;
  • regel 13: uitvoering van [PreparedStatement]. We halen een object van het type [ResultSet] op;

Een object van het type [ResultSet] vertegenwoordigt een tabel, dat wil zeggen een verzameling rijen en kolommen. Op een bepaald moment heeft men slechts toegang tot één rij van de tabel, de zogenaamde huidige rij. Bij de eerste aanmaak van [ResultSet] is er geen huidige rij. Er moet een bewerking [ResultSet.next()] worden uitgevoerd om deze te verkrijgen. De signatuur van de methode next is als volgt:

    boolean next()

Deze methode probeert door te gaan naar de volgende regel van de [ResultSet] en retourneert true als dit lukt, anders false. Als de bewerking slaagt, wordt de volgende regel de nieuwe huidige regel. De vorige regel gaat verloren en kan niet meer worden teruggehaald.

De tabel van [ResultSet] heeft kolommen met de namen labelCol1, labelCol2, ... zoals gespecificeerd in de uitgevoerde query [SELECT]. Met de query:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • komt de kolom [ID] terecht in een kolom van [ResultSet] met de naam [myId];
  • de kolom [NOM] wordt ondergebracht in een kolom van de tabel [ResultSet] met de naam [myNom];
  • ...

Hierboven worden de identificatiecodes [myCol] kolomlabels genoemd. Zonder deze labels zijn de kolomnamen van [ResultSet] afhankelijk van SGBD. Wanneer het [SELECT] op één enkele tabel wordt uitgevoerd, zijn de kolomlabels standaard de kolomnamen die door het SELECT worden opgevraagd. Het probleem doet zich voor wanneer de [SELECT] op meerdere tabellen wordt uitgevoerd en er in deze tabellen identieke kolomnamen voorkomen, zoals in het volgende voorbeeld:

SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

waarbij we aannemen dat de tabel [PRODUITS] een vreemde sleutel heeft naar de tabel [CATEGORIES], weergegeven door de relatie [Produits].CATEGORIE_ID --> [CATEGORIES].ID, en dat de tabellen [PRODUITS] en [CATEGORIES] beide een veld [NOM] hebben. In dit geval zijn de namen die in de tabel [ResultSet] aan de kolommen [PRODUITS.NOM] en [CATEGORIES.NOM] zijn gegeven, afhankelijk van de tabel SGBD. Voor de overdraagbaarheid tussen SGBD moeten hier dus kolomlabels worden gebruikt en schrijven we:


SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

Om de verschillende velden van de huidige regel van [ResultSet] te verwerken, zijn de volgende methoden beschikbaar:

Type getType("labelColi") 

om de kolom met de naam „labelColi” van de huidige regel op te halen en dus de kolom van het [SELECT]-record met dit label. Type geeft het type van het veld coli aan. De volgende [getType]-methoden kunnen worden gebruikt: getInt, getLong, getString, getDouble, getFloat, getDate, ... In plaats van de kolomnaam te gebruiken, kan men de positie ervan in de uitgevoerde [SELECT]-query gebruiken:

Type getType(i) 

waarbij i de index is van de gewenste kolom (i>=1).

  • regels 15-17: ophalen van de waarden die zijn gelezen in de BD;
  • regel 19: de transactie wordt gevalideerd (ook wel ‘committed’ genoemd). Hierdoor wordt de transactie beëindigd en worden de bronnen vrijgegeven die de transactie SGBD ervoor had gereserveerd;
  • regel 25: de resources worden vrijgegeven in de [finally]. Deze roept de volgende methode [doFinally] aan:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // afsluiting ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // afsluiten [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // verbinding verbreken
                connexion.close();
            } catch (SQLException e3) {
                 // de uitzondering afhandelen
            }
        }
    }
  • regels 3-9: afsluiting van [ResultSet];
  • regels 11-17: afsluiten van [PreparedStatement];
  • regels 18-27: verbinding afsluiten;

Het afsluiten in de regels 3-17 lijkt overbodig, aangezien de verbinding in de regels 18-25 wordt afgesloten. In sommige gevallen is dit echter niet het geval en wordt aangeraden om ze te laten staan: [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • regel 22: de uitzondering wordt afgehandeld door de volgende methode [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // transactie annuleren
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // de uitzondering afhandelen
        }
}
  • regels 4-6: de transactie wordt geannuleerd. Hiermee wordt de transactie beëindigd en kan de methode SGBD de daarvoor ingezette bronnen vrijgeven;

3.2.4. stap 3 – verzending van opdrachten SQL [INSERT, UPDATE, DELETE]

De opdrachten SQL en [INSERT, UPDATE, DELETE] zijn bijwerkingsbewerkingen: ze wijzigen de database, maar leveren geen rijen op. De enige informatie die wordt geretourneerd, is het aantal rijen waarop de bijwerkingsbewerking betrekking heeft.

De code


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // verbinding openen
            connexion = DriverManager.getConnection(url, user, passwd);
            // transactie starten
            connexion.setAutoCommit(false);
            // in lees-/schrijfmodus
            connexion.setReadOnly(false);
            // de tabel wordt bijgewerkt
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // categorie 1
            ps.setInt(1, 10);
            // uitvoering
            int nbLignes=ps.executeUpdate();
            // transactie vastleggen
            connexion.commit();
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            doCatchException(connexion, e1);
        } finally {
            // de finally-blok wordt verwerkt
            doFinally(null, ps, connexion);
        }
    }
  • regel 9: de verbinding wordt gebruikt voor zowel lezen als schrijven;
  • regel 11: een [PreparedStatement] met 1 parameter (aangeduid met ?). Er kunnen meerdere parameters zijn. Deze worden genummerd vanaf 1;
  • regel 13: de waarde wordt toegewezen aan de enige parameter. De eerste parameter van [setType] is de positie van de parameter in de [PreparedStatement] (1, 2, ...) en de tweede is de waarde die eraan wordt toegewezen. Men kan de methoden [setInt, setLong, setFloat, setDouble, setString, setDate, ...] gebruiken;
  • regel 15: hier wordt de methode [executeUpdate] gebruikt en niet [executeQuery], die is gereserveerd voor opdrachten van het type SELECT. De methode retourneert het aantal regels dat door de bewerking is beïnvloed. Dit kan 0 zijn.
  • regel 17: de transactie wordt gevalideerd;

3.2.5. stap 4 – het afsluiten van de verbinding

Een verbinding moet in een multi-user-omgeving zo snel mogelijk worden gesloten, omdat een SGBD slechts een beperkt aantal open verbindingen toestaat. In de voorgaande voorbeelden werd de verbinding gesloten in de clausule [finally] van de bewerkingen SQL, zodat deze altijd wordt gesloten, ongeacht of er een uitzondering is opgetreden of niet.

3.3. Configuratie van de laag JDBC van SGBD MySQL5

We gaan het project [mysql-config-jdbc] bekijken, dat de onderstaande laag JDBC configureert:

3.3.1. Het Eclipse-project

 

3.3.2. Maven-configuratie

Het bestand [pom.xml] van het project ziet er als volgt uit:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>configuration generic jdbc</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- afhankelijke variabelen ********************************************** -->
        <!-- stuurprogramma JDBC van SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- constante afhankelijkheden ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- bibliotheek jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- logs -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

In deze Maven-configuratie zijn een aantal archieven gebundeld die nodig zijn voor het project [mysql-config-jdbc] of voor projecten die daarop voortbouwen:

  • regels 4-6: het door het project gegenereerde Maven-artefact. Zoals eerder vermeld, genereren alle projecten van het type [*-config-jdbc] hetzelfde artefact. Er mogen dus niet twee projecten van het type [*-config-jdbc] tegelijkertijd worden geladen;
  • regels 9-13: het bovenliggende Maven-project. Dit definieert de versies van een groot aantal bibliotheken die door het Spring-ecosysteem worden gebruikt. Hierdoor hoeft u deze niet opnieuw te specificeren in de afgeleide projecten;
  • regels 18-21: het stuurprogramma-archief JDBC van SGBD en MySQL5. Dit is het enige archief dat nodig is voor het project [spring-jdbc-01];
  • regels 24-27: het artefact [tomcat-jdbc] levert een archief dat nodig is voor de projecten JDBC en [spring-jdbc-02 à 04];
  • regels 29-36: bevatten de bibliotheken die nodig zijn voor het beheer van jSON. Deze worden in vrijwel alle projecten in het document gebruikt;
  • regels 38-42: Google Guava is een bibliotheek voor het beheer van collecties. Wordt in vrijwel alle projecten in het document gebruikt;
  • regels 43-52: de bibliotheken waarmee tests kunnen worden geschreven waarin Spring en JUnit zijn geïntegreerd. Wordt in vrijwel alle projecten in het document gebruikt;
  • regels 54-57: de logboekbibliotheken. Worden in vrijwel alle projecten in dit document gebruikt;
  • regels 67-71: de plug-in waarmee het artefact van het project [mysql-config-jdbc] in de lokale Maven-repository kan worden geïnstalleerd;

3.3.3. De configuratieklasse [ConfigJdbc]

  

De klasse [ConfigJdbc] ziet er als volgt uit:


package generic.jdbc.config;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

public class ConfigJdbc {

    // verbindingsinstellingen
    public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
    public final static String USER_DBPRODUITS = "root";
    public final static String PASSWD_DBPRODUITS = "root";
...
    // opdrachten SQL [jdbc-01, jdbc-02]
    public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
    //public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
    public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

    // opdrachten SQL [jdbc-03]
    public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
    public final static String V2_DELETE_ALLPRODUITS = "DELETE FROM PRODUITS";
    public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_ALLPRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_PRODUIT_BYNAME = "SELECT ID, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE NOM=?";
    public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";

...

}

De klasse [ConfigJdbc] dient om de laag JDBC van de vier projecten [spring-jdbc-01 à 04] te configureren. Het grootste deel van de configuratie betreft het project [spring-jdbc-04]. We zullen dit deel behandelen wanneer we dit project bestuderen. Hierboven is alleen de configuratie van de projecten [spring-jdbc-01 à 03] weergegeven.

  • regels 14-17: de verbindingsparameters voor de database MySQL5 [dbproduits];
  • regels 20-25: de commando’s SQL die worden gebruikt in de projecten [spring-jdbc-01 et 02];
  • regels 28-34: de SQL-opdrachten die worden gebruikt in het project [spring-jdbc-03];

Deze opdrachten SQL maken gebruik van de tabel [PRODUITS] uit de database MySQL5 [dbproduits], waarvan de structuur als volgt is:

 
  • [ID]: primaire sleutel in de modus AUTO_INCREMENT (als er geen primaire sleutel wordt opgegeven, genereert SGBD deze);
  • [NOM]: naam van een product – uniek;
  • [CATEGORIE]: nummer van de categorie;
  • [PRIX]: de prijs;
  • [DESCRIPTION]: een beschrijving van het product;

3.3.4. De klasse [Produit]

  

De klasse [Produit] is de afbeelding van een rij uit de tabel [PRODUITS]:


package generic.jdbc.entities.dbproduits;

public class Produit {

    // velden
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // constructors
    public Produit() {

    }

    public Produit(int id, String nom, int categorie, double prix, String description) {
        this.id = id;
        this.nom = nom;
        this.categorie = categorie;
        this.prix = prix;
        this.description = description;
    }

    // getters en setters
...
}

Later zullen we twee producten moeten vergelijken om te bepalen of ze gelijk zijn of niet. We zeggen dat twee producten gelijk zijn als al hun velden gelijk zijn. Hiervoor gaan we de methode [equals] van de klasse [Object], waarvan de klasse [Produit] is afgeleid, opnieuw definiëren:


    // gelijkheidsmethode
    @Override
    public boolean equals(Object o) {
        // eenvoudige gevallen
        if (o == null || o.getClass() != this.getClass()) {
            return false;
        }
        Produit p = (Produit) o;
        return this == o
                || (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
                        && Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
  • regel 3: de methode [equals] ontvangt een object o dat ze moet vergelijken met het object this;
  • regels 5-7: de eenvoudige gevallen waarin direct kan worden vastgesteld dat de twee objecten niet gelijk zijn. [Object].getClass() levert een instantie van het type [Class], een type dat de werkelijke klasse van het object vertegenwoordigt;
  • regel 8: het object o wordt omgezet in product p;
  • regel 9: als de twee verwijzingen o en p naar een product gelijk zijn, dan gaat het fysiek om hetzelfde product;
  • regel 9: als o en p twee verschillende verwijzingen zijn naar twee producten met dezelfde velden, dan zeggen we dat ze gelijk zijn. Omdat de prijs van het type [double] is en er in de informatica geen exacte weergave van reële getallen bestaat, beschouwen we twee prijzen als identiek als ze tot op 10⁻⁶ nauwkeurig gelijk zijn;

Bovendien zullen we de methode [hasCode] van de klasse [Object] opnieuw definiëren:


    // hashcode
    @Override
    public int hashCode() {
        return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}

De waarden van de hashCode van twee producten moeten hetzelfde zijn als de methode [equals] deze twee producten als gelijk heeft aangemerkt. Deze waarde van de hashCode wordt gebruikt om objecten te verdelen over verzamelingen zoals woordenboeken. Als twee producten identiek zijn, zullen ze in het bovenstaande voorbeeld inderdaad dezelfde hashCode hebben.

3.3.5. De uitzondering [UncheckedException]

  

Laten we de volgende architectuur eens bekijken:

  • de laag [JDBC] genereert uitzonderingen van het type [SQLException]. Deze uitzondering moet door de lagen worden doorgegeven totdat de hoogste laag wordt bereikt, in dit geval de testlaag;

De laag [DAO] zou zich ermee kunnen tevredenstellen om de [SQLException] door te sturen naar de testlaag. Maar aangezien deze uitzondering ongecontroleerd is (rechtstreeks afgeleid van [Exception]), zou dit betekenen dat de interface [IDao] van de laag [DAO] als volgt zou moeten zijn:


public interface IDao {

    // producten toevoegen
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;

    // lijst met alle producten
    public List<Produit> getAllProduits() throws SQLException;

    // een specifiek product
    public Produit getProduitById(int id) throws SQLException;

    public Produit getProduitByName(String name) throws SQLException;

    // meerdere producten bijwerken
    public int updateProduits(List<Produit> produits) throws SQLException;

    // alle producten verwijderen
    public int deleteAllProduits() throws SQLException;

    // meerdere producten verwijderen
    public int deleteProduits(int[] ids) throws SQLException;
}

En dat is erg vervelend, want het verhindert ons om de interface [IDao] te implementeren met een klasse die een andere uitzondering zou genereren. Om dit probleem te omzeilen, zal de laag [DAO] een ongecontroleerde uitzondering [DaoException] (afgeleid van [RuntimeException]) genereren, waardoor we de clausule [throws] in de methodesignatuur van de interface kunnen vermijden. Daardoor kan deze worden geïmplementeerd door elke klasse die zelf ook een ongecontroleerde uitzondering genereert, die kan verschillen van de uitzondering [DaoException]. Onze architectuur ziet er nu als volgt uit:

Om het genereren van ongecontroleerde uitzonderingen voor verschillende lagen van een applicatie te vergemakkelijken, maken we voor deze lagen een bovenliggende klasse [UncheckedException]:

  

package generic.jdbc.infrastructure;

import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

// generieke uitzonderingsklasse
// de uitzondering is ongecontroleerd

public class UncheckedException extends RuntimeException {

    // serienummer ID gegenereerd
    private static final long serialVersionUID = -2924871763340170310L;

    // eigenschappen
    private int code;
    private String trace;
    private List<ShortException> exceptions;

    // constructors
    public UncheckedException() {
        super();
    }

    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // lokaal
        this.code = code;
        this.exceptions = getErreursForException(e);
        // trace
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
    }

    @Override
    public String getMessage() {
        return this.toString();
    }

    @Override
    public void printStackTrace() {
        System.out.println(this);
    }

    // lijst met foutmeldingen van een uitzondering
    private List<ShortException> getErreursForException(Throwable th) {
        // de elementen van de uitzonderingsstack worden opgehaald
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // de huidige uitzondering ophalen
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // volgende uitzondering
            cause = cause.getCause();
        }
        return exceptions;
    }

    @Override
    public String toString() {
        ObjectMapper jsonMapper = new ObjectMapper();
        try {
            return String.format("[code=%s, trace=%s, exceptions=%s", code, trace, jsonMapper.writeValueAsString(exceptions));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    // getters en setters
...
}
  • regel 12: de klasse is afgeleid van [RuntimeException] en is dus een type ongecontroleerde uitzondering. Deze zal worden gebruikt om een gecontroleerde uitzondering (SQLException) in te kapselen in een type ongecontroleerde uitzondering (UncheckedException);
  • om uitzonderingen van het type [UncheckedException] van elkaar te onderscheiden, kan aan elke uitzondering een code worden toegewezen die wordt opgeslagen in het privéveld van regel 18. Een Java-code die een uitzondering van het type [UncheckedException] onderschept, heeft toegang tot deze foutcode via de methode [getCode] (regels 80 en verder);
  • regel 20: slaat de foutmeldingen van de stack van de ingekapselde uitzondering op;
  • regels 23-43: de verschillende manieren om een object van het type [UncheckedException] te construeren;
  • regels 56-67: een privémethode waarmee de foutenlijst van regel 20 kan worden samengesteld op basis van een object van het type [Throwable] of een daarvan afgeleid type, met name het type [Exception];
  • regels 69-78: de methode [toString] retourneert een tekenreeks die de uitzondering weergeeft. Om de foutenlijst van regel 20 weer te geven, maakt deze gebruik van een bibliotheek jSON. Deze is opgenomen in de Maven-afhankelijkheden van het project:

        <!-- bibliotheek jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
</dependency>
  • regels 45-48: herdefiniëren de methode [getMessage] van de bovenliggende klasse [RuntimeException]. Deze methode retourneert hier de signatuur [toString] van de klasse;
  • regels 50-53: herdefiniëren de methode [printStackTrace] van de bovenliggende klasse [RuntimeException]. De signatuur [toString] van de klasse wordt weergegeven;

De klasse [UncheckedException] slaat in het veld van regel 20 een lijst met uitzonderingen op, die worden beschreven door het volgende type [ShortException]:


package pam.dao.exceptions;

public class ShortException {

    // eigenschappen
    private String className;
    private String errorMessage;

    // constructors
    public ShortException() {

    }

    public ShortException(String className, String errorMessage) {
        this.className = className;
        this.errorMessage = errorMessage;
    }

    // getters en setters
...
}
  • regel 6: de naam van de klasse van de uitzondering die zich heeft voorgedaan;
  • regel 7: de bijbehorende foutmelding;

Laten we de volgende constructor van de klasse [UncheckedException] eens bekijken:


    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // lokaal
        this.code = code;
        this.exceptions = getErreursForException(e);
        // trace
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
}
  • regel 1, de parameters zijn als volgt:
    • [code]: een foutcode;
    • [e]: de uitzondering die wordt ingekapseld. [Throwable] is de bovenliggende klasse van de klasse [Exception] en is rechtstreeks afgeleid van de klasse [Object]. Dit is de bovenliggende klasse van alle C-klassen waarmee men [throw c;] kan schrijven, waarbij c een instantie van C is;
    • [simpleClassName]: de eenvoudige naam van de gebruikersklasse waarin de uitzondering e is gedetecteerd;
  • regel 4: de foutcode wordt geregistreerd;
  • regel 5: de lijst van [ShortException] wordt samengesteld op basis van de als parameter doorgegeven [Throwable e];
  • regels 7-16: de zogenaamde sporen van de uitzondering worden onderzocht. Een initiële uitzondering doet zich voor op een specifieke plaats in de code en wordt vervolgens doorgegeven naar de methode die de methode heeft aangeroepen waarin de uitzondering zich heeft voorgedaan, enzovoort, totdat een try/catch-blok deze stopt. Tijdens deze opwaartse beweging laat de oorspronkelijke uitzondering sporen achter die worden opgeslagen in de array [e.stackTrace] van de uitzondering e. Deze worden hier in regel 8 verkregen op basis van de als parameter doorgegeven [Throwable e]. Elk element van het type [StackTraceElement] is een object met onder andere de volgende velden:
    • [fileName]: de naam van het Java-bestand waarin de uitzondering is opgetreden;
    • [lineNumber]: het regelnummer in dit bestand waar de uitzondering is opgetreden;
    • [methodName]: de naam van de methode in dit bestand waarin de uitzondering is opgetreden;
  • de regels 10-16 zoeken in de tabel met tracegegevens van de als parameter doorgegeven uitzondering naar de eerste keer dat de voorwaarde [trace.fileName==simpleClassName.java] voorkomt, waarbij [simpleClassName] de derde parameter van de constructor is. Het idee is om te onthouden waar de uitzondering in de gebruikerscode is opgetreden. Deze zal een uitzondering als volgt inkapselen:
1
2
3
4
5
6
7
try{
// code die een gecontroleerde uitzondering kan genereren
...
}catch(UnTypeDexception e){
// de gecontroleerde uitzondering e wordt ingekapseld in een ongecontroleerde uitzondering
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • regel 13: er wordt een tekenreeks van het type [fileName, methodName, lineNumber] aangemaakt die de plaats in de gebruikerscode aangeeft waar de uitzondering is opgevangen;

Laten we nu eens kijken naar de code die de lijst met uitzonderingen uit de uitzonderingsstack registreert van de uitzondering [Throwable th], die door de vorige constructor is ingekapseld:


    // lijst met foutmeldingen van een uitzondering
    private List<ShortException> getErreursForException(Throwable th) {
        // de elementen van de uitzonderingsstack worden opgehaald
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // de huidige uitzondering ophalen
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // volgende uitzondering
            cause = cause.getCause();
        }
        return exceptions;
}

Tijdens het doorlopen van de stack terug naar de methode die de uitzondering met een try/catch-blok heeft opgevangen, kan de oorspronkelijke uitzondering e in een andere uitzondering zijn ingekapseld. Het is dan deze laatste uitzondering die de stack doorloopt terug naar de methode die haar definitief zal opvangen. Zij kan dus ook zelf worden ingekapseld. Uiteindelijk, wanneer een methode besluit een uitzondering th te stoppen en te verwerken, zal zij de oorspronkelijke uitzondering e diep begraven in een stapel uitzonderingen aantreffen. Zo is hierboven de parameter [Throwable th] slechts het topje van de ijsberg van uitzonderingen. Aan de hand van het attribuut [th.cause] kan worden vastgesteld welke uitzondering deze zelf inkapselen. En zo verder. Wanneer een uitzondering e de waarde [e.getCause()==null] heeft, betekent dit dat e de oorspronkelijke uitzondering is.

  • regel 8: voor elke uitzondering in de uitzonderingsstapel van [Throwable th] worden twee gegevens opgeslagen:
    • [getClass().getName()]: de volledige naam van de uitzondering;
    • [getMessage()]: de bijbehorende foutmelding;

3.4. Exemple-01

3.4.1. De architectuur van het project

In dit voorbeeld maakt een consoleprogramma gebruik van de interface van de laag [JDBC].

3.4.2. Het Eclipse-project

We maken een Spring/Maven-project [spring-jdbc-01] aan volgens de procedure uit paragraaf 2.5.2.1.

  

Het project is een Maven-project dat wordt gedefinieerd door het volgende bestand [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-01</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- bovenliggende opzoeken in de repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configuratie JDBC van SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • regels 28-32: het project maakt gebruik van het artefact [generic-config-jdbc] uit het project [mysql-config-jdbc] dat we zojuist hebben bekeken. Het project [spring-jdbc-01] heeft dus toegang tot alle elementen van het project [mysql-config-jdbc];

Dit laatste punt kan op twee manieren worden bekeken door de Maven-afhankelijkheden van het project te inspecteren:

  • in [2] zien we dat het project [mysql-config-jdbc] in de Maven-afhankelijkheden van het project staat. Aangezien deze afhankelijkheden zich in het classpath van het project bevinden, betekent dit dat het project [mysql-config-jdbc] zich eveneens in dit classpath bevindt en dat de klassen en interfaces ervan dus zichtbaar zijn in het project [spring-jdbc-01];

Het Maven-project [mysql-config-jdbc] hoeft niet in het tabblad [Package Explorer] te staan om door andere Maven-projecten te kunnen worden gebruikt. Het volstaat dat het in de lokale Maven-repository aanwezig is. In tegenstelling tot bij een IDE zoals NetBeans, gebeurt dit bij Eclipse niet automatisch. Je moet dit handmatig forceren:

In paragraaf 2.3.5 hebben we de voorwaarden besproken die deze generatie mogelijk maken. Zodra dit is gebeurd, kan het project [mysql-config-jdbc] uit het tabblad [Package Explorer] worden verwijderd:

  • Vink [3] niet aan, want daarmee wordt het project fysiek van de schijf verwijderd, waardoor het onherstelbaar verloren gaat;

Deze bewerking start de berekening van de Maven-afhankelijkheden opnieuw op voor de projecten die afhankelijk zijn van het project dat uit [Package Explorer] is verwijderd. Hierdoor verandert de branch [Maven Dependencies] van deze projecten. Voor het project [spring-jdbc-01] wordt de branch [Maven Dependencies] bijvoorbeeld als volgt:

Deze keer is de afhankelijkheid niet meer van een project, maar van het bijbehorende Maven-artefact, in dit geval het artefact [generic-config-jdbc] [1]. We zien dat we inderdaad toegang hebben tot alle klassen en interfaces van dit artefact. Zoals gezegd, wordt dit artefact gegenereerd door alle [*-config-jdbc]-projecten. Om fouten te voorkomen, zullen we:

  • altijd één enkel project [*-config-jdbc] behouden in het tabblad [Package Explorer];
  • de Maven-configuratie van alle projecten in het tabblad [Package Explorer] (Alt-F5) bijwerken, zodat het gebruikte project [*-config-jdbc] in hun Maven-afhankelijkheden verschijnt;

3.4.3. Het skelet van de hoofdklasse

  

Het skelet van de hoofdklasse [IntroJdbc01] ziet er als volgt uit:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class IntroJdbc01 {

    // constanten
    final static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) {
        // het laden van de driver JDBC van SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // de tabel [PRODUITS] wordt geleegd
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // de tabel wordt gevuld
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // de tabel wordt gelezen
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // bijgewerkt
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // weergave
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // de tabel wordt geleegd [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // wordt weergegeven
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion van twee identieke elementen
        // de INSERTion moet mislukken en geen van beide elementen wordt ingevoegd vanwege de transactie
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // we controleren
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // klaar
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // productlijst
    private static void select() {
    ...
    }

    // weergave van een object jSON
    private static void affiche(Object object) {
...
    }

    // producten verwijderen
    public static void delete() {
...
    }

    // producten toevoegen
    public static void insert() {
...
    }

    // 2 producten met dezelfde primaire sleutels toevoegen
    public static void insert2() {
...
    }

    // bepaalde producten bijwerken
    public static void update() {
...
    }

    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // afsluiting ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // afsluiten [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // verbinding verbreken
                connexion.close();
            } catch (SQLException e3) {
                // foutmeldingen worden weergegeven
                show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        getErreursFromThrowable(e3));
            }
        }
    }

    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // foutmeldingen worden weergegeven
        show(title, getErreursFromThrowable(th));
        // transactie annuleren
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // foutmeldingen weergeven
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }

    private static List<String> getErreursFromThrowable(Throwable th) {
        // de lijst met foutmeldingen van de uitzondering wordt opgehaald
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // foutmelding van de throwable
            erreurs.add(th.getMessage());
            // ga naar de oorzaak van de throwable
            th = th.getCause();
        }
        // resultaat
        return erreurs;
    }

    private static void show(String title, List<String> messages) {
        // titel
        System.out.println(String.format("%s : ", title));
        // berichten
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • regels 23-29: het laden van de driver JDBC vanuit SGBD. Op regel 25 wordt de constante [ConfigJdbc.DRIVER_CLASSNAME] gebruikt, die is gedefinieerd in het project [mysql-config-jdbc];
  • regels 136-147: de methode [getErreursFromThrowable] retourneert de lijst met foutmeldingen, ingekapseld in een object van het type [Throwable], de bovenliggende klasse van de klasse [Exception]. Een uitzondering kan een andere uitzondering bevatten, die kan worden opgehaald met de methode [Throwable].getCause(). Op deze manier worden alle uitzonderingen doorlopen die zijn ingekapseld in het object [Throwable];
  • regels 149-156: de methode [show(String title, List<String> messages)] geeft de berichten weer, voorafgegaan door de tekst [title];
  • regels 122-134: de methode [doCatchException(String title, Connection connexion, Throwable th))] verwerkt de uitzonderingen die door de methoden van de klasse worden gegenereerd. De verwerkte uitzondering wordt vertegenwoordigd door de parameter [Throwable th]. Het doel van de methode is:
    • de lopende transactie van het object [Connection connexion] ongedaan te maken (regels 127-129);
    • de foutmeldingen die in de uitzondering [Throwable th] zijn ingekapseld, op te schrijven (regels 124, 132);
  • regels 93-120: de methode [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] beheert de tak [finally] van de toegangsmethoden voor SGBD. Het doel ervan is de door de verbinding gebruikte bronnen vrij te maken;

3.4.4. Verwijderen van de inhoud van de producttabel

De methode [delete] verwijdert de inhoud van de tabel:


    // producten verwijderen
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // verbinding openen
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // transactie starten
            connexion.setAutoCommit(false);
            // in lees-/schrijfmodus
            connexion.setReadOnly(false);
            // de tabel wordt geleegd [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // transactie vastleggen
            connexion.commit();
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // de finally-blok wordt verwerkt
            doFinally(null, ps, connexion);
        }
}

Regel 7 maakt gebruik van de volgende constanten van de klasse [ConfigJdbc]:


public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";

In regel 13 is de voorbereide opdracht SQL als volgt:


public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";

De methode [delete] maakt gebruik van transacties. Met een transactie kunnen opdrachten SQL worden gegroepeerd die allemaal moeten slagen of allemaal moeten worden geannuleerd. Er zijn vier bewerkingen die u moet kennen:

  • het starten van een transactie: [connexion.setAutoCommit(false)];
  • einde van een transactie met succes: [connexion.commit()]. In dit geval worden alle bewerkingen die tijdens de transactie op de BD zijn uitgevoerd, gevalideerd;
  • einde van een mislukte transactie: [connexion.rollback()]. In dit geval worden alle bewerkingen die tijdens de transactie op BD zijn uitgevoerd, ongedaan gemaakt;

In onze voorbeelden breken we de transactie telkens wanneer er een uitzondering optreedt af in de methode [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // de foutmeldingen worden weergegeven
        Static.show(title, Static.getErreursFromThrowable(th));
        // transactie ongedaan maken
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // foutmeldingen weergeven
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

3.4.5. Aanmaken van de inhoud van de producttabel

De methode [insert] maakt de inhoud van de tabel aan:


public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // verbinding openen
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // transactie starten
            connexion.setAutoCommit(false);
            // in lees-/schrijfmodus
            connexion.setReadOnly(false);
            // de tabel wordt gevuld
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // voorbereiding
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // uitvoering
                ps.executeUpdate();
            }
            // transactie vastleggen
            connexion.commit();
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // de finally-blok wordt verwerkt
            doFinally(null, ps, connexion);
        }
    }

Regel 12: de voorbereide opdracht SQL ziet er als volgt uit:


public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";

3.4.6. Weergave van de inhoud van de producttabel

De methode [select] geeft de inhoud van de tabel weer:


// productlijst
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // verbinding openen
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // transactie starten
            connexion.setAutoCommit(false);
            // in alleen-lezenmodus
            connexion.setReadOnly(true);
            // de tabel [PRODUITS] wordt gelezen
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // transactie vastleggen
            connexion.commit();
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // de finally-blok wordt verwerkt
            doFinally(rs, ps, connexion);
        }
    }

Regel 14: de voorbereide opdracht SQL is als volgt:


public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";

De methode [affiche] (regel 18) is als volgt:


    // weergave van een object jSON
    private static void affiche(Object object) {
        try {
            System.out.println(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Deze functie geeft de weergave jSON weer van het als parameter doorgegeven object (zie jSON, paragraaf 23.12).

3.4.7. De inhoud van de tabel bijwerken

De methode [update] werkt bepaalde producten bij:


    // bepaalde producten bijwerken
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // verbinding openen
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // transactie starten
            connexion.setAutoCommit(false);
            // in lees-/schrijfmodus
            connexion.setReadOnly(false);
            // de tabel wordt bijgewerkt
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // categorie 1
            ps.setInt(1, 1);
            // uitvoering
            ps.executeUpdate();
            // transactie vastleggen
            connexion.commit();
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // de finally-blok wordt verwerkt
            doFinally(null, ps, connexion);
        }
}

Regel 13: de voorbereide opdracht SQL is als volgt:


public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";

3.4.8. Rol van de transactie

De methode [insert2] voegt twee producten met dezelfde primaire sleutel in de tabel in, wat niet mogelijk is. Aangezien we ons in een transactie bevinden, wordt de eerste invoeging ongedaan gemaakt.


    // twee producten met dezelfde primaire sleutels toevoegen
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // verbinding openen
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // transactie gestart
            connexion.setAutoCommit(false);
            // in lees-/schrijfmodus
            connexion.setReadOnly(false);
            // er wordt 1 rij toegevoegd
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // uitvoering
            ps.executeUpdate();
            // dezelfde regel wordt een tweede keer toegevoegd, dus met dezelfde primaire sleutel
            // de INSERTion moet mislukken en geen van beide elementen mag worden ingevoegd vanwege de transactie
            ps.executeUpdate();
            // transactie vastleggen
            connexion.commit();
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // we verwerken de finally
            doFinally(null, ps, connexion);
        }
}

Regel 13: de voorbereide opdracht SQL is als volgt:


public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

3.4.9. Resultaten

We voeren de uitvoeringsconfiguratie met de naam [spring-jdbc-generic-01.IntroJdbc01] uit:

 

De volgende console-uitvoer wordt weergegeven:


------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé
  • regel 30: vóór het invoegen van de twee producten met dezelfde primaire sleutel is de tabel leeg;
  • regel 35: na het invoegen van de twee producten met dezelfde primaire sleutel is de tabel leeg. Dit illustreert de rol van de transactie:
    • de eerste invoeging slaagt. Er is geen reden waarom deze zou mislukken;
    • de tweede invoeging mislukt (regel 32). Omdat deze twee invoegingen deel uitmaken van dezelfde transactie, worden alle opdrachten SQL van deze transactie ongedaan gemaakt, waaronder de eerste invoeging.

3.4.10. Conclusie

Wat opvalt in de bovenstaande code, is de grote aandacht die wordt besteed aan de afhandeling van de uitzondering [SQLException]. Aangezien elke bewerking met JDBC deze uitzondering kan activeren, bevat de code talrijke try/catch-constructies.

3.5. Exemple-02

We gaan verder met de vorige toepassing, maar nu met een gegevensbron van het type [javax.sql.DataSource]:

Image

We gaan een gegevensbron gebruiken die is geïmplementeerd door de klasse [org.apache.tomcat.jdbc.pool.DataSource]. Deze klasse maakt gebruik van een verbindingspool, d.w.z. een verzameling geopende verbindingen:

  • wanneer de pool wordt geïnstantieerd, wordt een bepaald aantal verbindingen met de database geopend. Dit aantal is configureerbaar;
  • wanneer de Java-code een verbinding opent, wordt deze geleverd door de pool;
  • wanneer de Java-code een verbinding sluit, wordt deze teruggegeven aan de pool;

Uiteindelijk worden de verbindingen slechts één keer geopend, wat de prestaties bij het benaderen van de database verbetert. De gegevensbron wordt gedefinieerd in een Spring-configuratieklasse

3.5.1. De architectuur van het project

In dit voorbeeld maakt een consoleprogramma gebruik van de interface van de laag [JDBC].

3.5.2. Het Eclipse-project

Het nieuwe Eclipse-project kan worden verkregen door het vorige project [1-6] te kopiëren:

Vervolgens wordt het project van [6] aangepast naar [7]:

3.5.3. Maven-configuratie

Het project [7] is een Maven-project dat wordt gedefinieerd door het volgende bestand [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-02</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- opzoeken van bovenliggend element uit repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configuratie JDBC van SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • regels 28-33: de Maven-afhankelijkheid van het project [mysql-config-jdbc];

Het is het project [mysql-config-jdbc] dat in zijn Maven-afhankelijkheden de bibliotheek opneemt die een implementatie biedt van een gegevensbron van het type [javax.sql.DataSource] (zie paragraaf 3.3.2):


        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
</dependency>

3.5.4. Spring-configuratie

  

De Spring-configuratieklasse [AppConfig] ziet er als volgt uit:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
    // gegevensbron
    @Bean
    public DataSource dataSource() {
        // gegevensbron TomcatJdbc
        DataSource dataSource = new DataSource();
        // toegangscontrole JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // aanvankelijk geopende verbindingen
        dataSource.setInitialSize(5);
        // resultaat
        return dataSource;
    }

}
  • regel 10: [AppConfig] is een Spring-configuratieklasse;
  • regel 11: import van de configuratieklasse [generic.jdbc.config.ConfigJdbc.class] die is gedefinieerd in het project [mysql-config-jdbc]. Dit betekent dat alle beans beschikbaar zijn die in dit configuratiebestand zijn gedefinieerd;
  • regels 14-27: de Spring-bean die de gegevensbron definieert;
  • regel 17: aanmaken van de nog niet geconfigureerde gegevensbron;
  • regels 19-22: de gegevens waarmee de gegevensbron verbinding kan maken met de database;
  • regel 24: hiermee wordt een pool van 5 verbindingen aangemaakt. We hebben er hier maar één nodig. Er zijn nooit meerdere gelijktijdige verbindingen;

3.5.5. De hoofdklasse

De hoofdklasse [IntroJdbc02] ziet er als volgt uit:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class IntroJdbc02 {

    // toewijzen jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // gegevensbron
    private static DataSource dataSource;

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // ophalen van de Spring-context
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // ophalen van de gegevensbron
            dataSource = ctx.getBean(DataSource.class);
            // de tabel wordt leeggemaakt [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // klaar
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // productlijst
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // verbinding openen
            connexion = dataSource.getConnection();
            // transactie starten
            connexion.setAutoCommit(false);
            // in alleen-lezenmodus
            connexion.setReadOnly(true);
            // de tabel [PRODUITS] wordt gelezen
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // transactie vastleggen
            connexion.commit();
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // de finally-blok wordt verwerkt
            doFinally(rs, ps, connexion);
        }
    }
...
  • regel 25: de gegevensbron. Merk op dat deze van het type [javax.sql.DataSource] is (regel 13), wat een interface is;
  • regel 31: instantiëren van de Spring-objecten;
  • regel 32: het verkrijgen van een verwijzing naar de gegevensbron. Merk op dat de daadwerkelijk gebruikte klasse op geen enkel moment wordt genoemd. Hier wijst dus niets erop dat er een implementatie van [TomcatJdbc] wordt gebruikt;
  • regel 49: het verkrijgen van een open verbinding. Op deze manier verkrijgen de verschillende methoden van [IntroJdbc02] een verbinding met de database. De rest van de code is identiek aan die van de klasse [IntroJdbc01];

3.5.6. De tests

We voeren de uitvoeringsconfiguratie met de naam [spring-jdbc-generic-02.IntroJdbc02] uit:

 

We krijgen dezelfde resultaten als eerder (paragraaf 3.4.9).

3.6. Exemple-03

3.6.1. De architectuur van het project

In dit voorbeeld zijn de methoden voor gegevenstoegang geïsoleerd in een laag [dao]. Ze zullen worden getest door een test JUnit.

3.6.2. Het Eclipse-project

Het Eclipse-project [spring-jdbc-03] is een Spring/Maven-project dat op dezelfde manier is opgebouwd als het vorige en vervolgens als volgt is aangevuld:

 

De verschillende pakketten hebben de volgende functies:

  • [spring.jdbc.config]: configuratie van het Spring-project;
  • [spring.jdbc.dao]: implementatie van de laag [DAO];
  • [spring.jdbc.infrastructure]: implementeert de ongecontroleerde uitzondering [DaoException];

3.6.3. Maven-configuratie

Het Maven-project wordt geconfigureerd via het volgende bestand [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-03</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-03</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- opzoeken van bovenliggend object uit repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configuratie JDBC van SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

Het is identiek aan dat van het project [spring-jdbc-02]. Het maakt met name gebruik van de Maven-afhankelijkheid van het project [mysql-config-jdbc] (regels 28-32).

3.6.4. Interface van de laag [DAO]

  

De laag [DAO] heeft de volgende interface [IDao]:


package spring.jdbc.dao;

import java.util.List;

import spring.jdbc.entities.Produit;

public interface IDao {

    // producten toevoegen
    public List<Produit> addProduits(List<Produit> produits);

    // lijst van alle producten
    public List<Produit> getAllProduits();

    // een specifiek product
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // meerdere producten bijwerken
    public int updateProduits(List<Produit> produits);

    // alle producten verwijderen
    public int deleteAllProduits();

    // meerdere producten verwijderen
    public int deleteProduits(int[] ids);
}

3.6.5. De klasse [DaoException]

De klasse [DaoException] is slechts een uitbreiding van de klasse [UncheckedException] die in paragraaf 3.3.5 wordt beschreven:

  

package spring.jdbc.infrastructure;

public class DaoException extends UncheckedException {

    private static final long serialVersionUID = 1L;

    // fabrikanten
    public DaoException() {
        super();
    }

    public DaoException(int code, Throwable e, String className) {
        super(code, e, className);
    }

}

3.6.6. Configuratie van het Spring-project

  

De klasse [AppConfig], die het Spring-project configureert, is identiek aan het Spring-configuratiebestand uit het voorbeeld [spring-jdbc-02], op regel 11 na:


package spring.jdbc.config;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
    // gegevensbron
    @Bean
    public DataSource dataSource() {
        // gegevensbron TomcatJdbc
        DataSource dataSource = new DataSource();
        // toegangscontrole JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // aanvankelijk geopende verbindingen
        dataSource.setInitialSize(5);
        // resultaat
        return dataSource;
    }
}
  • regel 11: het pakket [spring.jdbc.dao] wordt doorzocht op andere Spring-componenten dan die welke in dit configuratiebestand zijn gedefinieerd;

3.6.7. Implementatie van de laag [DAO]

  

Ter herinnering (paragraaf 3.6.4): de laag [DAO] implementeert de volgende interface [IDao]:


package spring.jdbc.dao;

import generic.jdbc.entities.dbproduits.Produit;

import java.util.List;

public interface IDao {

    // producten toevoegen
    public List<Produit> addProduits(List<Produit> produits);

    // lijst met alle producten
    public List<Produit> getAllProduits();

    // een specifiek product
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // meerdere producten bijwerken
    public int updateProduits(List<Produit> produits);

    // alle producten verwijderen
    public int deleteAllProduits();

    // meerdere producten verwijderen
    public int deleteProduits(int[] ids);
}

De klassen [Dao1, Dao2] implementeren beide deze interface. De klasse [Dao2] is een variant van de klasse [Dao1] die een syntactische vernieuwing introduceert. We zullen ons concentreren op de klasse [Dao1]. De basisstructuur hiervan is als volgt:


package spring.jdbc.dao;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.jdbc.infrastructure.DaoException;

@Component("dao1")
public class Dao1 implements IDao {

    // naam van de klasse
    private String simpleClassName = getClass().getSimpleName();
    // gegevensbron
    @Autowired
    protected DataSource dataSource;

    // fabrikant
    public Dao1() {
        System.out.println("building Dao1...");
    }

    // ------------------------------- interface
    @Override
    public List<Produit> getAllProduits() {
...
    }

    @Override
    public Produit getProduitById(int id) {
...
    }

    @Override
    public Produit getProduitByName(String name) {
...
    }

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
....
    }

    @Override
    public int updateProduits(List<Produit> produits) {
...
    }

    @Override
    public int deleteAllProduits() {
...
    }

    @Override
    public int deleteProduits(int[] ids) {
...
    }

    // ---------------------------------------- lokale methoden
    // finally-afhandeling
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }

    // catch-afhandeling
    protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
  • regel 20: de klasse [Dao] is een Spring-component met de naam [dao1]. Deze naam is optioneel. Als deze ontbreekt, wordt de naam van de klasse gebruikt, waarbij de eerste hoofdletter wordt omgezet in een kleine letter;
  • regel 24: de naam van de klasse. We vermijden het hardcoderen van [Dao] om de mogelijkheid te behouden om de klasse te hernoemen zonder dit veld opnieuw te hoeven definiëren, waardoor het altijd geldig blijft;
  • regels 26-27: invoeging van de gegevensbron [tomcat-jdbc] die is gedefinieerd in de configuratieklasse [AppConfig];
  • regels 36-68: implementatie van de interface [IDao];
  • regels 78-80: gecentraliseerd beheer van de catch van de verschillende methoden;
  • regels 72-75: gecentraliseerd beheer van de finally van de verschillende methoden;

De catch van de verschillende methoden wordt als volgt beheerd:


    // catch-afhandeling
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // transactie annuleren
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            e2.printStackTrace();
        }
        // daoException
        return new DaoException(code, th, simpleClassName);
}
  • regel 2: de methode wordt gedeclareerd als [protected], waardoor dochterklassen deze kunnen gebruiken zonder dat deze openbaar is. De methode ontvangt de volgende parameters:
    • [Connection connexion]: de verbinding met de SGBD – mogelijk null;
    • [Throwable th]: de uitzondering die zich heeft voorgedaan en die we gaan inkapselen in een type [DaoException];
    • [int code]: een foutcode die moet worden gebruikt als de methode een nieuwe [DaoException] aanmaakt;
  • regels 4-7: de primaire functie van deze methode is het terugdraaien van de transactie die gekoppeld is aan de verbinding die als parameter 1 is doorgegeven;
  • regels 8-10: als het annuleren van de transactie mislukt, wordt de uitzonderingslog naar de console geschreven. Er kan verder niet veel worden gedaan, aangezien er in regel 12 een uitzondering wordt gegenereerd;

De finally van de verschillende methoden wordt als volgt beheerd:


// beheer finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // afsluiting ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // afsluiting [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // verbinding verbreken
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // de fout wordt geregistreerd indien mogelijk
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // resultaat
        return daoException;
    }
  • regel 2: ook deze methode is gedeclareerd als [protected]. Ze ontvangt de volgende parameters:
    • [ResultSet rs]: eventueel [ResultSet] als een bewerking [SELECT] is uitgevoerd – mogelijk null;
    • [PreparedStatement ps]: de [PreparedStatement] die is uitgevoerd – mogelijk null;
    • [Connection connexion]: de koppeling met SGBD – mogelijk null;
    • [int code]: een foutcode die moet worden gebruikt als de methode een nieuwe [DaoException] aanmaakt;
    • [DaoException daoException]: de eventuele [DaoException] als er een uitzondering is opgetreden vóór de finally – mogelijk null;
  • regels 21-30: het primaire doel van deze methode is het verbreken van de verbinding (regel 23);
  • regels 24-29: als er tijdens het afsluiten een uitzondering optreedt, wordt de status van de doorgegeven parameter [DaoException daoException] gecontroleerd: als deze [daoException == null] is, wordt er een nieuwe [DaoException] aangemaakt met de als parameter doorgegeven code;
  • regel 32: de oude of de nieuwe [DaoException] wordt als resultaat geretourneerd;

We gaan niet alle methoden van de klasse [Dao] bespreken, maar slechts enkele. Ze lijken allemaal op elkaar.

3.6.7.1. De methode [getProduitById]

De methode [getProduitById] retourneert het product waarvan de primaire sleutel gelijk is aan de parameter [id], of anders null;


@Override
    public Produit getProduitById(int id) {
        // verbindingsbronnen
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // in eerste instantie geen uitzondering
        DaoException daoException = null;
        // het gezochte product
        Produit produit = null;
        try {
            // verbinding tot stand gebracht
            connexion = dataSource.getConnection();
            // begin transactie
            connexion.setAutoCommit(false);
            // in alleen-lezenmodus
            connexion.setReadOnly(true);
            // de tabel [PRODUITS] wordt gelezen
            ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
            ps.setInt(1, id);
            rs = ps.executeQuery();
            if (rs.next()) {
                produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
            }
            // transactie vastleggen
            connexion.commit();
            // terug naar de standaardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // de finally-blok wordt verwerkt
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // uitzondering?
        if (daoException != null) {
            throw daoException;
        }
        // resultaat
        return produit;
    }
  • regel 10: het terug te geven product wordt ingesteld op null;
  • regel 19: de opdracht SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] luidt als volgt:

public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";

  • regels 22-24: als [ResultSet] één regel bevat, wordt deze gebruikt om het terug te sturen product aan te maken; anders blijft het terug te sturen product op null staan;
  • regel 41: het product wordt teruggestuurd;
  • regel 8: de uitzondering [DaoException] van de methode wordt geïnitialiseerd op null;
  • regel 31: de methode [doCatchException] genereert een uitzondering [DaoException];
  • regel 34: de parameter [daoException] van de methode [doFinally] is ofwel null,, ofwel de uitzondering die door de methode [doCatchException] is gegenereerd. De methode [doFinally]:
    • laat deze parameter ongewijzigd als de verbinding met succes wordt verbroken;
    • laat deze parameter ongewijzigd als de verbinding niet kan worden verbroken en er eerder al een [DaoException] is geweest;
    • maakt een nieuwe [DaoException] aan als de verbinding niet kan worden verbroken en er nog geen [DaoException] is uitgevoerd;
  • regels 37-39: als de lokale uitzondering [daoException] niet gelijk is aan null, dan wordt deze gegenereerd; anders wordt het gevraagde resultaat geretourneerd (regel 41);

3.6.7.2. De methode [deleteProduits]

De methode [deleteProduits] verwijdert de producten waarvan de primaire sleutels als parameter worden doorgegeven. Ze retourneert het aantal verwijderde producten.


@Override
    public int deleteProduits(int[] ids) {
        // verbindingsbronnen
        PreparedStatement ps = null;
        Connection connexion = null;
        // in eerste instantie geen uitzondering
        DaoException daoException = null;
        // aantal bijgewerkte producten
        int nbProduits = 0;
        try {
            // verbinding geopend
            connexion = dataSource.getConnection();
            // start transactie
            connexion.setAutoCommit(false);
            // in lees-/schrijfmodus
            connexion.setReadOnly(false);
            // producten worden verwijderd
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // instellingen
                ps.setInt(1, id);
                // uitvoering
                nbProduits += ps.executeUpdate();
            }
            // transactie vastleggen
            connexion.commit();
            // terug naar standaardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // de finally-blok wordt verwerkt
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // uitzondering?
        if (daoException != null) {
            throw daoException;
        }
        // resultaat
        return nbProduits;
    }
  • regel 18, de volgorde van SQL en [ConfigJdbc.V2_DELETE_PRODUITS] is als volgt:

public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";

  • regels 18-24: de code voor het verwijderen van producten. We zien dat de opdracht SQL één keer wordt voorbereid (regel 18) en n keer wordt uitgevoerd (regels 19-24). Dit is het nut van het object [PreparedStatement];
  • regel 23: de methode [PreparedStatement].executeUpdate() geeft het aantal regels weer dat door de bijwerkingsbewerking is beïnvloed;
  • regel 41: het aantal bijgewerkte producten wordt geretourneerd;

3.6.7.3. De methode [updateProduits]

De methode [updateProduits] werkt de producten bij in de database die als parameters worden doorgegeven. De methode retourneert het aantal bijgewerkte producten.


@Override
    public int updateProduits(List<Produit> produits) {
        // verbindingsbronnen
        PreparedStatement ps = null;
        Connection connexion = null;
        // in eerste instantie geen uitzondering
        DaoException daoException = null;
        // aantal bijgewerkte producten
        int nbProduits = 0;
        try {
            // verbinding geopend
            connexion = dataSource.getConnection();
            // transactie gestart
            connexion.setAutoCommit(false);
            // in lees-/schrijfmodus
            connexion.setReadOnly(false);
            // de tabel [PRODUITS] wordt bijgewerkt
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // parameters
                ps.setString(1, produit.getNom());
                ps.setDouble(2, produit.getPrix());
                ps.setInt(3, produit.getCategorie());
                ps.setString(4, produit.getDescription());
                ps.setInt(5, produit.getId());
                // uitvoering
                nbProduits += ps.executeUpdate();
            }
            // transactie vastleggen
            connexion.commit();
            // terug naar de standaardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // de uitzondering wordt afgehandeld
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // de finally-blok wordt verwerkt
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // uitzondering?
        if (daoException != null) {
            throw daoException;
        }
        // resultaat
        return nbProduits;
    }
  • regel 18: de volgorde van SQL en [ConfigJdbc.V2_UPDATE_PRODUITS] is als volgt:

public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
  • regels 19-28: de code voor het bijwerken van de producten;

3.6.7.4. De methode [addProduits]

De methode [addProduits] voegt de producten die als parameters worden doorgegeven toe aan de database. Deze methode retourneert dezelfde producten met hun primaire sleutels (vóór het toevoegen aan de database hebben de producten geen primaire sleutel).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // verbindingsbronnen
        PreparedStatement ps = null;
        Connection connexion = null;
        // in eerste instantie geen uitzondering
        DaoException daoException = null;
        try {
            // verbinding openen
            connexion = dataSource.getConnection();
            // in lees-/schrijfmodus
            connexion.setReadOnly(false);
            // transactie gestart
            connexion.setAutoCommit(false);
            // elementen worden toegevoegd aan de tabel [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // parameters
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // opdracht uitvoeren
                ps.executeUpdate();
                // primaire sleutel gegenereerd
                ResultSet generatedKeys = ps.getGeneratedKeys();
                if (generatedKeys.next()) {
                    produit.setId(generatedKeys.getInt(1));
                } else {
                    throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
                            produit.getNom()));
                }
            }
            // transactie vastleggen
            connexion.commit();
            // terug naar standaardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // de uitzondering wordt afgehandeld
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // de finally-blok wordt verwerkt
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // uitzondering?
        if (daoException != null) {
            throw daoException;
        }
        // resultaat
        return produits;
}
  • regel 16, de volgorde SQL [ConfigJdbc.V2_INSERT_PRODUITS] is als volgt:

public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";

Hierboven bevat de opdracht voor het invoegen van een product de primaire sleutel [ID] niet. Aangezien de primaire sleutel van de database MySQL het attribuut [AUTOINCREMENT] heeft, zal SGBD vervolgens bij elke invoeging een primaire sleutel genereren. Het probleem is dan om deze sleutel op te halen. Dit is een belangrijk punt, omdat bewerkingen op producten plaatsvinden via hun primaire sleutels. Het is dus noodzakelijk om deze te kennen;

  • regels 17-33: de lus voor het invoegen van producten;
  • regel 16: een specifieke variant van de methode [prepareStatement]. De tweede parameter [generatedColumns] is een array met kolomnamen waarvan we de waarden na het invoegen willen ophalen. In regel 16 hebben we aangegeven dat we de waarde van de kolom [id] willen ophalen. Hierbij moet worden opgemerkt dat, hoewel de kolomnamen van een tabel hoofdlettergevoelig zijn (hoofdletters/kleine letters), de SGBD PostgreSQL vereiste dat deze naam in kleine letters werd geschreven. Dit is typisch het soort problemen dat men tegenkomt bij het overzetten van code van het ene SGBD naar het andere;
  • regel 24: invoegen van een regel in de database;
  • regel 26: we halen de lijst met waarden op van de kolommen die in regel 16 van een [ResultSet] zijn gespecificeerd. Hier zal de [ResultSet] voor 1 invoeging 1 regel bevatten en zal deze regel 1 enkele kolom bevatten die de primaire sleutel bevat;
  • regel 28: de door de SGBD gegenereerde primaire sleutel wordt opgehaald;
  • regels 29-32: als de gegenereerde primaire sleutel niet wordt verkregen, wordt een [RuntimeException] gestart, die wordt ingekapseld in een [DaoException] (regels 38-40);

3.6.8. De klasse [Dao2]

  

De klasse [Dao2] is een variant van de klasse [Dao1] die gebruikmaakt van een syntaxis die ‘try-with-resource(resource)’ wordt genoemd:

1
2
3
4
try(resource){
...
}
...
  • [resource] is een resource die de interface [java.lang.AutoCloseable] implementeert. Alle resources die met de methode [close] worden vrijgegeven, maken hier deel van uit. Deze syntaxis zorgt ervoor dat in regel 4 de resource [resource] wordt gesloten. Hierdoor hoeft er geen [finally]-clausule te worden geschreven om deze sluiting uit te voeren;

Laten we als voorbeeld de methode [getAllProduits] van de klasse [Dao2] nemen:


    @Override
    public List<Produit> getAllProduits() {
        // eventuele uitzondering
        DaoException daoException = null;
        // productlijst
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // begin transactie
            connexion.setAutoCommit(false);
            // in alleen-lezenmodus
            connexion.setReadOnly(true);
            // de tabel [PRODUITS] wordt gelezen
            try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
                    }
                }
                // einde transactie
                connexion.commit();
                // terug naar de standaardmodus
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // de transactie wordt geannuleerd
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // de uitzondering wordt verwerkt
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // uitzondering?
        if (daoException != null) {
            throw daoException;
        }
        // resultaat
        return produits;
}
  • regel 7: try met de resource [Connection]. Op regel 27 weten we zeker dat deze is gesloten;
  • regel 13: try met de resource [PreparedStatement]. Op regel 23 is gegarandeerd dat deze is gesloten;
  • regel 14: try met de resource [ResultSet]. Op regel 19 is gegarandeerd dat deze is gesloten;
  • regel 25: de transactie wordt als volgt geannuleerd:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // genereren van de uitzondering
        return new DaoException(code, e1, simpleClassName);
}

Uiteindelijk hebben we een code die eenvoudiger te lezen is.

3.6.9. Implementatie van de testlaag

3.6.9.1. De testklassen

  
  • de test [JUnitTestDao1] is een test JUnit van de klasse [Dao1];
  • de test [JUnitTestDao2] is een test JUnit van de klasse [Dao2];
  • [AbstractJUnitTestDao] is de bovenliggende klasse van de twee voorgaande testklassen;
  • [MainTestDao1] is een testconsoleklasse van de klasse [Dao1];
  • [MainTestDao2] is een testconsoleklasse van de klasse [Dao2];
  • [AbstractMainTestDao] is de bovenliggende klasse van de twee voorgaande klassen. Deze klasse bevat de code van de reeds besproken consoleklassen [IntroJdbc01, IntroJdbc02], dus we zullen deze consoleklassen niet nader bekijken;

De klasse [JUnitTestDao1] is als volgt:


package spring.jdbc.tests;

import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao1 extends AbstractJUnitTestDao {

    // laag [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;

    @Override
    IDao getDao() {
        return dao;
    }

}
  • De annotaties op de regels 12-13 zijn besproken in paragraaf 2.5.5. Ze zorgen ervoor dat een JUnit-test eenvoudig toegang heeft tot de Spring-context en de bijbehorende beans. Deze context wordt geconfigureerd door de klasse [AppConfig] (regel 12), die in paragraaf 2.4.3 is besproken;
  • regel 14: de klasse is een uitbreiding van de klasse [AbstractJUnitTestDao], die we straks zullen bespreken. In deze klasse bevinden zich de testmethoden JUnit;
  • regels 17-19: de bean met de naam [dao1] (regel 18) wordt geïnjecteerd (regel 17). Het is dus een instantie van de klasse [Dao1] die hier wordt geïnjecteerd;
  • regels 21-24: de methode [getDao] herdefinieert de gelijknamige methode in de bovenliggende klasse;

Uiteindelijk is het doel van deze klasse om aan de bovenliggende klasse een verwijzing te geven naar de laag [DAO] die getest moet worden, in dit geval een instantie van [Dao1]. Op dezelfde manier geeft de klasse [JUnitTestDao2] aan de bovenliggende klasse [AbstractJUnitTestDao] een instantie van de klasse [Dao2] terug.

De klasse [AbstractJUnitTestDao] is een testklasse voor JUnit:


package spring.jdbc.tests;

import generic.jdbc.entities.dbproduits.Produit;

import java.util.ArrayList;
import java.util.List;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;

import spring.jdbc.dao.IDao;
import spring.jdbc.infrastructure.DaoException;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public abstract class AbstractJUnitTestDao {

    // laag [DAO]
    abstract IDao getDao();

    // toewijzing jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();

    @Before
    public void clean() {
        // de basis wordt voor elke test schoongemaakt
        log("Vidage de la base de données", 1);
        getDao().deleteAllProduits();
    }

    @Test
    public void getProduits() throws JsonProcessingException {
    ...
    }

    @Test
    public void getProduitBy() {
    ...
    }

    @Test
    public void doInsertsInTransaction() {
...
    }

    @Test
    public void updateProduits() {
    ...
    }

    @Test
    public void deleteProduits() {
    ....
    }

    @Test
    public void perf1() {
        ...
    }

    @Test
    public void perf2() {
    ...
    }

    @Test
    public void perf3() {
    ....
    }

    // -------------- privémethoden
...
}
  • regel 19: de klasse [AbstractJUnitTestDao] is abstract;
  • regel 22: de abstracte methode [getDao] waarmee de referentie naar de te testen laag [DAO] kan worden verkregen. Deze methode wordt geïmplementeerd door de onderliggende klassen;
  • regel 25: een mapper jSON waarmee we de waarde jSON van producten op de console kunnen weergeven;
  • regels 27-32: vóór elke test (regel 27) wordt de tabel [PRODUITS] geleegd;

3.6.9.2. De privémethode [fill]

De privémethode [fill] wordt gebruikt om producten in de tabel [PRODUITS] op te nemen.


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // er wordt een productlijst aangemaakt
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String naam, int categorie, double prijs, String beschrijving
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // we slaan deze op in de database - we halen producten op met hun primaire sleutel
        produits = getDao().addProduits(produits);
        // we maken een woordenboek van de producten aan om ze gemakkelijker terug te kunnen vinden
        // de sleutel van het woordenboek is de primaire sleutel van het product in de database
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // we geven de producten weer
        return produits;
    }
  • regel 1: de methode [fill] voegt [nbProduits] in de tabel [PRODUITS] in, waarvan wordt aangenomen dat deze leeg is;
  • regels 3-10: aanmaken van een productlijst in de vorm:

new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format("DESC%s", n)));

die gebruikmaakt van de constructor Product(int id, String naam, int categorie, double prijs, String beschrijving). De waarde van de eerste parameter [id] (primaire sleutel van de tabel [PRODUITS]) is niet van belang, aangezien de methode [addProduits] in regel 10 deze niet in de database invoert en de waarde door SGBD laat genereren;

  • regel 12: de productlijst wordt in de database opgeslagen. Elk product in deze lijst wordt voorzien van een nieuwe primaire sleutel [id]. De methode [addProduits] retourneert zijn parameter [produits] als resultaat. We hadden het resultaat dus ook niet hoeven ophalen;
  • regels 15-17: we plaatsen de producten in een woordenboek:

    // productenlijst
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

De sleutel van het woordenboek is de primaire sleutel van het product en de bijbehorende waarde is het product zelf;

  • regel 19: de lijst met producten wordt geretourneerd;

3.6.9.3. De test [getProduits]

Deze luidt als volgt:


    @Test
    public void getProduits() throws JsonProcessingException {
        // invullen
        fill(10);
        // productlijst
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // we controleren of de opgehaalde lijst en de opgeslagen lijst hetzelfde zijn
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // alle oorspronkelijke producten moeten uit het woordenboek zijn verdwenen
        Assert.assertEquals(0, mapProduits.size());
}
}
  • regel 4: er worden 10 producten in de database ingevoerd;
  • regel 7: daarna vragen we om alle producten uit de database te tonen;
  • regel 8: deze worden weergegeven. Het doel is om te controleren of de producten daadwerkelijk zijn opgeslagen en of ze een primaire sleutel hebben;
  • regels 10-13: er wordt gecontroleerd of de gevonden producten identiek zijn aan de producten die zijn opgeslagen en die terug te vinden zijn in het woordenboek [mapProduits];
  • regel 11: we halen uit het woordenboek het product op met dezelfde primaire sleutel als het product dat uit de database is opgehaald. Dit toont aan dat de opgeslagen producten inderdaad een primaire sleutel hebben gekregen;
  • regel 12: we controleren of de twee producten identiek zijn. Ter herinnering: de klasse [Produit] heeft een methode [equals] gedefinieerd (zie paragraaf 3.3.4);
  • regel 13: het gevonden element wordt uit het woordenboek verwijderd;
  • regel 16: er wordt gecontroleerd of het woordenboek met de oorspronkelijke producten inderdaad leeg is, wat betekent dat deze oorspronkelijke producten allemaal aanwezig waren in de lijst met producten die uit de database zijn opgehaald;

De methode [affiche] op regel 8 is de volgende privémethode:


    // productlijst weergeven
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • regel 2: de methode [affiche] is een generieke methode. Deze wordt geparametriseerd door een type T, syntactisch aangeduid als <T>. Als deze zou worden geparametriseerd door twee typen, T1 en T2, zou men <T1,T2> schrijven. De syntaxis van een methode m die wordt geparametriseerd door een type T is als volgt:
portée <T> type_résultat m(... , T value1, ...){
...
    T value2=...
}

In de code van methode m komen gegevens van het type T voor. De methode m van een instantie c van een klasse C kan dan als volgt worden aangeroepen:

type_résultat r=c.<T1>m(..., T1 value1, ..) ;

waarbij T1 het effectieve type is dat het formele type T van de methode m vervangt. Meestal kan de compiler het type T1 afleiden uit de argumenten van de methode m. Daarom wordt de bovenstaande instructie meestal vereenvoudigd tot:

type_résultat r=c.m(..., T1 value1, ..) ;

Laten we terugkeren naar de methode [affiche]. Deze geeft een lijst met elementen van het type T weer. Dit is mogelijk omdat de mapper jSON die in regel 4 wordt gebruikt, in staat is om de weergave jSON van elk type object weer te geven. In dit specifieke voorbeeld is het enige gebruikte type T het type [Produit].

De methode [affiche] had ook als volgt geschreven kunnen worden:


    // productlijst weergeven
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

Aangezien de daadwerkelijke parameter een lijst met producten is, zou regel 3 de weergave jSON van deze lijst hebben geschreven. Dit is niet hetzelfde als het één voor één schrijven van de weergave van elk van de elementen.

De weergave die door de test [getProduits] wordt gegenereerd, is als volgt:

-- Liste des produits
{"id":150189,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":150190,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":150191,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":150192,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":150193,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":150194,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":150195,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":150196,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":150197,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":150198,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}

3.6.9.4. De test [getProduitBy]

Deze ziet er als volgt uit:


    @Test
    public void getProduitBy() {
        // invullen
        fill(10);
        log("getProduitBy", 1);
        Produit produit = getDao().getProduitByName("NOM3");
        Produit produit2 = getDao().getProduitById(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
}
  • regel 6: de methode [getProduitByName] van de interface [IDao] wordt gebruikt om het product met de naam [NOM3] op te halen;
  • regel 7: vervolgens wordt de methode [getProduitById] van de interface [IDao] gebruikt om hetzelfde product op te halen, ditmaal geïdentificeerd aan de hand van de primaire sleutel;
  • regels 8-10: er wordt gecontroleerd of [produit2] en [produit] dezelfde kenmerken hebben;

3.6.9.5. De test [doInsertsInTransaction]

Deze test is als volgt:


    @Test
    public void doInsertsInTransaction() {
        log("Ajout de deux produits de même nom", 1);
        // we voeren de invoeging uit
        List<Produit> inserts = new ArrayList<Produit>();
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        boolean erreur = false;
        try {
            getDao().addProduits(inserts);
        } catch (DaoException daoException) {
            erreur = true;
        }
        // controles
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • regels 5-7: er wordt een lijst aangemaakt met twee producten met dezelfde naam [x];
  • regel 10: deze twee producten worden ingevoegd in de tabel [PRODUITS], die leeg is (methode [clean] geannoteerd met [@Before]). De eerste invoeging zal plaatsvinden, maar de tweede niet, omdat de tabel [PRODUITS] een uniekheidsbeperking heeft op de productnamen. Er moet dus een uitzondering optreden. Deze wordt getest in regel 15;
  • omdat alle methoden van de interface [IDao] binnen een transactie worden uitgevoerd, zal het mislukken van de tweede invoeging ertoe leiden dat de gehele transactie – en dus ook de eerste invoeging – wordt teruggedraaid. Uiteindelijk mag er geen enkele invoeging plaatsvinden in de tabel [PRODUITS];
  • regels 16-17: dit wordt gecontroleerd door de lijst met producten in de tabel [PRODUITS] op te vragen en te controleren of deze lijst leeg is;

3.6.9.6. De test [updateProduits]

Deze test is als volgt:


    @Test
    public void updateProduits() {
        // invullen
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // producten ophalen
        List<Produit> produits = getDao().getAllProduits();
        // die van categorie 1 bijwerken
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String naam, int categorie, double prijs, String beschrijving
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // controles
        // Assert.assertEquals(nbUpdated, nbProduits); -- werkt niet met DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • regel 4: er worden 10 producten in de database geplaatst;
  • regel 7: we halen ze eruit;
  • regels 9-18: de prijzen van de producten in categorie nr. 1 worden met 10% verhoogd;
  • regel 19: deze wijzigingen worden in de database opgeslagen;
  • regels 22-25: de lijst met producten die voor de update is gebruikt, wordt in het geheugen doorlopen. Voor elk product wordt in de database het product met dezelfde primaire sleutel opgezocht en wordt gecontroleerd of de prijs daadwerkelijk is bijgewerkt;
  • regel 19: het aantal producten wordt opgehaald dat door de bewerking [updateProduits] is bijgewerkt;
  • regel 21: er wordt gecontroleerd of dit aantal inderdaad overeenkomt met het verwachte aantal. Deze controle slaagt voor alle SGBD-bewerkingen, behalve voor de SGBD en DB2. Deze zijn daarom uitgecommentarieerd;

3.6.9.7. De test [deleteProduits]

Deze luidt als volgt:


    @Test
    public void deleteProduits() {
        // invullen
        fill(10);
        log("deleteProduits", 1);
        // productlijst
        List<Produit> produits = getDao().getAllProduits();
        // twee producten verwijderen
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // controles
        // Assert.assertEquals(2, nbDeleted); -- werkt niet met DB2
        Assert.assertNull(getDao().getProduitById(produit0.getId()));
        Assert.assertNull(getDao().getProduitById(produit5.getId()));
        Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
  • regel 4: we voegen 10 producten toe aan de database;
  • regels 7-11: we halen alle producten uit de database en verwijderen daaruit de producten op posities 0 en 5;
  • regels 14-16: we controleren of de twee producten niet meer in de database staan en of de database nu twee producten minder bevat;
  • de test van regel 13 slaagt niet met de SGBD DB2. Hij slaagt wel met de andere SGBD;

3.6.9.8. De prestatietests

We hebben drie methoden in de tests opgenomen die uitsluitend bedoeld zijn om de prestaties van de SGBD te beoordelen:


    @Test
    public void perf1() {
        // invullen
        fill(10000);
    }

    @Test
    public void perf2() {
        // invullen
        fill(10000);
        // wijziging
        List<Produit> produits = getDao().getAllProduits();
        // de items van categorie 1 worden bijgewerkt
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String naam, int categorie, double prijs, String beschrijving
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }

    @Test
    public void perf3() {
        // invullen
        fill(10000);
        // verwijderen
        List<Produit> produits = getDao().getAllProduits();
        // primaire sleutels
        int[] keys = new int[produits.size()];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = produits.get(i).getId();
        }
        getDao().deleteProduits(keys);
}
  • regels 1-5: invoeren van 10.000 producten;
  • regels 8-20: invoeren van 10.000 producten en deze vervolgens wijzigen via hun primaire sleutels;
  • regels 23-34: invoeren van 10.000 producten en deze vervolgens verwijderen via hun primaire sleutels;

Om de tests [JUnitTestDao1] en [JUnitTestDao2] uit te voeren, kunnen de volgende uitvoeringsconfiguraties worden gebruikt:

De resultaten van de test [JUnitTestDao1] zijn als volgt:

In [1] zijn de resultaten van [JUnitTestDao1] en in [2] die van [JUnitTestDao2]. Er zijn geen significante verschillen tussen beide. In [1]:

  • de test is geslaagd;
  • het invoeren van 10.000 producten duurt 3,15 seconden;
  • het toevoegen van 10.000 producten, gevolgd door het wijzigen ervan, duurt 4,80 seconden;
  • het invoeren van 10.000 producten, gevolgd door het verwijderen ervan, duurt 4,40 seconden;
  • het invoegen kost dus de meeste tijd;