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:
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:
- załadowanie sterownika bazy danych JDBC;
- nawiązanie połączenia z bazą;
- wysłanie polecenia SQL do bazy danych oraz przetworzenie wyników polecenia SQL;
- 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:
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:
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]:
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:
- 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]:

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):
- [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:
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:
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:
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:
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;



















































