Skip to content

3. Wprowadzenie do API JDBC

3.1. Konfiguracja środowiska pracy

Będziemy pracować z bazą danych MySQL5.

Należy posiadać:

  • zainstalować JDK (Java Development Kit) (punkt 23.1);
  • zainstalowany menedżer zależności Maven (punkt 23.2);
  • zainstalować pakiet IDE Spring Tool Suite (STS) (punkt 23.3);
  • zainstalowano SGBD MySQL5 (punkt 23.4) oraz jego klienta EMS MyManager (punkt 23.5);
  • pobrano kody z dokumentu [http://tahe.developpez.com/java/spring-database];

W dalszej części zakładamy, że administrator serwera MySQL5 jest użytkownikiem root i posiada hasło root. Uruchom serwer SGBD, MySQL5 oraz jego klienta [MyManager]. Za pomocą [MyManager] tworzymy bazę danych [dbproduits] [1-34]:

  • w [3] baza danych musi nosić nazwę [dbproduits];
  • na [8-9], jako root z hasłem root (czego nie widać na powyższym zrzucie ekranu);
  • na [14a], hasło to ponownie root (czego nie widać na zrzucie ekranu);
  • w [15] utworzono bazę danych o nazwie [dbproduits];
  • w [20] należy zwrócić uwagę na wybraną bazę. Musi to być baza [dbproduits];
  • na [22], katalog to <przykłady>/spring-database-config/mysql/databases, gdzie <przykłady> to katalog pobranych przykładów;
  • w [23] wybierz skrypt SQL [dbproduits.sql]. Spowoduje to wygenerowanie tabeli [PRODUITS] w bazie [dbproduits];
  • w [30] utworzono tabelę [produits];
  • w [33], kolumny tabeli [produits];
  • do [34], która początkowo jest pusta;

Teraz, korzystając z STS, zaimportuj następujące projekty (postępuj zgodnie z procedurą zastosowaną w przypadku projektów z folderu <exemples>/spring-core):

  • do [2], projekt [mysql-config-jdbc] zostanie znaleziony w folderze [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];

Projekt ten konfiguruje warstwę JDBC w poniższej architekturze:

Następnie należy ponownie zaimportować trzy poniższe projekty:

  • do [2]; projekty te znajdują się w folderze [<exemples>/spring-database-config/spring-jdbc] [1];

Te trzy projekty to projekty Maven, które wykorzystują projekt Maven o nazwie [mysql-config-jdbc]. Ten ostatni projekt generuje następujący artefakt Maven (patrz pom.xml):


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

Ten sam artefakt zostanie wygenerowany przez projekt [oracle-config-jdbc, db2-config-jdbc, ...]. Aby upewnić się, że projekty [spring-generic-jdbc-*] aktualnie załadowane do projektu STS faktycznie korzystają z projektu [mysql-config-jdbc]:

  • upewnij się, że inny projekt o nazwie [sgbd-config-jdbc] nie jest załadowany w tym samym czasie. Mogłoby to spowodować trudne do zrozumienia błędy;
  • zaktualizuj konfigurację Maven załadowanych projektów w następujący sposób:

Aby sprawdzić konfigurację, uruchom konfigurację uruchomieniową [spring-jdbc-generic-01.IntroJdbc01] [1-3]:

Powinieneś otrzymać następujące wyniki w konsoli:

------------------------------ 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é

W poniższych przykładach czytelnik będzie mógł:

  • albo pracować bezpośrednio z wcześniej załadowanymi projektami;
  • albo samodzielnie utworzyć projekty;

3.2. Etapy obsługi bazy danych

W powyższej architekturze obsługa bazy danych przez program konsolowy obejmuje następujące etapy:

  1. załadowanie sterownika bazy danych JDBC;
  1. nawiązanie połączenia z bazą;
  2. wysłanie polecenia SQL do bazy danych oraz przetworzenie wyników polecenia SQL;
  3. zamknięcie połączenia;

Krok 1 wykonuje się tylko raz. Kroki 2–4 są wykonywane wielokrotnie. Należy zauważyć, że nie pozostawia się otwartego połączenia. Zamyka się je, gdy tylko przestaje być potrzebne.

3.2.1. krok 1 – załadowanie sterownika JDBC do pamięci

Kod


        // ładowanie sterownika JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // obsługa wyjątku
}

Operacja w wierszu 3 ma na celu załadowanie do pamięci sterownika JDBC z bazy danych. Operację tę należy wykonać tylko raz. Jej powtórzenie nie powoduje jednak błędu. Klasa sterownika JDBC jest wyszukiwana w ścieżce Classpath projektu. W związku z tym w projekcie Eclipse plik [jar] zawierający klasę sterownika JDBC musi zostać dodany do ścieżki Classpath projektu.

3.2.2. Krok 2 – nawiązanie połączenia

Po zainstalowaniu sterownika JDBC należy polecić mu otwarcie połączenia ze sterownikiem BD:

Kod


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 {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // obsługa wyjątku
            ...
        } finally {
         // zamknięcie połączenia
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // obsługa wyjątku
                ...
            }
         }
}
  • wiersze 3–7: klasy implementujące interfejs JDBC znajdują się w pakiecie [java.sql]. Ponadto w przypadku błędu wszystkie one generują wyjątek typu [SQLException] (wiersze 19, 27). Wyjątek ten pochodzi od klasy [Exception] i jest tzw. wyjątkiem kontrolowanym: konieczne jest zastosowanie bloku try/catch w celu jego obsługi lub, alternatywnie, rezygnacja z obsługi i wskazanie, że metoda pozwala na wystąpienie wyjątku poprzez uzupełnienie sygnatury metody o [throws SQLException];
  • wiersz 17, [DriverManager.getConnection] to metoda statyczna, która oczekuje trzech parametrów:
    • [url]: wartość URL z bazy danych. Jest to ciąg znaków zależny od użytej wartości BD. W przypadku MySQL ma ona postać [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: właściciel połączenia;
    • [passwd]: jego hasło;
  • wiersze 24–30: połączenie musi zostać zamknięte w klauzuli [finally], tak aby zostało zamknięte niezależnie od tego, czy wystąpi wyjątek, czy nie.

3.2.3. Krok 3 – wysyłanie poleceń SQL i [SELECT]

Po nawiązaniu połączenia można wysyłać polecenia SQL. Sposób obsługi poleceń odczytu [SELECT] różni się od sposobu stosowanego w przypadku operacji aktualizacji [UPDATE, INSERT, DELETE]. Zaczynamy od poleceń SQL i [SELECT]:

Kod


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie tylko do odczytu
            connexion.setReadOnly(true);
            // odczyt tabeli [PRODUITS]
            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)));
            }
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
             doCatchException(connexion,e1);
        } finally {
            // obsługa bloku finally
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • wiersze 8, 10: otwarcie transakcji (wiersz 8) w trybie tylko do odczytu (wiersz 10). Transakcja to sekwencja poleceń SQL, które albo wszystkie zakończą się powodzeniem, albo wszystkie zakończą się niepowodzeniem. Zatem w transakcji zawierającej N poleceń SQL, jeśli polecenie I+1 zakończy się niepowodzeniem, wówczas poprzednie I polecenia zostaną anulowane. W przypadku operacji odczytu transakcja nie jest konieczna. Niemniej jednak utworzenie transakcji tylko do odczytu może umożliwić niektórym poleceniom SGBD przeprowadzenie pewnych optymalizacji;
  • wiersz 12: użycie polecenia [PreparedStatement]. Polecenie [PreparedStatement] zazwyczaj posiada parametry oznaczone znakiem ?. Tutaj ich nie ma. [PreparedStatement] to polecenie przygotowane przez SGBD. Przygotowanie to wiąże się z kosztem i odbywa się tylko raz. Następnie to przygotowane polecenie jest wykonywane przez SGBD z różnymi parametrami rzeczywistymi, które zastąpią parametry formalne „?”. Należy zauważyć, że lepiej jest podać nazwy żądanych kolumn niż używać symbolu * w celu uzyskania wszystkich kolumn. Określając nazwy kolumn, można następnie uzyskać ich wartości na podstawie ich pozycji w zapytaniu SELECT;
  • wiersz 13: wykonanie zapytania [PreparedStatement]. Pobieramy obiekt typu [ResultSet];

Obiekt typu [ResultSet] reprezentuje tabelę, czyli zbiór wierszy i kolumn. W danym momencie mamy dostęp tylko do jednego wiersza tabeli, zwanego wierszem bieżącym. Podczas początkowego tworzenia obiektu [ResultSet] nie ma wiersza bieżącego. Aby ją uzyskać, należy wykonać operację [ResultSet.next()]. Sygnatura metody next jest następująca:

    boolean next()

Metoda ta próbuje przejść do następnego wiersza w [ResultSet] i zwraca true w przypadku powodzenia, a false w przeciwnym razie. W przypadku powodzenia następny wiersz staje się nowym wierszem bieżącym. Poprzedni wiersz zostaje utracony i nie ma możliwości cofnięcia się, aby go odzyskać.

Tabela o nazwie [ResultSet] zawiera kolumny o nazwach labelCol1, labelCol2, ... określone w wykonanym zapytaniu [SELECT]. Przy użyciu zapytania:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • kolumna [ID] trafi do kolumny w tabeli [ResultSet] o nazwie [myId];
  • kolumna [NOM] zostanie przeniesiona do kolumny w tabeli [ResultSet] o nazwie [myNom];
  • ...

W powyższym przykładzie identyfikatory [myCol] nazywane są etykietami kolumn. W przypadku braku tych etykiet nazwy kolumn w tabeli [ResultSet] są zależne od tabeli SGBD. Gdy [SELECT] działa na jednej tabeli, etykiety kolumn będą domyślnie nazwami kolumn żądanych przez SELECT. Problem pojawia się, gdy [SELECT] działa na wielu tabelach, a w tych tabelach występują identyczne nazwy kolumn, jak w poniższym przykładzie:

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

zakładając, że tabela [PRODUITS] posiada klucz obcy do tabeli [CATEGORIES], symbolizowany przez relację [Produits].CATEGORIE_ID --> [CATEGORIES].ID, a tabele [PRODUITS] i [CATEGORIES] posiadają obie pole [NOM]. W tym przypadku nazwy nadane w tabeli [ResultSet] kolumnom [PRODUITS.NOM] i [CATEGORIES.NOM] są zależne od tabeli SGBD. Aby zapewnić przenoszalność między plikami SGBD, należy zatem użyć tutaj etykiet kolumn i zapisać:


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

Aby wykorzystać różne pola bieżącego wiersza pliku [ResultSet], dostępne są następujące metody:

Type getType("labelColi") 

aby uzyskać kolumnę o nazwie „labelColi” z bieżącego wiersza, a tym samym kolumnę z pliku [SELECT] posiadającą tę etykietę. Type określa typ pola coli. Można użyć następujących metod [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Zamiast nazwy kolumny można użyć jej pozycji w wykonanym zapytaniu [SELECT]:

Type getType(i) 

gdzie i to indeks żądanej kolumny (i>=1).

  • wiersze 15–17: pobieranie wartości odczytanych w zapytaniu BD;
  • wiersz 19: transakcja zostaje zatwierdzona (mówi się również o jej „commit”); powoduje to jej zakończenie i zwolnienie zasobów, które transakcja SGBD dla niej zablokowała;
  • wiersz 25: zasoby są zwalniane w transakcji [finally]. Ta z kolei wywołuje następującą metodę [doFinally]:

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

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

            }
        }
        if (connexion != null) {
            try {
                // zamknięcie połączenia
                connexion.close();
            } catch (SQLException e3) {
                 // obsługa wyjątku
            }
        }
    }
  • wiersze 3–9: zamknięcie obiektu [ResultSet];
  • wiersze 11–17: zamknięcie [PreparedStatement];
  • wiersze 18–27: zamknięcie połączenia;

Zamknięcia w wierszach 3–17 wydają się zbędne, ponieważ połączenie zamyka się w wierszach 18–25. W rzeczywistości w niektórych przypadkach nie są one zbędne i zaleca się pozostawienie ich w pliku [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • wiersz 22: wyjątek jest obsługiwany przez następującą metodę [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // anulowanie transakcji
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // obsługa wyjątku
        }
}
  • wiersze 4–6: transakcja zostaje anulowana. To ją kończy, a metoda SGBD będzie mogła zwolnić zasoby przydzielone do jej realizacji;

3.2.4. etap 3 – wysyłanie poleceń SQL i [INSERT, UPDATE, DELETE]

Polecenia SQL i [INSERT, UPDATE, DELETE] są operacjami aktualizującymi: modyfikują bazę danych, ale nie zwracają żadnych wierszy. Jedyną zwracaną informacją jest liczba wierszy, których dotyczy operacja aktualizacji.

Kod


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(url, user, passwd);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // aktualizacja tabeli
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // kategoria 1
            ps.setInt(1, 10);
            // wykonanie
            int nbLignes=ps.executeUpdate();
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException(connexion, e1);
        } finally {
            // przetwarzanie bloku finally
            doFinally(null, ps, connexion);
        }
    }
  • wiersz 9: połączenie jest wykorzystywane do odczytu i zapisu;
  • wiersz 11: operacja [PreparedStatement] z jednym parametrem (oznaczonym symbolem ?). Parametrów może być kilka. Są one numerowane od 1;
  • wiersz 13: przypisuje się wartość do jedynego parametru. Pierwszy parametr [setType] to pozycja parametru w [PreparedStatement] (1, 2, ...), a drugi to przypisana mu wartość. Można użyć metod [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • wiersz 15: stosuje się metodę [executeUpdate], a nie [executeQuery], która jest zarezerwowana dla poleceń SELECT. Metoda zwraca liczbę wierszy, których dotyczy operacja. Może wynosić 0.
  • wiersz 17: transakcja została zatwierdzona;

3.2.5. krok 4 – zamknięcie połączenia

W środowisku wieloużytkownikowym połączenie należy zamknąć jak najszybciej, ponieważ transakcja SGBD dopuszcza ograniczoną liczbę otwartych połączeń. W poprzednich przykładach połączenie było zamykane w klauzuli [finally] operacji SQL, tak aby zostało zamknięte niezależnie od tego, czy wystąpił wyjątek, czy nie.

3.3. Konfiguracja warstwy JDBC w ramach SGBD MySQL5

Przeanalizujemy projekt [mysql-config-jdbc], który konfiguruje poniższą warstwę JDBC:

3.3.1. Projekt Eclipse

 

3.3.2. Konfiguracja Maven

Plik [pom.xml] w projekcie wygląda następująco:


<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>
        <!-- zależności zmiennych ********************************************** -->
        <!-- sterownik JDBC dla SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- stałe zależności ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- biblioteka 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>
        <!-- Test Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- logi -->
        <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>

W tej konfiguracji Maven zgromadzono szereg archiwów niezbędnych albo dla projektu [mysql-config-jdbc], albo dla projektów, które będą się na nim opierać:

  • wiersze 4–6: artefakt Maven wygenerowany przez projekt. Jak już wspomniano, wszystkie projekty typu [*-config-jdbc] generują ten sam artefakt. Nie należy zatem ładować jednocześnie dwóch projektów typu [*-config-jdbc];
  • wiersze 9–13: nadrzędny projekt Maven. Określa on wersje wielu bibliotek wykorzystywanych przez ekosystem Spring. Dzięki temu nie trzeba ich określać w projektach pochodnych;
  • wiersze 18–21: archiwum sterownika JDBC dla projektów SGBD i MySQL5. Jest to jedyne archiwum niezbędne dla projektu [spring-jdbc-01];
  • wiersze 24–27: artefakt [tomcat-jdbc] zawiera archiwum niezbędne dla projektów JDBC i [spring-jdbc-02 à 04];
  • wiersze 29–36: zawierają biblioteki niezbędne do obsługi projektu jSON. Wykorzystywane są one w niemal wszystkich projektach wymienionych w dokumencie;
  • wiersze 38–42: Google Guava to biblioteka do zarządzania kolekcjami. Wykorzystywana w niemal wszystkich projektach zawartych w dokumencie;
  • wiersze 43–52: biblioteki umożliwiające pisanie testów z wykorzystaniem Spring i JUnit. Wykorzystywane w niemal wszystkich projektach w niniejszym dokumencie;
  • wiersze 54–57: biblioteki do logowania. Wykorzystywane w niemal wszystkich projektach opisanych w niniejszym dokumencie;
  • wiersze 67–71: wtyczka umożliwiająca zainstalowanie artefaktu projektu [mysql-config-jdbc] w lokalnym repozytorium Maven;

3.3.3. Klasa konfiguracyjna [ConfigJdbc]

  

Klasa [ConfigJdbc] ma następującą postać:


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 {

    // parametry połączenia
    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";
...
    // polecenia 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')";

    // polecenia 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=?";

...

}

Klasa [ConfigJdbc] służy do konfiguracji warstwy JDBC czterech projektów [spring-jdbc-01 à 04]. Większa część konfiguracji dotyczy projektu [spring-jdbc-04]. Omówimy tę część podczas analizy tego projektu. Powyżej uwzględniono jedynie konfigurację projektów [spring-jdbc-01 à 03].

  • wiersze 14–17: parametry połączenia z bazą danych MySQL5 [dbproduits];
  • wiersze 20–25: polecenia SQL używane w projektach [spring-jdbc-01 et 02];
  • wiersze 28–34: polecenia SQL używane w projekcie [spring-jdbc-03];

Te polecenia SQL wykorzystują tabelę [PRODUITS] z bazy danych MySQL5 [dbproduits], której struktura jest następująca:

 
  • [ID]: klucz główny w trybie AUTO_INCREMENT (jeśli nie podano klucza głównego, SGBD generuje go);
  • [NOM]: nazwa produktu – unikalna;
  • [CATEGORIE]: numer kategorii;
  • [PRIX]: cena;
  • [DESCRIPTION]: opis produktu;

3.3.4. Klasa [Produit]

  

Klasa [Produit] to obraz wiersza z tabeli [PRODUITS]:


package generic.jdbc.entities.dbproduits;

public class Produit {

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

    // konstruktory
    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;
    }

    // metody pobierające i ustawiające
...
}

W dalszej kolejności będziemy musieli porównać dwa produkty, aby ustalić, czy są one identyczne, czy nie. Uznamy, że dwa produkty są identyczne, jeśli wszystkie ich pola są identyczne. W tym celu zdefiniujemy na nowo metodę [equals] klasy [Object], od której wywodzi się klasa [Produit]:


    // metoda równości
    @Override
    public boolean equals(Object o) {
        // proste przypadki
        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));
}
  • wiersz 3: metoda [equals] otrzymuje obiekt o, który musi porównać z obiektem this;
  • wiersze 5–7: proste przypadki, w których od razu można stwierdzić, że oba obiekty nie są równe. Metoda [Object].getClass() zwraca instancję typu [Class], który reprezentuje rzeczywistą klasę obiektu;
  • wiersz 8: obiekt o jest przekształcany w produkt p;
  • wiersz 9: jeśli oba odwołania o i p do produktu są równe, to fizycznie chodzi o ten sam produkt;
  • wiersz 9: jeśli o i p to dwa różne odwołania do dwóch produktów posiadających te same pola, uznamy je za równe. Ponieważ cena jest typu [double], a w informatyce nie ma dokładnego odwzorowania liczb rzeczywistych, uznamy dwie ceny za identyczne, jeśli różnią się o nie więcej niż 10⁻⁶;

Ponadto zdefiniujemy na nowo metodę [hasCode] klasy [Object]:


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

Wartości hashCode dla dwóch produktów muszą być takie same, jeśli metoda [equals] uznała te dwa produkty za równe. Ta wartość hashCode służy do przyporządkowywania obiektów do zbiorów, takich jak słowniki. W powyższym przykładzie, jeśli dwa produkty są identyczne, będą miały tę samą wartość hashCode.

3.3.5. Wyjątek [UncheckedException]

  

Rozważmy następującą architekturę:

  • warstwa [JDBC] generuje wyjątki typu [SQLException]. Wyjątek ten musi być przekazywany w górę przez kolejne warstwy, aż dotrze do najwyższej warstwy, w tym przypadku warstwy testowej;

Warstwa [DAO] mogłaby po prostu pozwolić, by warstwa [SQLException] przeszła aż do warstwy testowej. Jednak ponieważ ten wyjątek jest niekontrolowany (wynika bezpośrednio z [Exception]), oznaczałoby to, że interfejs [IDao] warstwy [DAO] wyglądałby następująco:


public interface IDao {

    // dodawanie produktów
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;

    // lista wszystkich produktów
    public List<Produit> getAllProduits() throws SQLException;

    // konkretny produkt
    public Produit getProduitById(int id) throws SQLException;

    public Produit getProduitByName(String name) throws SQLException;

    // aktualizacja wielu produktów
    public int updateProduits(List<Produit> produits) throws SQLException;

    // usunięcie wszystkich produktów
    public int deleteAllProduits() throws SQLException;

    // usunięcie wielu produktów
    public int deleteProduits(int[] ids) throws SQLException;
}

A to jest bardzo kłopotliwe, ponieważ uniemożliwia nam zaimplementowanie interfejsu [IDao] za pomocą klasy, która zgłaszałaby inny wyjątek. Aby obejść tę trudność, warstwa [DAO] będzie generować niekontrolowany wyjątek [DaoException] (pochodny od [RuntimeException]), co pozwala nam uniknąć klauzuli [throws] w sygnaturze metod interfejsu. Dzięki temu interfejs ten będzie mógł być zaimplementowany przez dowolną klasę, która również zgłasza niekontrolowany wyjątek, który może różnić się od wyjątku [DaoException]. Nasza architektura wygląda teraz następująco:

Aby ułatwić tworzenie niekontrolowanych wyjątków dla różnych warstw aplikacji, tworzymy dla nich klasę nadrzędną [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;

// ogólna klasa wyjątków
// wyjątek jest niekontrolowany

public class UncheckedException extends RuntimeException {

    // wygenerowany numer seryjny ID
    private static final long serialVersionUID = -2924871763340170310L;

    // właściwości
    private int code;
    private String trace;
    private List<ShortException> exceptions;

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

    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // lokalny
        this.code = code;
        this.exceptions = getErreursForException(e);
        // ślad
        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);
    }

    // lista komunikatów o błędach związanych z wyjątkiem
    private List<ShortException> getErreursForException(Throwable th) {
        // pobieranie elementów stosu wyjątku
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // pobieranie bieżącego wyjątku
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // następny wyjątek
            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;
        }
    }

    // metody pobierające i ustawiające
...
}
  • wiersz 12: klasa ta wywodzi się z [RuntimeException] i jest zatem typem wyjątku niekontrolowanego. Będzie ona służyć do enkapsulacji wyjątku kontrolowanego (SQLException) w typie wyjątku niekontrolowanego (UncheckedException);
  • aby odróżnić od siebie wyjątki typu [UncheckedException], można przypisać im kod, który zostanie zapisany w polu prywatnym w wierszu 18. Kod Java przechwytujący wyjątek typu [UncheckedException] będzie miał dostęp do tego kodu błędu dzięki metodzie [getCode] (wiersze 80 i kolejne);
  • wiersz 20: przechowuje komunikaty o błędach ze stosu wyjątku;
  • wiersze 23–43: różne sposoby tworzenia obiektu typu [UncheckedException];
  • wiersze 56–67: prywatna metoda umożliwiająca utworzenie listy błędów z wiersza 20 na podstawie obiektu typu [Throwable] lub pochodnego, w szczególności typu [Exception];
  • wiersze 69–78: metoda [toString] zwraca ciąg znaków reprezentujący wyjątek. Aby wyświetlić listę błędów z wiersza 20, wykorzystuje bibliotekę jSON. Znajduje się ona wśród zależności Maven projektu:

        <!-- biblioteka 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>
  • wiersze 45–48: redefiniują metodę [getMessage] klasy nadrzędnej [RuntimeException]. Metoda ta zwraca tutaj sygnaturę klasy [toString];
  • wiersze 50–53: redefiniują metodę [printStackTrace] klasy nadrzędnej [RuntimeException]. Wyświetlana będzie sygnatura klasy [toString];

Klasa [UncheckedException] zapisuje w polu w wierszu 20 listę wyjątków opisanych przez następujący typ [ShortException]:


package pam.dao.exceptions;

public class ShortException {

    // właściwości
    private String className;
    private String errorMessage;

    // konstruktory
    public ShortException() {

    }

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

    // metody pobierające i ustawiające
...
}
  • wiersz 6: nazwa klasy wyjątku, który wystąpił;
  • wiersz 7: powiązany komunikat o błędzie;

Przyjrzyjmy się następującemu konstruktorowi klasy [UncheckedException]:


    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // lokalne
        this.code = code;
        this.exceptions = getErreursForException(e);
        // ślad
        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;
            }
        }
}
  • wiersz 1, parametry są następujące:
    • [code]: kod błędu;
    • [e]: wyjątek, który jest enkapsulowany. [Throwable] jest klasą nadrzędną klasy [Exception] i wywodzi się bezpośrednio z klasy [Object]. Jest to klasa nadrzędna wszystkich klas C, z którymi można zapisać [throw c;], gdzie c jest instancją klasy C;
    • [simpleClassName]: prosta nazwa klasy kodu użytkownika, w której wykryto wyjątek e;
  • wiersz 4: rejestrowany jest kod błędu;
  • wiersz 5: lista [ShortException] jest tworzona na podstawie [Throwable e] przekazanego jako parametr;
  • wiersze 7–16: analizowane są tzw. ślady wyjątku. Początkowy wyjątek występuje w konkretnym miejscu kodu, a następnie jest śledzony wstecz do metody, która wywołała tę, w której wystąpił wyjątek, i tak dalej, aż do momentu, gdy zostanie zatrzymany przez blok try/catch. Podczas tego prześledzenia początkowy wyjątek pozostawia ślady zapisane w tablicy [e.stackTrace] wyjątku e. Są one tutaj pobierane w wierszu 8 na podstawie wartości [Throwable e] przekazanej jako parametr. Każdy element typu [StackTraceElement] jest obiektem posiadającym następujące pola:
    • [fileName]: nazwa pliku Java, w którym wystąpił wyjątek;
    • [lineNumber]: numer wiersza w tym pliku, w którym wystąpił wyjątek;
    • [methodName]: nazwa metody w tym pliku, w której wystąpił wyjątek;
  • wiersze 10–16 przeszukują tablicę śladów wyjątku przekazanego jako parametr w poszukiwaniu pierwszego wystąpienia warunku [trace.fileName==simpleClassName.java], gdzie [simpleClassName] jest trzecim parametrem konstruktora. Chodzi o to, aby zapamiętać, gdzie w kodzie użytkownika wystąpił wyjątek. Kod ten będzie enkapsulował wyjątek w następujący sposób:
1
2
3
4
5
6
7
try{
// kod, który może wywołać wyjątek kontrolowany
...
}catch(UnTypeDexception e){
// kontrolowany wyjątek e jest enkapsulowany w wyjątku niekontrolowanym
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • wiersz 13: tworzy się ciąg znaków typu [fileName, methodName, lineNumber], charakteryzujący miejsce w kodzie użytkownika, w którym wyjątek został zatrzymany;

Teraz przyjrzyjmy się kodowi, który rejestruje listę wyjątków ze stosu wyjątków wyjątku [Throwable th], zakapsułkowanego przez poprzedni konstruktor:


    // lista komunikatów o błędach związanych z wyjątkiem
    private List<ShortException> getErreursForException(Throwable th) {
        // pobieranie elementów stosu wyjątku
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // pobieranie bieżącego wyjątku
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // następny wyjątek
            cause = cause.getCause();
        }
        return exceptions;
}

W trakcie przechodzenia w górę do metody, która zatrzymała ją za pomocą bloku try/catch, początkowy wyjątek e mógł zostać enkapsulowany w innym wyjątku. To właśnie ten ostatni przechodzi w górę do metody, która ostatecznie go zatrzyma. On sam może zatem również ulec enkapsulacji. Ostatecznie, gdy metoda zdecyduje się zatrzymać wyjątek th i go wykorzystać, znajdzie początkowy wyjątek e ukryty na samym dnie stosu wyjątków. Tak więc w powyższym przykładzie parametr [Throwable th] stanowi jedynie widoczną część góry lodowej wyjątków. Jej atrybut [th.cause] pozwala zidentyfikować wyjątek, który ona sama enkapsuluje. I tak dalej. Gdy wyjątek e ma postać [e.getCause()==null], oznacza to, że e jest wyjątkiem początkowym.

  • wiersz 8: dla każdego wyjątku ze stosu wyjątków [Throwable th] zapisywane są dwie informacje:
    • [getClass().getName()]: pełna nazwa wyjątku;
    • [getMessage()]: powiązany komunikat o błędzie;

3.4. Exemple-01

3.4.1. Architektura projektu

W tym przykładzie program konsolowy korzysta z interfejsu warstwy [JDBC].

3.4.2. Projekt Eclipse

Tworzymy projekt Spring / Maven o nazwie [spring-jdbc-01], postępując zgodnie z instrukcjami zawartymi w punkcie 2.5.2.1.

  

Projekt ten jest projektem Maven zdefiniowanym w następującym pliku [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 /> <!-- wyszukiwanie elementu nadrzędnego w repozytorium -->
    </parent>

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

    <dependencies>
        <!-- konfiguracja JDBC dla 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>
  • wiersze 28–32: projekt wykorzystuje artefakt [generic-config-jdbc] z projektu [mysql-config-jdbc], który właśnie przeanalizowaliśmy. Projekt [spring-jdbc-01] ma zatem dostęp do wszystkich elementów projektu [mysql-config-jdbc];

Tę ostatnią kwestię można zaobserwować na dwa sposoby, sprawdzając zależności Maven projektu:

  • w projekcie [2] widać, że projekt [mysql-config-jdbc] znajduje się wśród zależności Maven tego projektu. Ponieważ te ostatnie znajdują się w ścieżce klasy (Classpath) projektu, oznacza to, że projekt [mysql-config-jdbc] również znajduje się w tej ścieżce klasy, a zatem jego klasy i interfejsy są widoczne w projekcie [spring-jdbc-01];

Projekt Maven o nazwie [mysql-config-jdbc] nie musi znajdować się w zakładce [Package Explorer], aby mógł być wykorzystywany przez inne projekty Maven. Wystarczy, że znajduje się w lokalnym repozytorium Maven. W przeciwieństwie do środowiska IDE, takiego jak NetBeans, w Eclipse obecność ta nie następuje automatycznie. Należy ją wymusić:

Warunki umożliwiające to wygenerowanie omówiliśmy w punkcie 2.3.5. Po zakończeniu tego procesu można usunąć projekt [mysql-config-jdbc] z zakładki [Package Explorer]:

  • nie należy zaznaczać opcji [3], która fizycznie usuwa projekt z dysku, uniemożliwiając jego odzyskanie;

Operacja ta powoduje ponowne obliczenie zależności Maven dla projektów zależnych od projektu usuniętego z [Package Explorer]. Powoduje to zmianę gałęzi [Maven Dependencies] w tych projektach. Na przykład w przypadku projektu [spring-jdbc-01] gałąź [Maven Dependencies] przyjmuje następujący kształt:

Tym razem zależność nie dotyczy już projektu, ale jego artefaktu Maven, w tym przypadku artefaktu [generic-config-jdbc] [1]. Widać, że mamy dostęp do wszystkich klas i interfejsów tego artefaktu. Jak już wspomniano, artefakt ten będzie generowany przez wszystkie projekty [*-config-jdbc]. Aby uniknąć błędów:

  • zawsze zachowamy tylko jeden projekt [*-config-jdbc] w zakładce [Package Explorer];
  • zaktualizujemy konfigurację Maven wszystkich projektów w zakładce [Package Explorer] (Alt-F5), tak aby w ich zależnościach Maven pojawił się wykorzystywany projekt [*-config-jdbc];

3.4.3. Szkielet klasy głównej

  

Szkielet klasy głównej [IntroJdbc01] wygląda następująco:


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 {

    // stałe
    final static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) {
        // ładowanie sterownika JDBC z SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // opróżnianie tabeli [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // wypełnianie tabeli
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // odczyt tabeli
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // aktualizacja
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // wyświetlanie
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // opróżnianie tabeli [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // wyświetlanie
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion z dwóch identycznych elementów
        // operacja INSERTion musi zakończyć się niepowodzeniem i żaden z tych dwóch elementów nie zostanie wstawiony z powodu transakcji
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // sprawdzamy
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // zakończono
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // lista produktów
    private static void select() {
    ...
    }

    // wyświetlenie obiektu jSON
    private static void affiche(Object object) {
...
    }

    // usuwanie produktów
    public static void delete() {
...
    }

    // dodawanie produktów
    public static void insert() {
...
    }

    // dodanie 2 produktów o tych samych kluczach głównych
    public static void insert2() {
...
    }

    // aktualizacja niektórych produktów
    public static void update() {
...
    }

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

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

            }
        }
        if (connexion != null) {
            try {
                // zamknij połączenie
                connexion.close();
            } catch (SQLException e3) {
                // wyświetlane są komunikaty o błędach
                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) {
        // wyświetla się komunikat o błędzie
        show(title, getErreursFromThrowable(th));
        // anulowanie transakcji
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // wyświetla komunikaty o błędach
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }

    private static List<String> getErreursFromThrowable(Throwable th) {
        // pobieranie listy komunikatów o błędach związanych z wyjątkiem
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // komunikat o błędzie obiektu typu throwable
            erreurs.add(th.getMessage());
            // przechodzenie do przyczyny obiektu typu throwable
            th = th.getCause();
        }
        // wynik
        return erreurs;
    }

    private static void show(String title, List<String> messages) {
        // tytuł
        System.out.println(String.format("%s : ", title));
        // komunikaty
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • wiersze 23–29: ładowanie sterownika JDBC z projektu SGBD. W wierszu 25 wykorzystano stałą [ConfigJdbc.DRIVER_CLASSNAME] zdefiniowaną w projekcie [mysql-config-jdbc];
  • wiersze 136–147: metoda [getErreursFromThrowable] zwraca listę komunikatów o błędach zawartych w obiekcie typu [Throwable], który jest klasą nadrzędną klasy [Exception]. Wyjątek może zawierać inny wyjątek, który można uzyskać za pomocą metody [Throwable].getCause(). W ten sposób przegląda się wszystkie wyjątki zawarte w obiekcie [Throwable];
  • wiersze 149–156: metoda [show(String title, List<String> messages)] wyświetla komunikaty poprzedzone tekstem [title];
  • wiersze 122–134: metoda [doCatchException(String title, Connection connexion, Throwable th))] obsługuje wyjątki napotkane przez metody klasy. Obsługiwany wyjątek jest reprezentowany przez parametr [Throwable th]. Celem tej metody jest:
    • anulowanie bieżącej transakcji obiektu [Connection connexion] (wiersze 127–129);
    • zapisanie komunikatów o błędach zawartych w wyjątku [Throwable th] (wiersze 124, 132);
  • wiersze 93–120: metoda [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] obsługuje gałąź [finally] metod dostępu do SGBD. Jej celem jest zwolnienie zasobów wykorzystanych przez połączenie;

3.4.4. Usunięcie zawartości tabeli produktów

Metoda [delete] usuwa zawartość tabeli:


    // usunięcie produktów
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // opróżnianie tabeli [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // obsługa bloku finally
            doFinally(null, ps, connexion);
        }
}

W wierszu 7 wykorzystano następujące stałe z klasy [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 = "";

W wierszu 13 przygotowane polecenie SQL ma następującą postać:


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

Metoda [delete] wykorzystuje transakcje. Transakcja pozwala zgrupować zlecenia SQL, które muszą zostać zrealizowane w całości lub anulowane w całości. Należy zapoznać się z czterema operacjami:

  • rozpoczęcie transakcji: [connexion.setAutoCommit(false)];
  • zakończenie transakcji z powodzeniem: [connexion.commit()]. W tym przypadku wszystkie operacje wykonane w ramach BD podczas transakcji są zatwierdzane;
  • zakończenie transakcji z niepowodzeniem: [connexion.rollback()]. W tym przypadku wszystkie operacje wykonane na BD w trakcie transakcji są anulowane;

W naszych przykładach za każdym razem, gdy wystąpi wyjątek, anulujemy transakcję w metodzie [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // wyświetlanie komunikatów o błędach
        Static.show(title, Static.getErreursFromThrowable(th));
        // anulowanie transakcji
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // wyświetlanie komunikatów o błędach
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

3.4.5. Tworzenie zawartości tabeli produktów

Metoda [insert] tworzy zawartość tabeli:


public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // wypełnianie tabeli
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // przygotowanie
                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));
                // wykonanie
                ps.executeUpdate();
            }
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // obsługa bloku finally
            doFinally(null, ps, connexion);
        }
    }

W wierszu 12 przygotowane polecenie SQL ma następującą postać:


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

3.4.6. Wyświetlanie zawartości tabeli produktów

Metoda [select] wyświetla zawartość tabeli:


// lista produktów
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie tylko do odczytu
            connexion.setReadOnly(true);
            // odczyt tabeli [PRODUITS]
            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)));
            }
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // obsługa bloku finally
            doFinally(rs, ps, connexion);
        }
    }

W wierszu 14 przygotowane polecenie SQL ma następującą postać:


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

Metoda [affiche] (wiersz 18) ma następującą postać:


    // wyświetlanie obiektu jSON
    private static void affiche(Object object) {
        try {
            System.out.println(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Wyświetla reprezentację jSON obiektu przekazanego jako parametr (por. jSON, punkt 23.12).

3.4.7. Aktualizacja zawartości tabeli

Metoda [update] aktualizuje niektóre produkty:


    // aktualizacja niektórych produktów
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // aktualizacja tabeli
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // kategoria 1
            ps.setInt(1, 1);
            // wykonanie
            ps.executeUpdate();
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // przetwarzanie bloku finally
            doFinally(null, ps, connexion);
        }
}

Wiersz 13, przygotowane polecenie SQL ma następującą postać:


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

3.4.8. Rola transakcji

Metoda [insert2] wstawia do tabeli dwa produkty o tym samym kluczu głównym, co jest niemożliwe. Ponieważ mamy do czynienia z transakcją, pierwsze wstawienie zostanie cofnięte.


    // dodanie 2 produktów o tych samych kluczach głównych
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // nawiązanie połączenia
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // dodawanie 1 wiersza
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // wykonanie
            ps.executeUpdate();
            // dodajemy ten sam wiersz po raz drugi, a więc z tym samym kluczem głównym
            // transakcja INSERTion powinna zakończyć się niepowodzeniem i żaden z tych dwóch elementów nie powinien zostać wstawiony z powodu transakcji
            ps.executeUpdate();
            // zatwierdź transakcję
            connexion.commit();
        } catch (SQLException e1) {
            // obsługujemy wyjątek
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // obsługujemy blok finally
            doFinally(null, ps, connexion);
        }
}

Wiersz 13, przygotowane polecenie SQL ma następującą postać:


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. Wyniki

Uruchamiamy konfigurację wykonania o nazwie [spring-jdbc-generic-01.IntroJdbc01]:

 

Otrzymujemy następujące wyniki na konsoli:


------------------------------ 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é
  • wiersz 30: przed wstawieniem dwóch produktów o tym samym kluczu głównym tabela jest pusta;
  • wiersz 35: po wstawieniu dwóch produktów o tym samym kluczu głównym tabela jest pusta. Pokazuje to rolę transakcji:
    • pierwsze wstawienie przebiega pomyślnie. Nie ma powodu, by się nie powiodło;
    • drugi wstaw (wiersz 32) kończy się niepowodzeniem. W związku z tym, że oba wstawienia odbywają się w ramach tej samej transakcji, wszystkie polecenia SQL w ramach tej transakcji zostają anulowane, w tym również pierwsze wstawienie.

3.4.10. Wniosek

W powyższym kodzie na uwagę zasługuje duże znaczenie, jakie przypisano obsłudze wyjątku [SQLException]. Ponieważ każda operacja JDBC może wywołać ten wyjątek, w kodzie występuje wiele instrukcji try / catch.

3.5. Exemple-02

Wrócimy do poprzedniej aplikacji, wykorzystując źródło danych typu [javax.sql.DataSource]:

Image

Wykorzystamy źródło danych zaimplementowane przez klasę [org.apache.tomcat.jdbc.pool.DataSource]. Klasa ta korzysta z puli połączeń, czyli zbioru otwartych połączeń:

  • gdy pula jest instancjonowana, otwierana jest określona liczba połączeń z bazą danych. Liczbę tę można skonfigurować;
  • gdy kod Java otwiera połączenie, jest ono dostarczane przez pulę;
  • gdy kod Java zamyka połączenie, jest ono zwracane do puli;

W rezultacie połączenia są otwierane tylko raz, co poprawia wydajność dostępu do bazy danych. Źródło danych zostanie zdefiniowane w klasie konfiguracyjnej Spring

3.5.1. Architektura projektu

W tym przykładzie program konsolowy korzysta z interfejsu warstwy [JDBC].

3.5.2. Projekt Eclipse

Nowy projekt Eclipse można uzyskać poprzez skopiowanie poprzedniego projektu [1-6]:

Następnie projekt [6] jest aktualizowany do wersji [7]:

3.5.3. Konfiguracja Maven

Projekt [7] jest projektem Maven zdefiniowanym przez następujący plik [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 /> <!-- wyszukiwanie elementu nadrzędnego z repozytorium -->
    </parent>

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

    <dependencies>
        <!-- konfiguracja JDBC dla 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>
  • wiersze 28–33: zależność Maven od projektu [mysql-config-jdbc];

To właśnie projekt [mysql-config-jdbc] zawiera w swoich zależnościach Maven bibliotekę zapewniającą implementację źródła danych typu [javax.sql.DataSource] (patrz punkt 3.3.2):


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

3.5.4. Konfiguracja Spring

  

Klasa konfiguracyjna Springa [AppConfig] wygląda następująco:


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 {
    // źródło danych
    @Bean
    public DataSource dataSource() {
        // źródło danych TomcatJdbc
        DataSource dataSource = new DataSource();
        // konfiguracja dostępu JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // początkowo otwarte połączenia
        dataSource.setInitialSize(5);
        // wynik
        return dataSource;
    }

}
  • wiersz 10: [AppConfig] jest klasą konfiguracyjną Spring;
  • wiersz 11: import klasy konfiguracyjnej [generic.jdbc.config.ConfigJdbc.class] zdefiniowanej w projekcie [mysql-config-jdbc]. Oznacza to, że dostępne są wszystkie bean zdefiniowane w tym pliku konfiguracyjnym;
  • wiersze 14–27: bean Spring definiujący źródło danych;
  • wiersz 17: utworzenie źródła danych, które nie zostało jeszcze skonfigurowane;
  • wiersze 19–22: informacje umożliwiające źródłu danych połączenie się z bazą danych;
  • wiersz 24: tworzy pulę 5 połączeń. W tym przypadku potrzebne jest tylko jedno. Nigdy nie ma wielu połączeń jednocześnie;

3.5.5. Klasa główna

Główna klasa [IntroJdbc02] wygląda następująco:


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 {

    // mapowanie jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // źródło danych
    private static DataSource dataSource;

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // pobieranie kontekstu Spring
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // pobieranie źródła danych
            dataSource = ctx.getBean(DataSource.class);
            // opróżnianie tabeli [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // zakończono
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // lista produktów
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // nawiązanie połączenia
            connexion = dataSource.getConnection();
            // rozpoczęcie transakcji
            connexion.setAutoCommit(false);
            // w trybie tylko do odczytu
            connexion.setReadOnly(true);
            // odczyt tabeli [PRODUITS]
            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)));
            }
            // zatwierdzenie transakcji
            connexion.commit();
        } catch (SQLException e1) {
            // obsługa wyjątku
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // przetwarzanie bloku finally
            doFinally(rs, ps, connexion);
        }
    }
...
  • wiersz 25: źródło danych. Należy zauważyć, że jest to typ [javax.sql.DataSource] (wiersz 13), który jest interfejsem;
  • wiersz 31: instancjonowanie obiektów Spring;
  • wiersz 32: uzyskanie odwołania do źródła danych. Należy zauważyć, że w żadnym momencie nie podano nazwy faktycznie używanej klasy. W tym przypadku nic nie wskazuje na to, że używana jest implementacja [TomcatJdbc];
  • wiersz 49: uzyskanie otwartego połączenia. W ten sposób różne metody klasy [IntroJdbc02] uzyskują połączenie z bazą danych. Pozostała część kodu jest identyczna jak w klasie [IntroJdbc01];

3.5.6. Testy

Uruchamiamy konfigurację wykonania o nazwie [spring-jdbc-generic-02.IntroJdbc02]:

 

Otrzymujemy te same wyniki, co poprzednio (punkt 3.4.9).

3.6. Exemple-03

3.6.1. Architektura projektu

W tym przykładzie metody dostępu do danych są wyodrębnione w warstwie [dao]. Zostaną one przetestowane za pomocą testu JUnit.

3.6.2. Projekt Eclipse

Projekt Eclipse [spring-jdbc-03] to projekt Spring / Maven zbudowany tak samo jak poprzedni, a następnie uzupełniony w następujący sposób:

 

Poszczególne pakiety pełnią następujące funkcje:

  • [spring.jdbc.config]: konfiguracja projektu Spring;
  • [spring.jdbc.dao]: implementacja warstwy [DAO];
  • [spring.jdbc.infrastructure]: implementacja niekontrolowanego wyjątku [DaoException];

3.6.3. Konfiguracja Maven

Projekt Maven jest skonfigurowany za pomocą następującego pliku [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 /> <!-- wyszukiwanie elementu nadrzędnego z repozytorium -->
    </parent>

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

    <dependencies>
        <!-- konfiguracja JDBC dla 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>

Jest on identyczny z plikiem projektu [spring-jdbc-02]. Wykorzystuje on w szczególności zależność Maven projektu [mysql-config-jdbc] (wiersze 28–32).

3.6.4. Interfejs warstwy [DAO]

  

Warstwa [DAO] posiada następujący interfejs [IDao]:


package spring.jdbc.dao;

import java.util.List;

import spring.jdbc.entities.Produit;

public interface IDao {

    // dodaj produkty
    public List<Produit> addProduits(List<Produit> produits);

    // lista wszystkich produktów
    public List<Produit> getAllProduits();

    // konkretny produkt
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // aktualizacja wielu produktów
    public int updateProduits(List<Produit> produits);

    // usunięcie wszystkich produktów
    public int deleteAllProduits();

    // usunięcie wielu produktów
    public int deleteProduits(int[] ids);
}

3.6.5. Klasa [DaoException]

Klasa [DaoException] stanowi jedynie rozszerzenie klasy [UncheckedException] przedstawionej w punkcie 3.3.5:

  

package spring.jdbc.infrastructure;

public class DaoException extends UncheckedException {

    private static final long serialVersionUID = 1L;

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

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

}

3.6.6. Konfiguracja projektu Spring

  

Klasa [AppConfig], która konfiguruje projekt Spring, jest identyczna z plikiem konfiguracyjnym Spring z przykładu [spring-jdbc-02], z wyjątkiem wiersza 11:


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 {
    // źródło danych
    @Bean
    public DataSource dataSource() {
        // źródło danych TomcatJdbc
        DataSource dataSource = new DataSource();
        // konfiguracja dostępu JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // połączenia otwarte początkowo
        dataSource.setInitialSize(5);
        // wynik
        return dataSource;
    }
}
  • wiersz 11: pakiet [spring.jdbc.dao] zostanie przeszukany w celu znalezienia innych komponentów Springa niż te zdefiniowane w tym pliku konfiguracyjnym;

3.6.7. Implementacja warstwy [DAO]

  

Przypomnijmy (punkt 3.6.4), że warstwa [DAO] implementuje następujący interfejs [IDao]:


package spring.jdbc.dao;

import generic.jdbc.entities.dbproduits.Produit;

import java.util.List;

public interface IDao {

    // dodaj produkty
    public List<Produit> addProduits(List<Produit> produits);

    // lista wszystkich produktów
    public List<Produit> getAllProduits();

    // konkretny produkt
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // aktualizacja wielu produktów
    public int updateProduits(List<Produit> produits);

    // usunięcie wszystkich produktów
    public int deleteAllProduits();

    // usunięcie wielu produktów
    public int deleteProduits(int[] ids);
}

Obie klasy [Dao1, Dao2] implementują ten interfejs. Klasa [Dao2] jest odmianą klasy [Dao1], która wprowadza nowość składniową. Skupimy się na klasie [Dao1]. Jej szkielet wygląda następująco:


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 {

    // nazwa klasy
    private String simpleClassName = getClass().getSimpleName();
    // źródło danych
    @Autowired
    protected DataSource dataSource;

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

    // ------------------------------- interfejs
    @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) {
...
    }

    // ---------------------------------------- metody lokalne
    // obsługa finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }

    // obsługa catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
  • wiersz 20: klasa [Dao] jest komponentem Spring o nazwie [dao1]. Nazwa ta jest opcjonalna. Jeśli nie jest podana, używana jest nazwa klasy, w której pierwsza litera jest zamieniona na małą;
  • wiersz 24: nazwa klasy. Unika się wpisywania na stałe nazwy [Dao], aby zachować możliwość zmiany nazwy klasy bez konieczności ponownego definiowania tego pola, które w ten sposób pozostaje zawsze aktualne;
  • wiersze 26–27: wstawienie źródła danych [tomcat-jdbc] zdefiniowanego w klasie konfiguracyjnej [AppConfig];
  • wiersze 36–68: implementacja interfejsu [IDao];
  • wiersze 78–80: scentralizowane zarządzanie catch dla różnych metod;
  • wiersze 72–75: scentralizowane zarządzanie finally dla różnych metod;

Interfejs catch dla różnych metod jest obsługiwany w następujący sposób:


    // obsługa catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // anulowanie transakcji
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            e2.printStackTrace();
        }
        // daoException
        return new DaoException(code, th, simpleClassName);
}
  • wiersz 2: metoda jest zadeklarowana jako [protected], co pozwala klasom potomnym na korzystanie z niej, mimo że nie jest ona publiczna. Przyjmuje ona następujące parametry:
    • [Connection connexion]: połączenie z SGBD – być może null;
    • [Throwable th]: wyjątek, który wystąpił i który zostanie zamknięty w typie [DaoException];
    • [int code]: kod błędu, który należy zastosować, jeśli metoda utworzy nowy obiekt typu [DaoException];
  • wiersze 4–7: głównym zadaniem tej metody jest anulowanie transakcji powiązanej z połączeniem przekazanym jako parametr 1;
  • wiersze 8–10: jeśli anulowanie transakcji nie powiodło się, na konsoli wyświetlany jest ślad wyjątku. Nie można zrobić nic więcej, ponieważ w wierszu 12 zostanie wygenerowany wyjątek;

Kod finally dla poszczególnych metod jest obsługiwany w następujący sposób:


// obsługa finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // zamknięcie ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

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

            }
        }
        // zamknięcie połączenia
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // rejestruje się błąd, jeśli to możliwe
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // wynik
        return daoException;
    }
  • wiersz 2: ta metoda jest również zadeklarowana jako [protected]. Otrzymuje ona następujące parametry:
    • [ResultSet rs]: ewentualnie [ResultSet], jeśli wykonano operację [SELECT] – być może null;
    • [PreparedStatement ps]: operacja [PreparedStatement], która została wykonana – być może null;
    • [Connection connexion]: połączenie z SGBD – być może null;
    • [int code]: kod błędu, którego należy użyć, jeśli metoda utworzy nowy obiekt [DaoException];
    • [DaoException daoException]: ewentualny [DaoException], jeśli przed finally wystąpił wyjątek – być może null;
  • wiersze 21–30: głównym celem tej metody jest zamknięcie połączenia (wiersz 23);
  • wiersze 24–29: jeśli podczas tego zamykania wystąpi wyjątek, wówczas sprawdzamy stan przekazanego nam parametru [DaoException daoException]: jeśli jest to [daoException == null], to tworzymy nowy [DaoException] z kodem przekazanym w parametrze;
  • wiersz 32: jako wynik zwracany jest stary lub nowy obiekt [DaoException];

Nie będziemy omawiać wszystkich metod klasy [Dao], a jedynie kilka z nich. Wszystkie są do siebie podobne.

3.6.7.1. Metoda [getProduitById]

Metoda [getProduitById] zwraca produkt, którego klucz główny jest równy parametrowi [id], w przeciwnym razie zwraca inny wynik;


@Override
    public Produit getProduitById(int id) {
        // zasoby połączenia
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // na początku brak wyjątku
        DaoException daoException = null;
        // szukany produkt
        Produit produit = null;
        try {
            // nawiązanie połączenia
            connexion = dataSource.getConnection();
            // początek transakcji
            connexion.setAutoCommit(false);
            // w trybie tylko do odczytu
            connexion.setReadOnly(true);
            // odczyt tabeli [PRODUITS]
            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));
            }
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // obsługa wyjątku
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // przetwarzanie bloku finally
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // wyjątek?
        if (daoException != null) {
            throw daoException;
        }
        // wynik
        return produit;
    }
  • wiersz 10: produkt do zwrócenia ustawiono na null;
  • wiersz 19: zlecenie SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] ma następującą treść:

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

  • wiersze 22–24: jeśli [ResultSet] ma jeden wiersz, wykorzystuje się go do utworzenia produktu do zwrotu, w przeciwnym razie produkt do zwrotu pozostaje w null;
  • wiersz 41: zwracamy produkt;
  • wiersz 8: wyjątek [DaoException] metody jest inicjowany na null;
  • wiersz 31: metoda [doCatchException] generuje wyjątek [DaoException];
  • wiersz 34: parametr [daoException] metody [doFinally] ma wartość albo null,, albo jest wyjątkiem wygenerowanym przez metodę [doCatchException]. Metoda [doFinally]:
    • pozostawia ten parametr bez zmian, jeśli uda jej się zamknąć połączenie;
    • pozostawia ten parametr bez zmian, jeśli nie uda jej się zamknąć połączenia i wcześniej wystąpiła już metoda [DaoException];
    • tworzy nową metodę [DaoException], jeśli nie uda jej się zamknąć połączenia i wcześniej nie wystąpiła metoda [DaoException];
  • wiersze 37–39: jeśli lokalny wyjątek [daoException] nie jest równy null, to jest on wywoływany, w przeciwnym razie zwracany jest żądany wynik (wiersz 41);

3.6.7.2. Metoda [deleteProduits]

Metoda [deleteProduits] usuwa produkty, których klucze główne zostały jej przekazane jako parametry. Zwraca liczbę usuniętych produktów.


@Override
    public int deleteProduits(int[] ids) {
        // zasoby połączenia
        PreparedStatement ps = null;
        Connection connexion = null;
        // na początku brak wyjątku
        DaoException daoException = null;
        // liczba zaktualizowanych produktów
        int nbProduits = 0;
        try {
            // nawiązanie połączenia
            connexion = dataSource.getConnection();
            // początek transakcji
            connexion.setAutoCommit(false);
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // usuwanie produktów
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // parametry
                ps.setInt(1, id);
                // wykonanie
                nbProduits += ps.executeUpdate();
            }
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // obsługa wyjątku
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // przetwarzanie bloku finally
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // wyjątek?
        if (daoException != null) {
            throw daoException;
        }
        // wynik
        return nbProduits;
    }
  • w wierszu 18 kolejność SQL [ConfigJdbc.V2_DELETE_PRODUITS] jest następująca:

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

  • wiersze 18–24: kod usuwania produktów. Widać, że zlecenie SQL jest przygotowywane 1 raz (wiersz 18) i wykonywane n razy (wiersze 19–24). Na tym polega zaleta obiektu [PreparedStatement];
  • wiersz 23: metoda [PreparedStatement].executeUpdate() zwraca liczbę wierszy, których dotyczy operacja aktualizacji;
  • wiersz 41: zwracana jest liczba zaktualizowanych produktów;

3.6.7.3. Metoda [updateProduits]

Metoda [updateProduits] aktualizuje w bazie produkty przekazane jej jako parametry. Zwraca liczbę zaktualizowanych produktów.


@Override
    public int updateProduits(List<Produit> produits) {
        // zasoby połączenia
        PreparedStatement ps = null;
        Connection connexion = null;
        // na początku brak wyjątku
        DaoException daoException = null;
        // liczba zaktualizowanych produktów
        int nbProduits = 0;
        try {
            // nawiązanie połączenia
            connexion = dataSource.getConnection();
            // początek transakcji
            connexion.setAutoCommit(false);
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // aktualizowana jest tabela [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // parametry
                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());
                // wykonanie
                nbProduits += ps.executeUpdate();
            }
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // obsługa wyjątku
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // obsługa bloku finally
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // wyjątek?
        if (daoException != null) {
            throw daoException;
        }
        // wynik
        return nbProduits;
    }
  • wiersz 18: kolejność SQL [ConfigJdbc.V2_UPDATE_PRODUITS] jest następująca:

public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
  • wiersze 19–28: kod aktualizacji produktów;

3.6.7.4. Metoda [addProduits]

Metoda [addProduits] dodaje do bazy produkty przekazane jej w parametrach. Zwraca te same produkty wraz z ich kluczami głównymi (przed dodaniem do bazy produkty nie mają klucza głównego).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // zasoby połączenia
        PreparedStatement ps = null;
        Connection connexion = null;
        // na początku brak wyjątku
        DaoException daoException = null;
        try {
            // nawiązanie połączenia
            connexion = dataSource.getConnection();
            // w trybie odczytu/zapisu
            connexion.setReadOnly(false);
            // początek transakcji
            connexion.setAutoCommit(false);
            // dodawanie elementów do tabeli [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // parametry
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // wykonanie polecenia
                ps.executeUpdate();
                // wygenerowano klucz główny
                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()));
                }
            }
            // zatwierdzenie transakcji
            connexion.commit();
            // powrót do trybu domyślnego
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // obsługa wyjątku
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // przetwarzanie bloku finally
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // wyjątek?
        if (daoException != null) {
            throw daoException;
        }
        // wynik
        return produits;
}
  • W wierszu 16 kolejność SQL [ConfigJdbc.V2_INSERT_PRODUITS] jest następująca:

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

W powyższym przykładzie polecenie dodania produktu nie zawiera klucza głównego [ID]. Ponieważ klucz podstawowy bazy MySQL ma atrybut [AUTOINCREMENT], SGBD wygeneruje klucz podstawowy dla każdego wstawiania. Powstaje problem z odzyskaniem tego klucza. Jest to istotna kwestia, ponieważ operacje na produktach są wykonywane za pomocą ich kluczy głównych. Należy zatem je znać;

  • wiersze 17–33: pętla wstawiania produktów;
  • wiersz 16: szczególna forma metody [prepareStatement]. Drugi parametr [generatedColumns] to tablica nazw kolumn, z których chcemy pobrać wartości po wstawieniu. W wierszu 16 wskazaliśmy, że chcemy pobrać wartość z kolumny [id]. Należy tutaj zauważyć, że chociaż nazwy kolumn w tabeli są niewrażliwe na wielkość liter (wielkie/małe), to SGBD PostgreSQL wymagało, aby nazwa ta była zapisana małymi literami. Jest to typowy rodzaj problemów, które pojawiają się podczas przenoszenia kodu z jednego systemu do drugiego;
  • wiersz 24: wstawienie wiersza do bazy danych;
  • wiersz 26: pobieramy listę wartości z kolumn określonych w wierszu 16 w pliku [ResultSet]. W tym przypadku, przy jednym wstawieniu, plik [ResultSet] będzie zawierał jeden wiersz, a ten wiersz będzie miał jedną kolumnę zawierającą klucz główny;
  • wiersz 28: pobieramy klucz główny wygenerowany przez SGBD;
  • wiersze 29–32: jeśli nie uzyskano wygenerowanego klucza głównego, uruchamia się [RuntimeException], który zostanie zamknięty w [DaoException] w wierszach 38–40;

3.6.8. Klasa [Dao2]

  

Klasa [Dao2] jest wariantem klasy [Dao1] wykorzystującym składnię zwaną try-with-resource(resource):

1
2
3
4
try(resource){
...
}
...
  • [resource] to zasób, który implementuje interfejs [java.lang.AutoCloseable]. Wszystkie zasoby zwalniane za pomocą metody [close] należą do tej klasy. Ta składnia gwarantuje, że w wierszu 4 zasób [resource] zostanie zamknięty. Pozwala to uniknąć pisania klauzuli [finally] w celu wykonania tej operacji zamknięcia;

Weźmy jako przykład metodę [getAllProduits] klasy [Dao2]:


    @Override
    public List<Produit> getAllProduits() {
        // ewentualny wyjątek
        DaoException daoException = null;
        // lista produktów
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // początek transakcji
            connexion.setAutoCommit(false);
            // w trybie tylko do odczytu
            connexion.setReadOnly(true);
            // odczyt tabeli [PRODUITS]
            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)));
                    }
                }
                // koniec transakcji
                connexion.commit();
                // powrót do trybu domyślnego
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // anulowanie transakcji
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // obsługa wyjątku
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // wyjątek?
        if (daoException != null) {
            throw daoException;
        }
        // wynik
        return produits;
}
  • wiersz 7: blok try z zasobem [Connection]. W wierszu 27 mamy pewność, że zasób ten jest zamknięty;
  • wiersz 13: blok try z zasobem [PreparedStatement]. W wierszu 23 mamy pewność, że zasób ten został zamknięty;
  • wiersz 14: instrukcja try z zasobem [ResultSet]. W wierszu 19 mamy pewność, że zasób ten został zamknięty;
  • wiersz 25: transakcja zostaje anulowana w następujący sposób:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // wygenerowanie wyjątku
        return new DaoException(code, e1, simpleClassName);
}

W rezultacie otrzymujemy kod łatwiejszy do odczytania.

3.6.9. Implementacja warstwy testowej

3.6.9.1. Klasy testowe

  
  • test [JUnitTestDao1] jest testem JUnit klasy [Dao1];
  • test [JUnitTestDao2] jest testem JUnit należącym do klasy [Dao2];
  • [AbstractJUnitTestDao] jest klasą nadrzędną dla obu poprzednich klas testowych;
  • [MainTestDao1] jest klasą konsoli testowej klasy [Dao1];
  • [MainTestDao2] jest klasą testową konsoli klasy [Dao2];
  • [AbstractMainTestDao] jest klasą nadrzędną dla dwóch poprzednich klas. Zawiera ona kod klas konsolowych [IntroJdbc01, IntroJdbc02], które zostały już omówione, dlatego nie będziemy analizować tych klas konsolowych;

Klasa [JUnitTestDao1] ma następującą postać:


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 {

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

    @Override
    IDao getDao() {
        return dao;
    }

}
  • komentarze w wierszach 12–13 zostały omówione w punkcie 2.5.5. Umożliwiają one testowi JUnit prosty dostęp do kontekstu Springa i jego beanów. Kontekst ten jest konfigurowany przez klasę [AppConfig] (wiersz 12), omówioną w punkcie 2.4.3;
  • wiersz 14: klasa ta dziedziczy po klasie [AbstractJUnitTestDao], którą zamierzamy przedstawić. To właśnie w tej klasie znajdują się metody testowe JUnit;
  • wiersze 17–19: fasola o nazwie [dao1] (wiersz 18) jest wstrzykiwana (wiersz 17). Jest to zatem instancja klasy [Dao1], która jest tutaj wstrzykiwana;
  • wiersze 21–24: metoda [getDao] redefiniuje metodę o tej samej nazwie w klasie nadrzędnej;

Ostatecznie celem tej klasy jest zwrócenie klasie nadrzędnej odwołania do warstwy [DAO], która ma zostać przetestowana – w tym przypadku jest to instancja klasy [Dao1]. W podobny sposób klasa [JUnitTestDao2] zwraca klasie nadrzędnej [AbstractJUnitTestDao] instancję klasy [Dao2].

Klasa [AbstractJUnitTestDao] jest klasą testową klasy 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 {

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

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

    @Before
    public void clean() {
        // przed każdym testem czyści się bazę
        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() {
    ....
    }

    // -------------- metody prywatne
...
}
  • w wierszu 19 klasa [AbstractJUnitTestDao] jest klasą abstrakcyjną;
  • wiersz 22: abstrakcyjna metoda [getDao], która pozwala uzyskać odwołanie do warstwy [DAO], która ma zostać przetestowana. Metoda ta jest implementowana przez klasy potomne;
  • wiersz 25: mapper jSON, który pozwoli nam wyświetlić na konsoli wartość jSON produktów;
  • wiersze 27–32: przed każdym testem (wiersz 27) tabela [PRODUITS] jest opróżniana;

3.6.9.2. Metoda prywatna [fill]

Prywatna metoda [fill] służy do umieszczania produktów w tabeli [PRODUITS].


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // tworzymy listę produktów
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String nazwa, int kategoria, double cena, String opis
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // zapisujemy ją w bazie danych – pobieramy produkty na podstawie ich klucza głównego
        produits = getDao().addProduits(produits);
        // tworzymy słownik produktów, aby łatwiej je odnaleźć
        // kluczem słownika jest klucz główny produktu w bazie danych
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // zwracamy produkty
        return produits;
    }
  • wiersz 1: metoda [fill] wstawia [nbProduits] do tabeli [PRODUITS], która ma być pusta;
  • wiersze 3–10: utworzenie listy produktów w postaci:

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

która wykorzystuje konstruktor Produkt(int id, String nazwa, int kategoria, double cena, String opis). Wartość pierwszego parametru [id] (klucz podstawowy tabeli [PRODUITS]) nie ma znaczenia, ponieważ metoda [addProduits] z wiersza 10 nie wstawia go do bazy danych i pozwala, by SGBD wygenerował jego wartość;

  • wiersz 12: lista produktów jest zapisywana w bazie danych. Każdy z produktów na tej liście jest wzbogacany o nowy klucz główny [id]. Metoda [addProduits] zwraca jako wynik swój parametr [produits]. Można było zatem nie pobierać wyniku;
  • wiersze 15–17: umieszczamy produkty w słowniku:

    // słownik produktów
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

Kluczem słownika jest klucz główny produktu, a powiązaną z nim wartością jest sam produkt;

  • wiersz 19: zwracamy listę produktów;

3.6.9.3. Test [getProduits]

Wygląda on następująco:


    @Test
    public void getProduits() throws JsonProcessingException {
        // wypełnianie
        fill(10);
        // lista produktów
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // sprawdzamy, czy pobrana lista jest identyczna z listą zapisaną w bazie
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // wszystkie pierwotne produkty muszą zniknąć ze słownika
        Assert.assertEquals(0, mapProduits.size());
}
}
  • wiersz 4: do bazy danych dodano 10 produktów;
  • wiersz 7: po wykonaniu tej czynności wyświetlane są wszystkie produkty z bazy;
  • wiersz 8: wyświetla się je. Celem jest sprawdzenie, czy produkty zostały poprawnie zapisane i czy posiadają klucz główny;
  • wiersze 10–13: sprawdzamy, czy znalezione produkty są identyczne z tymi, które zostały zapisane w bazie i które można odnaleźć w słowniku [mapProduits];
  • wiersz 11: pobieramy ze słownika produkt o tym samym kluczu głównym, co ten pobrany z bazy danych. Pokazuje to, że zapisane produkty rzeczywiście otrzymały klucz główny;
  • wiersz 12: upewniamy się, że oba produkty są identyczne. Przypominamy, że klasa [Produit] zdefiniowała metodę [equals] (por. paragraf 3.3.4);
  • wiersz 13: usuwa się znaleziony element ze słownika;
  • wiersz 16: sprawdzamy, czy słownik produktów początkowych jest rzeczywiście pusty, co oznacza, że wszystkie te produkty początkowe znajdowały się na liście produktów pobranych z bazy;

Metoda [affiche] z wiersza 8 jest następującą metodą prywatną:


    // wyświetlanie listy produktów
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • wiersz 2: metoda [affiche] jest metodą generyczną. Jest ona parametryzowana przez typ T, oznaczany składniowo jako <T>. Gdyby była parametryzowana przez dwa typy T1 i T2, zapisano by to jako <T1,T2>. Składnia metody m parametryzowanej typem T jest następująca:
portée <T> type_résultat m(... , T value1, ...){
...
    T value2=...
}

W kodzie metody m znajdą się dane typu T. Metodę m instancji c klasy C można zatem wywołać w następujący sposób:

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

gdzie T1 jest typem rzeczywistym, który zastąpi typ formalny T metody m. W większości przypadków kompilator jest w stanie wywnioskować typ T1 na podstawie argumentów metody m. Dlatego powyższa instrukcja zostanie najczęściej uproszczona do postaci:

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

Wróćmy do metody [affiche]. Wyświetla ona listę elementów typu T. Jest to możliwe, ponieważ mapper jSON użyty w wierszu 4 jest w stanie utworzyć reprezentację jSON dla dowolnego typu obiektu. W tym konkretnym przykładzie jedynym używanym typem T będzie typ [Produit].

Metoda [affiche] mogłaby również zostać napisana w następujący sposób:


    // wyświetlenie listy produktów
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

Ponieważ rzeczywistym parametrem jest lista produktów, wiersz 3 zapisałby reprezentację jSON tej listy. Nie jest to to samo, co zapisywanie reprezentacji każdego z jej elementów pojedynczo.

Wynik wygenerowany przez test [getProduits] wygląda następująco:

-- 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. Test [getProduitBy]

Wygląda on następująco:


    @Test
    public void getProduitBy() {
        // wypełnianie
        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());
}
  • wiersz 6: metoda [getProduitByName] z interfejsu [IDao] jest wykorzystywana do pobrania produktu o nazwie [NOM3];
  • wiersz 7: następnie wykorzystywana jest metoda [getProduitById] z interfejsu [IDao] w celu pobrania tego samego produktu, tym razem identyfikowanego na podstawie klucza głównego;
  • wiersze 8–10: sprawdza się, czy [produit2] i [produit] mają te same cechy;

3.6.9.5. Test [doInsertsInTransaction]

Wygląda on następująco:


    @Test
    public void doInsertsInTransaction() {
        log("Ajout de deux produits de même nom", 1);
        // przeprowadzamy wstawianie
        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;
        }
        // sprawdzanie
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • wiersze 5–7: tworzymy listę dwóch produktów o tej samej nazwie [x];
  • wiersz 10: te dwa produkty są wstawiane do tabeli [PRODUITS], która jest pusta (metoda [clean] oznaczona jako [@Before]). Pierwsze wstawienie zostanie wykonane, ale drugie nie, ponieważ tabela [PRODUITS] ma ograniczenie unikalności dotyczące nazw produktów. W związku z tym musi wystąpić wyjątek. Jest on sprawdzany w wierszu 15;
  • ponieważ wszystkie metody interfejsu [IDao] są wykonywane w ramach transakcji, niepowodzenie drugiego wstawienia spowoduje anulowanie całej transakcji, a tym samym również pierwszego wstawienia. Ostatecznie żadne wstawienie nie powinno nastąpić w tabeli [PRODUITS];
  • wiersze 16–17: sprawdzamy to, żądając listy produktów zawartych w tabeli [PRODUITS] i upewniając się, że lista ta jest pusta;

3.6.9.6. Test [updateProduits]

Wygląda on następująco:


    @Test
    public void updateProduits() {
        // wypełnianie
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // pobieranie produktów
        List<Produit> produits = getDao().getAllProduits();
        // aktualizujemy produkty z kategorii 1
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String nazwa, int kategoria, double cena, String opis
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // sprawdzanie
        // Assert.assertEquals(nbUpdated, nbProduits); -- nie działa z DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • wiersz 4: dodajemy 10 produktów do bazy;
  • wiersz 7: pobieramy je;
  • wiersze 9–18: podwyższamy ceny produktów z kategorii nr 1 o 10%;
  • wiersz 19: zmiany te są zapisywane w bazie danych;
  • wiersze 22–25: przeglądamy w pamięci listę produktów, która posłużyła do aktualizacji. Dla każdego z nich wyszukujemy w bazie produkt o tym samym kluczu głównym i sprawdzamy, czy aktualizacja ceny rzeczywiście nastąpiła;
  • wiersz 19: pobierana jest liczba produktów zaktualizowanych w wyniku operacji [updateProduits];
  • wiersz 21: sprawdzamy, czy liczba ta jest zgodna z oczekiwaną. Test ten przebiega pomyślnie dla wszystkich operacji SGBD z wyjątkiem SGBD i DB2. Zostały one zatem skomentowane;

3.6.9.7. Test [deleteProduits]

Wygląda on następująco:


    @Test
    public void deleteProduits() {
        // wypełnienie
        fill(10);
        log("deleteProduits", 1);
        // lista produktów
        List<Produit> produits = getDao().getAllProduits();
        // usunięcie dwóch produktów
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // sprawdzenie
        // Assert.assertEquals(2, nbDeleted); -- nie działa z DB2
        Assert.assertNull(getDao().getProduitById(produit0.getId()));
        Assert.assertNull(getDao().getProduitById(produit5.getId()));
        Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
  • wiersz 4: dodajemy 10 produktów do bazy;
  • wiersze 7–11: pobieramy wszystkie produkty z bazy danych i usuwamy z niej produkty pobrane z pozycji 0 i 5;
  • wiersze 14–16: sprawdzamy, czy tych dwóch produktów nie ma już w bazie i czy w bazie jest o dwa produkty mniej;
  • test w wierszu 13 nie przebiega pomyślnie w przypadku SGBD i DB2. Przebiega pomyślnie w przypadku pozostałych SGBD;

3.6.9.8. Testy wydajności

W testach uwzględniono trzy metody, których jedynym celem jest ocena wydajności produktu SGBD:


    @Test
    public void perf1() {
        // wypełnianie
        fill(10000);
    }

    @Test
    public void perf2() {
        // wypełnienie
        fill(10000);
        // modyfikacja
        List<Produit> produits = getDao().getAllProduits();
        // aktualizujemy te z kategorii 1
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String nazwa, int kategoria, double cena, String opis
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }

    @Test
    public void perf3() {
        // wypełnianie
        fill(10000);
        // usunięcie
        List<Produit> produits = getDao().getAllProduits();
        // klucze główne
        int[] keys = new int[produits.size()];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = produits.get(i).getId();
        }
        getDao().deleteProduits(keys);
}
  • wiersze 1–5: wstawienie 10 000 produktów;
  • wiersze 8–20: dodanie 10 000 produktów, a następnie ich modyfikacja na podstawie kluczy głównych;
  • wiersze 23–34: wstawienie 10 000 produktów, a następnie ich usunięcie na podstawie kluczy głównych;

Aby uruchomić testy [JUnitTestDao1] i [JUnitTestDao2], można użyć następujących konfiguracji uruchomienia:

Wyniki testu [JUnitTestDao1] są następujące:

W przypadku [1] wyniki odpowiadają wynikom z [JUnitTestDao1], a w przypadku [2] – wynikom z [JUnitTestDao2]. Nie ma między nimi żadnych istotnych różnic. W [1]:

  • test zakończył się powodzeniem;
  • wprowadzenie 10 000 produktów trwa 3,15 sekundy;
  • dodanie 10 000 produktów, a następnie ich modyfikacja trwa 4,80 sekundy;
  • dodanie 10 000 produktów, a następnie ich usunięcie trwa 4,40 sekundy;
  • a zatem najbardziej czasochłonne jest dodawanie;