4. Wprowadzenie do Spring JDBC
W tym rozdziale przyjrzymy się następującej architekturze:
![]() |
Jest to więc ta sama architektura, co poprzednio. Wprowadzimy dwie zmiany:
- baza danych będzie zawierała dwie tabele połączone relacją klucza obcego;
- warstwa [DAO] zostanie zaimplementowana przy użyciu biblioteki [Spring JDBC], która ułatwia zarządzanie API i JDBC;
4.1. Konfiguracja środowiska pracy
Za pomocą STS zaimportuj projekt [spring-jdbc-04] znajdujący się w folderze [<exemples>/spring-database-generic/spring-jdbc]
![]() |
Ponadto należy utworzyć nową bazę danych MySQL przy użyciu klienta [MyManager] (patrz punkt 3.1):
![]() |
- w [3]; poniższe przykłady dotyczą bazy MySQL o nazwie [dbproduitscategories];
![]() |
- na [9] należy wprowadzić hasło użytkownika root (w niniejszym dokumencie hasło to brzmi root);
![]() |
![]() |
- w [18] utworzono pustą bazę danych [dbproduitscategories]. Tworzy się tabele i wypełnia je za pomocą skryptu SQL [19-20];
![]() |
- w [21] należy przejść do folderu [<exemples>/spring-database-config/mysql/databases];
![]() |
- w [25] upewnij się, że znajdujesz się w bazie [dbproduitscategories], a nie w bazie [dbproduits];
- w [29] skrypt SQL utworzył pięć tabel. Tabele [ROLES, USERS, USERS_ROLES] będą wykorzystywane dopiero w momencie, gdy zajmiemy się zabezpieczeniem serwisu internetowego stworzonego w celu udostępnienia bazy danych [dbproduitscategories] w sieci;
4.2. Baza danych [dbproduitscategories]
Baza danych [dbproduitscategories] stanowi rozszerzenie bazy [dbproduits] omówionej wcześniej. Podczas gdy w tabeli [PRODUITS] produkt miał kategorię oznaczoną numerem, który nie miał żadnego szczególnego znaczenia, tutaj numer ten będzie kluczem obcym w tabeli [CATEGORIES].
Tabela [PRODUITS] ma następujący wygląd:
![]() |
- [ID]: autoinkrementujący się klucz główny tabeli [2];
- [NOM]: unikalna nazwa produktu [4];
- [PRIX]: cena produktu;
- [DESCRIPTION]: opis produktu;
- [VERSIONING] to numer wersji produktu. Jego wersja początkowa to 1 [3]. Za każdym razem, gdy produkt zostanie zmodyfikowany, jego numer wersji zostanie zwiększony przez kod obsługujący tabelę;
- [CATEGORIE_ID]: klucz obcy w tabeli [CATEGORIES], określający kategorię, do której należy produkt;
![]() |
- w [1-3], klucz obcy [CATEGORIE_ID] z tabeli [PRODUITS]. Odnosi się do kolumny [ID] w tabeli [CATEGORIES] [4-5];
- gdy kategoria zostanie usunięta, wszystkie powiązane z nią produkty również zostaną usunięte ([6]). Należy zwrócić na to szczególną uwagę, ponieważ jest to wykorzystywane przy tworzeniu warstwy [DAO] opartej na bazie [dbproduitscategories];
Tabela kategorii [CATEGORIES] ma następujący wygląd:
![]() |
- [ID]: autoinkrementujący klucz główny;
- [VERSIONING]: numer wersji kategorii;
- [NOM]: unikalna nazwa kategorii;
4.3. Projekt Eclipse
![]() |
Projekt [spring-jdbc-04] wykorzystuje następującą architekturę:
![]() |
Projekt [spring-jdbc-04] jest projektem Maven skonfigurowanym 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-04</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-04</name>
<description>Demo project for Spring JdbcTemplate</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>
<!-- Spring JdbcTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</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 opiera się na projekcie [mysql-config-jdbc], który konfiguruje warstwę JDBC;
- wiersze 34–37: artefakt [spring-boot-starter-jdbc] pobiera biblioteki Springa JDBC;
Ostatecznie zależności są następujące:
![]() |
4.4. Konfiguracja Spring
![]() |
Klasa [AppConfig], która konfiguruje projekt Spring, wygląda następująco:
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;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
@EnableTransactionManagement
@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_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// początkowo otwarte połączenia
dataSource.setInitialSize(5);
// wynik
return dataSource;
}
// Menedżer transakcji
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// JdbcTemplate
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
// dodanie produktu
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_PRODUITS).usingGeneratedKeyColumns(
ConfigJdbc.TAB_PRODUITS_ID);
}
// dodanie kategorii
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES).usingGeneratedKeyColumns(
ConfigJdbc.TAB_CATEGORIES_ID);
}
}
- wiersz 16: klasa ta jest klasą konfiguracyjną Spring;
- wiersz 17: pakiet [spring.jdbc.dao] zostanie przeszukany w celu znalezienia innych komponentów Springa niż te obecne w klasie [AppConfig]. Znajdziemy tam komponent implementujący warstwę [DAO];
- wiersz 18: nie będziemy samodzielnie zarządzać transakcjami, lecz pozostawimy to Springowi JDBC. Jedyne, co należy zrobić, to opatrzyć metody, które mają być wykonywane w ramach transakcji, adnotacją Springa [@Transactional]. Wiersz 18 gwarantuje, że ta adnotacja zostanie uwzględniona, a nie zignorowana. Zarządzanie transakcjami zapewnia jedna z zależności projektu Spring JDBC, importowana przez plik [pom.xml];
- wiersz 19: importujemy bean’y już zdefiniowane w klasie [generic.jdbc.config.ConfigJdbc] z projektu [mysql-config-jdbc];
- wiersze 23–36: źródło danych [tomcat-jdbc] wprowadzone w przykładzie [spring-jdbc-02];
- wiersze 40–42: menedżer transakcji powiązany z wcześniej zdefiniowanym źródłem danych. Bean musi bezwzględnie nosić nazwę [transactionManager], ponieważ właśnie ta nazwa jest wykorzystywana przez adnotację [@EnableTransactionManagement]. Menedżer [DataSourceTransactionManager] jest dostarczany przez bibliotekę Spring JDBC (wiersz 12);
- wiersze 45–48: bean [namedParameterJdbcTemplate], na którym będzie opierać się implementacja warstwy [DAO]. Ten bean jest dostarczany przez bibliotekę Spring JDBC (wiersz 10). Bean ten jest również powiązany z wcześniej zdefiniowanym źródłem danych (wiersz 47);
- wiersze 51–55: bean [simpleJdbcInsertProduit] (nazwa dowolna) zostanie wykorzystany do wstawienia produktu do tabeli [PRODUITS] oraz pobrania wygenerowanego klucza głównego. Wykorzystywane są następujące parametry:
- [dataSource]: źródło danych [tomcat-jdbc] z wierszy 24–36;
- [ConfigJdbc.TAB_PRODUITS]: tabela [PRODUITS];
- [ConfigJdbc.TAB_CATEGORIES_ID]: kolumna klucza głównego tabeli [PRODUITS]. Należy pamiętać, że w przypadku PostgreSQL nazwa tej kolumny musi być zapisana małymi literami;
- wiersze 58–62: bean [simpleJdbcInsertCategorie] zostanie wykorzystany do wstawienia kategorii do tabeli [CATEGORIES] oraz pobrania wygenerowanego klucza głównego;
4.5. Wyjątki w projekcie
![]() |
Klasy [UncheckedException, DaoException, ShortException] omówiliśmy już w projekcie [spring-jdbc-03]. Dodajemy nową:
package spring.jdbc.infrastructure;
public class MyIllegalArgumentException extends UncheckedException {
private static final long serialVersionUID = 1L;
// producenci
public MyIllegalArgumentException() {
super();
}
public MyIllegalArgumentException(int code, Throwable e, String className) {
super(code, e, className);
}
}
- klasa [MyIllegalArgumentException] wywodzi się z klasy [UncheckedException] i w związku z tym jest klasą niekontrolowaną. Będzie ona wykorzystywana do sygnalizowania wywołania metody z warstwy [DAO] z nieprawidłowymi argumentami. Nie nazwano jej [IllegalArgumentException], ponieważ ten wyjątek istnieje już w klasie JDK, co czasami powodowało, że kompilator generował nieprawidłową klasę [import];
4.6. Elementy projektu
![]() |
Klasy w pakiecie [spring.jdbc.entities] są odwzorowaniami wierszy tabel bazy danych [dbproduitscategories]. Na razie pominiemy odwzorowania tabel [USERS, ROLES, USERS_ROLE].
Wszystkie encje dziedziczą po klasie nadrzędnej [AbstractCoreEntity]:
package spring.jdbc.entities;
public abstract class AbstractCoreEntity {
// właściwości
protected Long id;
protected Long version;
// producenci
public AbstractCoreEntity() {
}
public AbstractCoreEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
public AbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
public void setAbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
// ------------------------------------------------------------
// przedefiniowanie [equals] i [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
return id != null && other.id != null && id.equals(other.id);
}
// metody pobierające i ustawiające
...
}
- wiersz 5: pole [id] zostanie powiązane z kolumną [ID], która jest kluczem głównym tabel;
- wiersz 6: pole [version] zostanie powiązane z kolumną [VERSIONING] w tabelach;
- wiersze 8–26: różne konstruktory i metody służące do tworzenia lub inicjalizacji obiektu [AbstractCoreEntity];
- wiersze 35–47: metoda [equals] określa, że dwa obiekty [AbstractCoreEntity] są równe, jeśli mają to samo pole [id]. Należy tutaj pamiętać, że obiekty [AbstractCoreEntity] będą reprezentować wiersze tabel, w których [id] jest kluczem głównym, a zatem nie mogą istnieć dwa wiersze o tym samym [id];
- wiersze 30–33: propozycja [hashCode];
Klasa [Produit] będzie obrazem wiersza z tabeli [PRODUITS]:
package spring.jdbc.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
// właściwości
private String nom;
private Long idCategorie;
private double prix;
private String description;
private Categorie categorie;
// konstruktory
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
super(id, version);
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// sygnatura
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// metody pobierające i ustawiające
...
}
- wiersz 6: klasa [Produit] rozszerza klasę [AbstractCoreEntity];
- wiersze 8–12: pola [id, version, nom, idCategorie, prix, description] są odwzorowaniami kolumn [ID, VERSIONING, NOM, CATEGORIE_ID, PRIX, DESCRIPTION] z tabeli [PRODUITS];
- wiersz 12: obiekt typu [Categorie] o kluczu głównym [idCategorie]. Pole to może być wypełnione lub nie, w zależności od sytuacji. Gdy jest wypełnione, mówimy o produkcie w wersji długiej [LongProduit], w przeciwnym razie o produkcie w wersji krótkiej [ShortProduit];
- wiersz 5: filtr o nazwie jSON. Przypominamy, że projekt o nazwie [mysql-config-jdbc] zawiera bibliotekę o nazwie jSON. Konieczność zastosowania filtra wynika z faktu, że pole [categorie] może być wypełnione lub nie. W zależności od tego przedstawienie produktu jSON będzie się różnić. Aby obsłużyć te dwa przypadki, należy skonfigurować filtr [jsonFilterProduit] w wierszu 5. Filtr jSON pozwala dynamicznie określić pola, które mają zostać wykluczone z reprezentacji jSON. Gdy okaże się, że pole [categorie] nie zostało wypełnione, zostanie ono wykluczone z reprezentacji produktu jSON;
Klasa [Categorie] stanowi odzwierciedlenie wiersza z tabeli [CATEGORIES]:
package spring.jdbc.entities;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
// właściwości
private String nom;
public List<Produit> produits;
// konstruktory
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
super(id, version);
this.nom = nom;
this.produits = produits;
}
// sygnatura
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// metody
public void addProduit(Produit produit) {
// dodawanie produktu
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// dodajemy produkt
produits.add(produit);
// ustalanie kategorii
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// metody pobierające i ustawiające
...
}
- wiersz 9: klasa [Categorie] rozszerza klasę [AbstractCoreEntity];
- wiersz 12: pola [id, version, nom] są odwzorowaniami kolumn [ID, VERSIONING, NOM] z tabeli [CATEGORIES];
- wiersz 13: pole [produits] zawiera listę produktów z danej kategorii. Pole to nie zawsze jest wypełnione. Jeśli nie jest wypełnione, mowa jest o kategorii w wersji skróconej [ShortCategorie], w przeciwnym razie o kategorii w wersji pełnej [LongCategorie];
- wiersze 32–44: metoda [addProduit] pozwala dodać produkt do kategorii (wiersz 39) oraz przypisać do dodanego produktu cechy charakterystyczne dla tej kategorii (idCategorie i kategoria);
- wiersz 8: filtr jSON. Gdy biblioteka jSON będzie musiała serializować/deserializować obiekt [Categorie], należy jej wskazać, jak obsługiwać filtr o nazwie [jsonFilterCategorie];
4.7. Interfejs Idao<T>
![]() |
![]() |
Interfejs [IDao] warstwy [DAO] ma następującą sygnaturę:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.AbstractCoreEntity;
public interface IDao<T extends AbstractCoreEntity> {
// lista wszystkich encji T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// poszczególnych encji – wersja skrócona
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// poszczególnych encji – wersja długa
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// aktualizacja wielu elementów
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// usunięcie wszystkich elementów
public void deleteAllEntities();
// usunięcie wielu elementów
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
- wiersz 7: mamy tu interfejs [IDao] zdefiniowany przez typ T z warunkiem: typ ten musi rozszerzać klasę [AbstractCoreEntity] lub implementować interfejs [AbstractCoreEntity]. Słowo kluczowe [extends] stosuje się w obu przypadkach. W tym przypadku T zostanie zainicjowane albo przez typ [Produit], albo przez typ [Categorie]. W rzeczywistości dość szybko okazuje się, że wykonujemy ten sam rodzaj operacji (wstawianie, modyfikowanie, usuwanie, wybieranie) na typach [Produit] i [Categorie]. Wydaje się zatem logiczne, aby zgrupować te metody w jednym interfejsie generycznym;
- w zależności od przypadku terminy [LongEntity] i [ShortEntity] odnoszą się do różnych sytuacji:
- gdy T jest typem [Produit]:
- [ShortEntity] to produkt bez wypełnionego pola [Categorie categorie];
- [LongEntity] to produkt z wypełnionym polem [Categorie categorie];
- gdy T jest typem [Categorie]:
- [ShortEntity] to kategoria bez wypełnionego pola [List<Produit> produits];
- [LongEntity] to produkt z wypełnionym polem [List<Produit> produits];
- gdy T jest typem [Produit]:
Mamy zatem interfejs zawierający 19 metod. Większość metod występuje w dwóch wersjach. Weźmy na przykład metodę [getShortEntitiesById]:
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
- wiersze 1 i 3: parametrem jest lista kluczy głównych encji, dla których chcemy uzyskać wersję skróconą. Lista ta jest przedstawiona w dwóch różnych formach:
- wiersz 1: lista implementująca interfejs [Iterable<Long>]. Typ [List<Long>] implementuje ten interfejs, ale istnieje wiele innych. Gdybyśmy podali [List<Long> ids], wystarczyłoby to dla naszych przykładów, ale zmuszałoby to użytkownika naszych przykładów do przeprowadzania konwersji, gdyby jego parametr nie był dokładnie tego typu, jakiego oczekiwano;
- wiersz 3: niestety typ Long[] nie implementuje interfejsu [Iterable<Long>]. W takim przypadku użyjemy wersji z wiersza 3. Parametr formalny [Long... ids] (3 kropki) może przyjmować wartość zarówno tablicy, jak i ciągu identyfikatorów: getShortEntitiesById(id1, id2, ...);
Ten sam interfejs IDao<T> zostanie zaimplementowany przez następującą architekturę:
![]() |
gdzie warstwa [JPA] (Java Persistence API) zostanie umieszczona pomiędzy warstwą [DAO] a sterownikiem JDBC w ramach SGBD. Pozwoli nam to na stworzenie wspólnej warstwy testowej dla obu architektur. W obu przypadkach warstwa [DAO] będzie posiadać dwa interfejsy:
- IDao<Produkt> w celu uzyskania dostępu do tabeli [PRODUITS];
- IDao<Kategoria> w celu uzyskania dostępu do tabeli [CATEGORIES];
4.8. Implementacja interfejsu IDao<T>
![]() |
- interfejs IDao<Produkt> jest zaimplementowany przez klasę [DaoProduit];
- interfejs IDao<Kategoria> jest zaimplementowany przez klasę [DaoCategorie];
Klasy [DaoProduit] i [DaoCategorie] dziedziczą po klasie abstrakcyjnej [AbstractDao] o następującej :
package spring.jdbc.dao;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.transaction.annotation.Transactional;
import spring.jdbc.entities.AbstractCoreEntity;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
public abstract class AbstractDao<T extends AbstractCoreEntity> implements IDao<T> {
// wstawki
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// lokalne
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// poprawność argumentu
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// pobieranie w partiach
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// wynik
return entities;
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// poprawność argumentu
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// wynik
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(String... names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Iterable<Long> ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Long... ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(String... names) {
...
}
@Override
@Transactional
public List<T> saveEntities(Iterable<T> entities) {
...
}
@Override
@Transactional
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities) {
...
}
@Override
public void deleteEntitiesById(Iterable<Long> ids) {
...
}
@Override
public void deleteEntitiesById(Long... ids) {
...
}
@Override
public void deleteEntitiesByName(Iterable<String> names) {
...
}
@Override
public void deleteEntitiesByName(String... names) {
...
}
@Override
public void deleteEntitiesByEntity(Iterable<T> entities) {
...
}
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities) {
...
}
protected void deleteEntitiesByEntity(List<T> entities) {
...
}
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
// metody prywatne ----------------------------------------------
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
...
}
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
// metody chronione ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
}
- wiersz 15: klasa [AbstractDao] jest klasą abstrakcyjną (słowo kluczowe „abstract”). W związku z tym nie można jej instancjonować. Można z niej jedynie tworzyć klasy pochodne. Klasa ta pełni kilka ról:
- określenie charakteru transakcji, w ramach której wykonywana jest każda metoda;
- zrealizowanie jak największej liczby elementów wspólnych dla obu implementacji interfejsów [IDao<Produit>] i [IDao<Categorie>]. Chodzi głównie o sprawdzanie poprawności argumentów. Nie będą akceptowane argumenty typu null ani puste listy;
- ujednolicić typy parametrów T... params i Iterable<T> params do jednego: List<T> params;
- przekazać zadania klasom potomnym, gdy tylko zadanie to staje się specyficzne dla jednego z dwóch interfejsów;
Dzięki ujednoliceniu parametrów różnych metod, przeprowadzonemu przez klasę [AbstractDao], klasy potomne [DaoProduit] i [DaoCategorie] będą musiały zaimplementować tylko 10 metod zamiast 19:
// metody zaimplementowane przez klasy potomne ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
Przyjrzyjmy się kilku metodom klasy [AbstractDao].
Metoda [getShortEntitiesById]
Metoda ta służy do uzyskania skróconej wersji encji, dla których podano klucze główne.
// wstrzyknięcia
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// lokalne
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- wiersze 2–4: wstrzykuje się bean [maxPreparedStatementParameters] zdefiniowany w pliku konfiguracyjnym [ConfigJdbc], który konfiguruje warstwę JDBC konkretnego obiektu SGBD:
// maksymalna liczba parametrów [PreparedStatement]
public final static int MAX_PREPAREDSTATEMENT_PARAMETERS = 10000;
@Bean(name = "maxPreparedStatementParameters")
public int maxPreparedStatementParameters() {
return MAX_PREPAREDSTATEMENT_PARAMETERS;
}
- wiersze 1–7: definiują komponent [maxPreparedStatementParameters], który ustali maksymalną liczbę parametrów, jakie można przekazać typowi [PreparedStatement]. Potrzeba ta nie pojawiła się w przypadku SGBD i MySQL, które akceptowały 10 000 parametrów dla typu [PreparedStatement]. Podczas testów z serwerem SGBD i SQL serwer zgłosił wyjątek wskazujący, że maksymalna liczba parametrów dla typu [PreparedStatement] wynosi 2100. W związku z tym liczba ta stała się parametrem konfiguracyjnym poszczególnych serwerów SGBD. Należy ją zatem umieścić w projekcie konfiguracyjnym [sgbd-config-jdbc] każdego serwera SGBD;
Wróćmy do kodu metody [getShortEntitiesById]:
// wstrzyknięcia
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// lokalny
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- wiersz 7: nazwa klasy. Jest używana jako parametr jednego z konstruktorów klasy wyjątku [DaoException];
- wiersz 10: adnotacja [@Transactional(readOnly = true)] wskazuje, że metoda musi być wykonywana w transakcji tylko do odczytu. Można zastanawiać się nad sensownością takiej transakcji, ponieważ metoda wykonuje wyłącznie operacje odczytu, a zatem w przypadku niepowodzenia nie ma nic do cofnięcia. Autor biblioteki [Spring Data] zaleca to i wyjaśnia dlaczego. Postąpiłem zgodnie z jego radą;
Treść metody wygląda następująco:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// poprawność argumentu
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
...
}
- wiersz 5: poprawność parametru [ids] jest sprawdzana za pomocą następującej metody:
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
// elementy null?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"), simpleClassName);
}
// puste elementy?
if (!elements.iterator().hasNext()) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),
simpleClassName);
} else {
return new ArrayList<T>();
}
}
// wynik domyślny
return null;
}
- wiersz 1: metoda [checkNullOrEmptyArgument] jest metodą generyczną, której typem jest <T2>. T2 to typ elementów przekazywanych jako drugi parametr tej metody. Może to być [Long, String, AbstractCoreEntity];
- wiersz 1: metoda [checkNullOrEmptyArgument] przyjmuje dwa parametry:
- [Iterable<T2> elements]: parametr, który ma zostać przetestowany;
- [checkEmpty]: przyjmuje wartość „prawda”, jeśli należy sprawdzić, czy poprzedni parametr jest listą niepustą;
- wiersze 4–6: sprawdzamy, czy parametr [elements] nie jest równy null. Jeśli tak nie jest, zgłaszany jest wyjątek typu [MyIllegalArgumentException];
- wiersze 8–15: jeśli lista jest pusta, a należało sprawdzić, czy nie jest pusta, zgłaszany jest wyjątek typu [MyIllegalArgumentException];
- wiersz 13: jeśli lista jest pusta, a nie należy sprawdzać, czy jest niepusta, zwracana jest pusta lista elementów typu T. Interfejs [Iterable<T2>] posiada metodę [iterator()], która umożliwia iterację po elementach listy implementującej ten interfejs. Przydatne są dwie metody tego iteratora:
- [itérateur].hasNext(): zwraca wartość true, jeśli lista zawiera jeszcze element do przetworzenia, w przeciwnym razie zwraca wartość false;
- [iterateur].next(): zwraca bieżący element listy i przesuwa wskaźnik o jeden element dalej;
- w rezultacie,
- jeśli argumentem [T2... elements] jest null lub lista pusta, zgłaszany jest wyjątek typu [MyIllegalArgumentException];
- jeśli argumentem [T2... elements] jest pusta lista i było to dopuszczalne, wówczas zwracana jest pusta lista elementów typu T;
Analogiczna metoda istnieje w przypadku, gdy argumentem do sprawdzenia jest typ [T2... elements]:
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
Wróćmy do kodu metody [getShortEntitiesById]:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// poprawność argumentu
List<T> entities = checkNullOrEmptyArgument(true, ids);
// pobieranie w transzach
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// wynik
return entities;
}
- wiersz 7: jeśli dotarliśmy do tego miejsca, oznacza to, że argument [Iterable<Long> ids] jest poprawny;
- wiersze 7–14: zobaczymy później, że metoda [getShortEntitiesById] zostanie zaimplementowana przez typ [PreparedStatement], którego parametrami będzie lista kluczy głównych do wyszukania. Na przykład:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
: ids jest parametrem, którego rzeczywistą wartością będzie typ List<Long>. Każdy element tej listy będzie przedmiotem parametru ? w typie [PreparedStatement]. Stwierdziliśmy jednak, że typ ten akceptuje maksymalną liczbę parametrów, ustaloną przez pole [maxPreparedStatementParameters] klasy;
- wiersz 7: lista encji T, która zostanie zwrócona przez metodę [getShortEntitiesById]. Lista ta będzie tworzona z fragmentów składających się z [maxPreparedStatementParameters] elementów;
- wiersz 9: na podstawie argumentu [Iterable<Long> ids] tworzony jest typ [List<Long> listIds]. Klasa [Lists] jest klasą biblioteki Google Guava, która oferuje wiele metod statycznych do operowania na zbiorach obiektów. Biblioteka Google Guava została zaimportowana (pom.xml) przez projekt Maven [mysql-config-jdbc]:
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- wiersz 10: liczba elementów T, które mają zostać wyszukane w bazie;
- wiersze 11–13: są one wyszukiwane w grupach po [taille = maxPreparedStatementParameters] elementach;
- wiersz 12: obliczenie mające na celu uniknięcie przekroczenia końca listy [listIds];
- wiersz 13: elementy T są uzyskiwane poprzez wywołanie [getShortEntitiesById(listIds.subList(i, limit))]. Metoda ta jest zdefiniowana w klasie poprzez:
abstract protected List<T> getShortEntitiesById(List<Long> ids);
Zatem to klasa potomna będzie wyszukiwać encje T w bazie danych:
- [DaoProduit], jeśli T jest typem [Produit];
- [DaoCategorie], jeśli T jest typem [Categorie];
Korzyści płynące z tej operacji klasy nadrzędnej są dwojakie:
- sygnatura metody [getShortEntitiesById] w klasie potomnej jest unikalna: jej argumentem jest typ [List<Long> ids];
- klasa potomna nie musi zajmować się problemem parametrów [maxPreparedStatementParameters] dla [PreparedStatement]. Zajęła się tym za nią jej klasa nadrzędna;
- wiersz 13: encje zwrócone przez klasę potomną są dodawane do listy encji, która zostanie zwrócona przez klasę nadrzędną (wiersz 16);
Teraz przyjrzyjmy się implementacji drugiej metody klasy, [getShortEntitiesById]:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// poprawność argumentu
List<T> entities = checkNullOrEmptyArgument(true, ids);
// wynik
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
- wiersz 3: zmienił się typ argumentu: Long... ids;
- wiersz 5: sprawdzana jest poprawność tego argumentu;
- wiersz 7: wywoływana jest metoda [getShortEntitiesById], którą właśnie opisaliśmy. Również w tym przypadku korzystamy z klasy [Lists] z biblioteki [Google Guava]. Należy zauważyć, że konieczne jest jawne rzutowanie na typ [Iterable<Long>], aby pomóc kompilatorowi w wyborze właściwej metody, ponieważ metoda [getShortEntitiesById] ma w klasie trzy sygnatury:
- List<T> getShortEntitiesById(Long... ids);
- List<T> getShortEntitiesById(Iterable<Long> ids);
- List<T> getShortEntitiesById(List<Long> ids), która jest abstrakcyjna i zaimplementowana przez klasę potomną;
Nie będziemy dalej omawiać klasy abstrakcyjnej [AbstractDao], która jest klasą nadrzędną dla klas [DaoProduit] i [DaoCategorie]. Warto jedynie zauważyć, że czasami warto wyodrębnić zachowania wspólne dla kilku klas do klasy nadrzędnej, niezależnie od tego, czy jest ona abstrakcyjna, czy nie. Po wykonaniu tej pracy klasy potomne muszą zaimplementować już tylko następujące metody:
// metody zaimplementowane przez klasy potomne ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
Kod z paragrafu 4.8 pokazuje różne typy transakcji używane dla każdej metody. Zwróćmy uwagę na kilka kwestii:
- metody odczytujące bazę danych są oznaczone tagiem [@Transactional(readOnly = true)];
- metody modyfikujące bazę danych są oznaczone adnotacją [@Transactional];
- metody o oznaczeniu [delete] nie są oznaczone i dlatego nie są wykonywane w ramach transakcji. Chodzi o to, że jeśli usunięcie nie powiedzie się, użytkownik prawdopodobnie nie będzie chciał cofnąć wszystkich wcześniejszych operacji, które zakończyły się powodzeniem;
4.9. Klasa [DaoCategorie]
![]() |
![]() |
Klasa [DaoCategorie] implementuje interfejs [IDao<Categorie>], który zapewniadostęp do danych z tabeli [CATEGORIES] w bazie danych MySQL [dbproduitscategories]. Jej szkielet wygląda następująco:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
// stałe
// wstrzyknięcia
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
@Autowired
private IDao<Produit> daoProduit;
@Override
public List<Categorie> getAllShortEntities() {
...
}
@Override
public List<Categorie> getAllLongEntities() {
...
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
...
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
...
}
// --------------------- mapery
class ShortCategorieMapper implements RowMapper<Categorie> {
....
}
class LongCategorieMapper implements RowMapper<Categorie> {
....
}
- wiersz 28: klasa [DaoCategorie] jest komponentem Spring i jako taka może być wstrzykiwana do innych komponentów Spring;
- wiersz 29: klasa [DaoCategorie] rozszerza klasę abstrakcyjną [AbstractDao<Categorie>], co czyni ją implementacją interfejsu [IDao<Categorie>];
- wiersze 34–37: wstrzyknięcie beanów zdefiniowanych w klasie [AppConfig] opisanej w paragrafie 4.4;
- wiersze 38–39: wstrzyknięcie odwołania do klasy [DaoProduit], która implementuje interfejs [IDao<Produit>] zarządzający dostępem do danych z tabeli [PRODUITS];
- wiersze 41–89: implementacja interfejsu [IDao<Categorie>];
- wiersze 95–101: dwie klasy wewnętrzne implementujące interfejs [RowMapper<T>];
Przeanalizujmy metody po kolei.
4.9.1. Metoda [getAllShortEntities]
Metoda [getAllShortEntities] zwraca wszystkie kategorie z tabeli [CATEGORIES] w skróconej wersji:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
Wszystkie metody opierają się na obiekcie [namedParameterJdbcTemplate] zdefiniowanym w pliku konfiguracyjnym Spring i dostarczonym przez bibliotekę Spring JDBC. Obiekt ten posiada wiele metod. Metoda wykorzystana powyżej jest następująca:
![]()
- [sql] to polecenie SQL, które ma zostać wykonane;
- [rowMapper] jest instancją następującego interfejsu [RowMapper<T>]:

Zasada działania jest następująca:
- metoda [namedParameterJdbcTemplate].query(String sql, RowMapper<T> rowMapper) wykonuje polecenie SQL typu [Select]. Obsługuje ewentualne wyjątki, a także otwieranie i zamykanie połączenia z SGBD. Jedyną rzeczą, której nie może zrobić, jestenkapsulować elementy [ResultSet] z obiektów, które otrzymuje, w typie [Categorie], ponieważ nie zna powiązania między polami typu [Categorie] a kolumnami [Resultset]. W dalszej części zobaczymy, że powiązanie to jest tworzone za pomocą technologii JPA, co spowoduje automatyczne enkapsulowanie elementów typu [ResultSet] w instancjach typu T. Na razie drugim parametrem metody [query] jest instancja interfejsu [RowMapper<T>] zdolna do przeprowadzenia tego enkapsulowania;
Wróćmy do kodu:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
Kolejność SQL [ConfigJdbc.SELECT_ALLSHORTCATEGORIES] jest następująca:
public final static String SELECT_ALLSHORTCATEGORIES = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c";
Zapytanie dotyczy kolumn [ID, VERSIONING, NOM] z elementów tabeli [CATEGORIES]. Będziemy konsekwentnie stosować następującą składnię:
SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...
Ważne jest nazewnictwo kolumn uzyskanych przez SELECT z atrybutem [as nom_colonne]. Jest to jedyny sposób na zapewnienie kompatybilności między programami typu SGBD, ponieważ wszystkie one stosują własny sposób nazewnictwa kolumn uzyskanych za pomocą programu typu SELECT, w którym kolumny z różnych tabel mają tę samą nazwę (na przykład ID, NOM lub VERSIONING w naszym przypadku). Eliminujemy tę niejednoznaczność, samodzielnie określając nazwy, jakie powinny nosić te kolumny.
Klasa wewnętrzna [ShortCategorieMapper] wygląda następująco:
class ShortCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSIONING"), rs.getString("c_NOM"), null);
}
}
- wiersz 1: klasa [ShortCategorieMapper] implementuje interfejs [RowMapper<Categorie>] i w związku z tym musi implementować metodę [mapRow] z wierszy 4–5, której zadaniem jest enkapsulacja wiersza klasy [ResultSet rs], wygenerowanego przez polecenie [SELECT], w typie [Categorie];
- wiersz 5: enkapsulacja ta została przeprowadzona. Należy zwrócić uwagę, że nazwa używana przez metody [rs.getType(nom)] jest tą samą nazwą, która występuje w atrybutach [as nom] kolumn pliku SELECT;
W ten sposób uzyskaliśmy listę kategorii w ich skróconej wersji bez konieczności obsługi wyjątków ani nawiązywania połączenia. Na tym polega zaleta biblioteki Spring JDBC, która zajmuje się wszystkim, co można uogólnić w zarządzaniu elementami tabeli, pozostawiając programiście to, czego nie da się uogólnić.
4.9.2. Metoda [getAllLongEntities]
Metoda [getAllLongEntities] zwraca wszystkie kategorie z tabeli [CATEGORIES] w ich pełnej wersji:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
Kolejność SQL [ConfigJdbc.SELECT_ALLLONGCATEGORIES] jest następująca:
public final static String SELECT_ALLLONGCATEGORIES = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID";
Chodzi o powiązanie kategorii z ich produktami. Osiąga się to poprzez połączenie tabeli [CATEGORIES] z tabelą [PRODUITS] za pomocą klucza obcego [CATEGORIE_ID], któryłączy tabelę [PRODUITS] z tabelą [CATEGORIES]. Składnia [FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID] pozwala również na pobranie kategorii, które nie mają przypisanych produktów. W tym przypadku zapytanie SELECT zwraca kategorię i produkt wraz ze wszystkimi kolumnami do tabeli NULL.
Klasa [LongCategorieMapper] ma następującą postać:
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// przypadek kategorii bez produktów
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
- wiersz 4: metoda [mapRow] musi zwrócić obiekt [Categorie] z wypełnionym polem [produits], na podstawie wiersza z obiektu [ResultSet] pochodzącego z poprzedniego zlecenia SELECT;
W rezultacie operacja:
[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]
zwróci listę o następującej postaci:
gdzie każda kategoria [ci] będzie miała pole [produits], które będzie listą produktów zawierającą tylko jeden element [produitsij]. Potrzebujemy jednak następującej listy:
gdzie każda kategoria [ci] będzie miała pole [produits], które będzie zawierało listę produktów [produiti1, produiti2, ...]. Osiąga się to poprzez przekazanie uzyskanej listy kategorii do prywatnej metody [filterCategories]:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
Metoda [filterCategories] wygląda następująco:
private List<Categorie> filterCategories(List<Categorie> categories) {
if (categories.size() == 0) {
return categories;
}
// kategorie do wyświetlenia
List<Categorie> cats = new ArrayList<Categorie>();
// przeglądamy listę uzyskanych kategorii
for (Categorie categorie : categories) {
boolean trouve = false;
for (Categorie cat : cats) {
if (categorie.equals(cat)) {
cat.addProduit(categorie.getProduits().get(0));
trouve = true;
break;
}
}
// znaleziono?
if (!trouve) {
cats.add(categorie);
}
}
// wynik
return cats;
}
- wiersz 1: [List<Categorie> categories] to lista kategorii do filtrowania (lub grupowania);
- wiersz 6: lista kategorii, które mają zostać zwrócone wywołującemu;
- wiersze 8–21: przetwarzana jest każda kategoria z listy do filtrowania;
- wiersze 10–16: sprawdzane jest, czy bieżąca kategoria [categorie] występuje już na liście kategorii [cats], która ma zostać utworzona (przypominamy, że dwie kategorie są uznawane za równe, jeśli mają ten sam klucz pierwotny, por. paragraf 4.6);
- wiersze 11–14: jeśli tak jest, wówczas produkt zawarty w kategorii [categorie] jest dodawany do listy produktów kategorii [cat];
- wiersze 18–20: jeśli bieżąca kategoria [categorie] nie występuje jeszcze na liście kategorii [cats] przeznaczonych do utworzenia, wówczas dodaje się ją do tej listy wraz z jej listą produktów zawierającą jeden element;
Przyjrzyjmy się przypadkowi, w którym polecenie SQL Select zwraca kategorie bez powiązanych produktów. Który obiekt zwraca klasę [LongCategorieMapper]?
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// przypadek kategorii bez produktów
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
W przypadku, gdy zapytanie „SQL Select” zwróciło kategorię bez produktów, kolumny produktu zwróconego wraz z kategorią zawierają wartość „SQL NULL”. Przypadek ten jest omówiony w wierszach 7–9:
- wiersz 7: pobierany jest klucz podstawowy produktu jako liczba całkowita typu long;
- wiersz 9: sprawdzamy, czy odczytana wartość to SQL NULL (rs.wasNull). Jeśli tak nie jest, dodajemy produkt do listy z wiersza 6, w przeciwnym razie nic nie jest dodawane, a lista produktów pozostaje pusta.
Należy zauważyć, że w każdym przypadku zwracana jest kategoria z polem [produits], które nie jest równe null.
4.9.3. Metoda [getShortEntitiesById]
Metoda [getShortEntitiesById] jest analogiczna do metody [getAllShortEntities], z tą różnicą, że zwraca jedynie encje, których klucze główne są wymienione na liście:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
- w wierszu 4 podpis używanej metody [query] wygląda następująco:

Pierwszym parametrem jest skonfigurowane polecenie SQL [Select]. Drugim jest słownik przypisujący każdemu z parametrów odpowiednią wartość. Trzecim jest instancja klasy, która dokonuje enkapsulacji wiersza z pliku [ResultSet] – będącego wynikiem działania [Select] – w obiekcie typu T;
- wiersz 4: zlecenie SQL [Select] z ustawionymi parametrami ma następującą postać:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
To polecenie pobiera z tabeli [CATEGORIES] kategorie, których klucze główne znajdują się na liście: ids.
- wiersz 5: drugim parametrem metody [query] jest tutaj słownik łączący klucz „ids” (pierwszy parametr) z listą [ids], przekazaną w wierszu 1 jako parametr do metody [getShortEntitiesById]. Klasa [Collections] należy do biblioteki [Google Guava], o której już wspominaliśmy. [Collections.singleMap] zwraca słownik zawierający jeden element;
- wiersz 5: klasą odpowiedzialną za enkapsulację wiersza z [ResultSet] – będącego wynikiem działania [Select] – w obiekcie typu [Categorie] jest już omówiona klasa [ShortCategorieMapper];
Właśnie w tym miejscu zazwyczaj wkracza bean [maxPreparedStatementParameters]. Parametr [:ids] zlecenia SQL, który reprezentuje listę kluczy głównych, może zawierać od 1 do kilku tysięcy parametrów. Liczba ta podlega ograniczeniu, które zależy od konkretnego SGBD. W przypadku MySQL udało się przekazać 10 000 parametrów bez błędów; nie testowano wartości wyższych. W przypadku serwera SQL oficjalny limit wynosi 2100. W przypadku Firebirda 1000 było już za dużo. Zredukowaliśmy tę liczbę do 100. Ogólnie rzecz biorąc, nie testowaliśmy maksymalnego limitu tej liczby dla poszczególnych plików SGBD.
4.9.4. Metoda [getLongEntitiesById]
Metoda [getLongEntitiesById] jest analogiczna do metody [getShortEntitiesById], z tą różnicą, że zwraca długie wersje kategorii:
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
Wiersz 4, zapytanie SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYID] ma następującą postać:
public final static String SELECT_LONGCATEGORIE_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.ID in (:ids)";
4.9.5. Metoda [getShortEntitiesByName]
Metoda [getShortEntitiesByName] jest analogiczna do metody [getShortEntitiesById], z tą różnicą, że kategorie są wyszukiwane na podstawie ich nazw, a nie kluczy głównych:
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
W wierszu 4 kolejność SQL [ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME] jest następująca:
public final static String SELECT_SHORTCATEGORIE_BYNAME = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.NOM in (:noms)";
4.9.6. Metoda [getLongEntitiesByName]
Metoda [getLongEntitiesByName] jest analogiczna do metody [getShortEntitiesByName], z tą różnicą, że kategorie są wyszukiwane w ich pełnych wersjach:
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(215, e, simpleClassName);
}
}
W wierszu 4 kolejność SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME] jest następująca:
public final static String SELECT_LONGCATEGORIE_BYNAME = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.NOM in(:noms)";
4.9.7. Metoda [deleteAllEntities]
Metoda [deleteAllEntities] usuwa wszystkie kategorie z tabeli [CATEGORIES]:
@Override
public void deleteAllEntities() {
try {
// usuwamy wszystkie kategorie, a w konsekwencji wszystkie produkty
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_ALLCATEGORIES, (Map<String, Object>) null);
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
- wiersz 4: zastosowana metoda [namedParameterJdbcTemplate.update] ma następującą sygnaturę:
![]()
Pierwszym parametrem jest zlecenie SQL skonfigurowane do aktualizacji (INSERT, UPDATE, DELETE). Drugim parametrem jest słownik przypisujący wartości do poszczególnych parametrów zlecenia SQL. Metoda zwraca liczbę wierszy zaktualizowanych przez zlecenie SQL.
- wiersz 4: zlecenie SQL [ConfigJdbc.DELETE_ALLCATEGORIES] ma następującą postać:
public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";
Nie jest to więc polecenie z parametrami. Dlatego drugi parametr metody [update] ma wartość null.
4.9.8. Metoda [deleteAllEntitiesById]
Metoda [deleteAllEntitiesById] usuwa kategorie z tabeli [CATEGORIES], podając jej klucze główne:
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYID, Collections.singletonMap("ids", ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
W wierszu 4 kolejność SQL [ConfigJdbc.DELETE_CATEGORIESBYID] jest następująca:
public final static String DELETE_CATEGORIESBYID = "DELETE FROM CATEGORIES WHERE ID in (:ids)";
4.9.9. Metoda [deleteAllEntitiesByName]
Metoda [deleteAllEntitiesByName] usuwa kategorie z tabeli [CATEGORIES], podając ich nazwy:
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYNAME, Collections.singletonMap("noms", names));
} catch (Exception e) {
throw new DaoException(225, e, simpleClassName);
}
}
W wierszu 4 polecenie SQL [ConfigJdbc.DELETE_CATEGORIESBYNAME] ma następującą postać:
public final static String DELETE_CATEGORIESBYNAME = "DELETE FROM CATEGORIES WHERE NOM in (:noms)";
4.9.10. Metoda [saveEntities]
4.9.10.1. Kod
Sygnatura tej metody jest następująca:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
Metoda przyjmuje jako parametr listę kategorii. Wykonuje na niej następujące operacje:
- jeśli kategoria ma klucz główny null, wykonywana jest operacja SQL INSERT; w przeciwnym razie wykonywana jest operacja SQL UPDATE;
- operacja ta jest powtarzana dla każdego produktu w tej kategorii;
Metoda zwraca listę kategorii, które zostały zapisane lub zaktualizowane. Zwracana lista stanowi dokładne odzwierciedlenie kategorii i produktów obecnych w tabelach, z uwzględnieniem wersji: wersje te nie są bowiem modyfikowane w aktualizowanych encjach, mimo że zostały zwiększone w bazie danych.
Jest to zdecydowanie najbardziej złożona metoda. Jej kod wygląda następująco:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// --------------------------------------------- kategorie
List<Categorie> insertCategories = new ArrayList<Categorie>();
List<Categorie> updateCategories = new ArrayList<Categorie>();
// przeszukiwanie kategorii
for (Categorie categorie : entities) {
// dodać czy zaktualizować?
if (categorie.getId() == null) {
insertCategories.add(categorie);
} else {
updateCategories.add(categorie);
}
}
// dodawanie kategorii
if (insertCategories.size() > 0) {
insertCategories(insertCategories);
}
// aktualizacje kategorii
if (updateCategories.size() > 0) {
updateCategories(updateCategories);
}
// --------------------------------------------- produkty
// aktualizujemy produkty w kategoriach
List<Produit> allProduits = new ArrayList<Produit>();
for (Categorie categorie : entities) {
List<Produit> produits = categorie.getProduits();
Long idCategorie = categorie.getId();
if (produits != null) {
// dodaje się go do listy wszystkich produktów
allProduits.addAll(produits);
// produkty są skanowane pojedynczo w celu przypisania ich do odpowiedniej kategorii
for (Produit produit : produits) {
// produkt jest przypisywany do odpowiedniej kategorii
produit.setIdCategorie(idCategorie);
produit.setCategorie(categorie);
}
}
}
// dodawanie / aktualizacja produktów
daoProduit.saveEntities(allProduits);
// wynik
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(207, e, simpleClassName);
}
}
- wiersze 5–23: wstawianie lub aktualizacja kategorii;
- wiersze 26–43: dodawanie lub aktualizacja produktów;
- wiersze 35–39: kod ten powiązuje każdy produkt z jego kategorią. W poprzednim etapie dodawania kategorii przypisano im klucz główny, który należy umieścić w polu [idCategorie] produktu (wiersz 37). Ponadto wiersze 37–38 pozwalają skorygować sytuacje, w których użytkownik nieprawidłowo powiązał dany produkt z kategorią. Aby powiązanie to było poprawne, należy użyć metody [Categorie] .add(Produkt p), jednak nic nie stoi na przeszkodzie, aby użytkownik dodał produkt bezpośrednio do listy produktów w danej kategorii, pomijając tę metodę, co wiąże się z ryzykiem nieprawidłowego wypełnienia pól [idCategorie, categorie] produktu p;
- wiersz 43: przekazujemy instancji interfejsu [IDao<Produit>] zadanie zapisywania / aktualizowania produktów. Przypominamy, że instancja ta została wstrzyknięta do klasy [DaoCategorie]:
@Autowired
private IDao<Produit> daoProduit;
4.9.10.2. Wstawianie kategorii
Kategorie są wstawiane do tabeli [CATEGORIES] za pomocą następującej prywatnej metody [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// kategorie do dodania
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// zapisujemy klucz główny
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// wszystko to jest OK – przypisuje się klucze główne do zachowanych kategorii
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// wynik
return categories;
}
- wiersz 6: wykorzystuje się bean [simpleJdbcInsertCategorie] wstrzyknięty do klasy przez następujące wiersze:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
Bean ten jest zdefiniowany w klasie [AppConfig] projektu w następujący sposób:
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_CATEGORIES_ID)
.usingColumns(ConfigJdbc.TAB_CATEGORIES_NOM);
}
- w wierszu 5 klasa [SimpleJdbcInsert] jest klasą biblioteki Spring JDBC (wiersz 1):
- parametr konstruktora [SimpleJdbcInsert] stanowi źródło danych, na którym wykonywana jest operacja;
- klauzula [withTableName] pozwala wskazać tabelę, do której ma zostać wstawiony element, w tym przypadku tabelę [CATEGORIES];
- klauzula [usingGeneratedKeyColumns] pozwala określić kolumnę automatycznie generowanego klucza głównego, w tym przypadku kolumnę [ID];
- klauzula [usingColumns] pozwala ograniczyć wstawianie do określonych kolumn. W tym przypadku wykluczamy kolumnę [ID], która jest generowana automatycznie przez SGBD, oraz kolumnę [VERSIONING], której wartość domyślna wynosi 1;
Wróćmy do kodu metody [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// kategorie do dodania
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// zapisujemy klucz główny
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// wszystko to jest OK – przypisuje się klucze główne do trwałych kategorii
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// wynik
return categories;
}
- wiersz 6: wykorzystywana jest metoda [simpleJdbcInsertCategorie.executeAndReturnKey]:
![]()
Metoda ta oczekuje jako parametr słownika, który tworzy powiązania między kolumnami tabeli a wartościami, które mają zostać w nich wstawione. Zwraca jako wynik klucz główny w postaci typu [Number]. Metoda [Number.longValue()] pozwala uzyskać klucz główny w postaci typu [Long].
Metoda [getMapForCategorie] jest następującą metodą prywatną:
private Map<String, ?> getMapForCategorie(Categorie categorie) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_CATEGORIES_NOM, categorie.getNom());
return map;
}
Klucze słownika to nazwy kolumn, które należy wypełnić ([NOM]), a wartości słownika to wartości, które należy wstawić do tych kolumn.
- wiersz 8 [insertCategories]: odzyskany klucz główny jest zapisywany w słowniku. Zanim przypiszemy klucze główne do poszczególnych rekordów, poczekamy, aż upewnimy się, że wszystkie rekordy zostały wstawione. W przypadku wystąpienia wyjątku wszystkie operacje wstawiania zostaną cofnięte, a chcemy, aby wówczas rekordy [categories] z wiersza 1 również pozostały niezmienione;
- wiersze 14–17: teraz, gdy mamy pewność, że wszystko przebiegło pomyślnie, przypisujemy wygenerowane klucze główne do kategorii;
- wiersz 19: zwracamy listę kategorii wraz z ich kluczami głównymi;
4.9.10.3. Aktualizacja kategorii
Kategorie są aktualizowane za pomocą następującej prywatnej metody [updateCategories]:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// aktualizacja kategorii w bazie danych
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// czy się udało?
Long idCategorie = null;
if (nbLignes == 0) {
// nie udało się – szukamy przyczyny
// szukamy kategorii w bazie
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// kategoria nie istnieje
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// wersja była nieprawidłowa
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
Aktualizacja kategorii C1 w bazie danych za pomocą kategorii C2 znajdującej się w pamięci jest dozwolona tylko wtedy, gdy kategorie C1 i C2 mają tę samą wersję. Ten numer wersji służy do zapobiegania jednoczesnej aktualizacji encji przez dwóch różnych użytkowników: dwaj użytkownicy o numerach U1 i U2 odczytują encję E o numerze wersji równym V1. U1 modyfikuje E i zapisuje tę zmianę w bazie danych: numer wersji zmienia się wówczas na V1+1. U2 z kolei modyfikuje E i zapisuje tę zmianę w bazie: otrzyma wyjątek, ponieważ posiada wersję (V1) inną niż ta w bazie (V1+1).
- wiersze 2–29: blok try ma dwa bloki catch:
- pierwszy, w wierszu 25, służy do przepuszczenia ewentualnego wyjątku typu [DaoException] wygenerowanego przez kod w wierszu 13;
- drugi, w wierszu 27, służy do obsługi pozostałych typów wyjątków;
- wiersz 3: skanujemy wszystkie kategorie, które mają zostać zaktualizowane;
- wiersz 4: aktualizujemy bieżącą kategorię za pomocą metody [namedParameterJdbcTemplate.update]:

- przeanalizujmy tę instrukcję:
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES, new BeanPropertySqlParameterSource(categorie));
Sekwencja poleceń SQL [ConfigJdbc.UPDATE_CATEGORIES] wygląda następująco:
public final static String UPDATE_CATEGORIES = "UPDATE CATEGORIES SET VERSIONING=VERSIONING+1, NOM=:nom WHERE ID=:id AND VERSIONING=:version";
Polecenie ma trzy parametry (:id, :version, :nom), których wartości znajdują się w polach o tych samych nazwach w zmodyfikowanym obiekcie [categorie]. Wykorzystujemy tę cechę, przekazując jako drugi parametr [new BeanPropertySqlParameterSource(categorie)], co oznacza: „wartości parametrów znajdują się w polach o tych samych nazwach w tym Java bean”;
Wynikiem tej operacji, gdy przebiega ona normalnie, jest liczba zmodyfikowanych wierszy, a więc 0 lub 1.
Wróćmy do analizowanego kodu:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// aktualizacja kategorii w bazie danych
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// czy operacja się powiodła?
Long idCategorie = null;
if (nbLignes == 0) {
// nie udało się – szukamy przyczyny
// szukamy kategorii w bazie
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// kategoria nie istnieje
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// wersja była nieprawidłowa
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
- wiersz 9: sprawdzamy, czy modyfikacja zakończyła się powodzeniem;
- wiersz 10: modyfikacja nie powiodła się. Ponieważ klauzula [WHERE] obejmuje kolumny [ID] i [VERSIONING], szukamy kolumny, która spowodowała niepowodzenie klauzuli [WHERE];
- wiersze 12–18: sprawdzamy, czy klucz [id] kategorii znajduje się w bazie. Jeśli tak nie jest, uruchamiamy [RuntimeException] z odpowiednim komunikatem o błędzie;
- wiersze 19–22: obsługują przypadek, w którym to wersja była nieprawidłowa;
4.10. Klasa [DaoProduit]
![]() |
![]() |
Klasa [DaoProduit] implementuje interfejs [IDao<Produit>], który zapewniadostęp do danych z tabeli [PRODUITS] w bazie danych MySQL [dbproduitscategories]. Jej szkielet wygląda następująco:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
// wstawki
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
@Override
public List<Produit> getAllShortEntities() {
...
}
@Override
public List<Produit> getAllLongEntities() {
....
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
....
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
....
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
}
// --------------------- mapery
class ShortProduitMapper implements RowMapper<Produit> {
...
}
class LongProduitMapper implements RowMapper<Produit> {
...
}
Kod jest bardzo podobny do kodu klasy [DaoCategorie]. Omówimy tylko kilka metod.
4.10.1. Metoda [getShortEntitiesById]
Metoda [getShortEntitiesById] zwraca skróconą wersję produktów, dla których przekazano klucze główne:
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTPRODUIT_BYID,
Collections.singletonMap("ids", ids), new ShortProduitMapper());
} catch (Exception e) {
throw new DaoException(109, e, simpleClassName);
}
}
- wiersz 4: polecenie SQL Select [ConfigJdbc.SELECT_SHORTPRODUIT_BYID] ma następującą postać:
public final static String SELECT_SHORTPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSIONING, p.NOM as p_NOM, p.CATEGORIE_ID as p_CATEGORIE_ID, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION FROM PRODUITS p WHERE p.ID in (:ids)";
- wiersz 4: klasa [ShortProduitMapper], odpowiedzialna za umieszczenie [ResultSet] na liście produktów, wygląda następująco:
class ShortProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSIONING"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), null);
}
}
4.10.2. Metoda [getLongEntitiesByName]
Metoda [getShortEntitiesById] generuje rozszerzoną wersję produktów, których nazwy są przekazywane:
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
- wiersz 4: polecenie SQL Select [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] wygląda następująco:
public final static String SELECT_LONGPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p, CATEGORIES c WHERE p.ID in (:ids) AND p.CATEGORIE_ID=c.ID";
- wiersz 4: klasa [LongProduitMapper], odpowiedzialna za umieszczenie elementów z [ResultSet] w produktach (wersja rozszerzona), ma następujący wygląd:
class LongProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSION"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null));
}
}
4.10.3. Metoda [saveEntities]
Metoda [saveEntities] jest używana zarówno do dodawania nowych produktów (id==null), jak i do aktualizowania istniejących produktów (id!=null):
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
try {
// produkty do dodania
List<Produit> insertProduits = new ArrayList<Produit>();
// produkty do aktualizacji
List<Produit> updateproduits = new ArrayList<Produit>();
// przeprowadzane jest skanowanie listy otrzymanych elementów
for (Produit produit : entities) {
Long id = produit.getId();
if (id == null) {
insertProduits.add(produit);
} else {
updateproduits.add(produit);
}
}
// dodatki
insertProduits(insertProduits);
// zmiany
updateProduits(updateproduits);
// wynik
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
W wierszu 18 produkty są dodawane za pomocą następującej prywatnej metody [insertProduits]:
private List<Produit> insertProduits(List<Produit> produits) {
Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
try {
// produkty do dodania
for (Produit produit : produits) {
Number newId = simpleJdbcInsertProduit.executeAndReturnKey(getMapForProduit(produit));
// odnotowuje się klucz główny
mapProduits.put(newId.longValue(), produit);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// wszystko to jest OK – przypisuje się klucze główne do zapisanych produktów
for (Long id : mapProduits.keySet()) {
Produit produit = mapProduits.get(id);
produit.setId(id);
}
// wynik
return produits;
}
private Map<String, ?> getMapForProduit(Produit produit) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_PRODUITS_NOM, produit.getNom());
map.put(ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, produit.getIdCategorie());
map.put(ConfigJdbc.TAB_PRODUITS_PRIX, produit.getPrix());
map.put(ConfigJdbc.TAB_PRODUITS_DESCRIPTION, produit.getDescription());
return map;
}
Metoda ta jest analogiczna do metody [insertCategories] omówionej w paragrafie 4.9.10.3.
- wiersz 4: wykorzystuje się bean [simpleJdbcInsertProduit], który został wstrzyknięty do klasy:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
Ten bean został zdefiniowany w klasie [AppConfig], która konfiguruje projekt:
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource)
.withTableName(ConfigJdbc.TAB_PRODUITS)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_PRODUITS_ID)
.usingColumns(ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION,ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID);
}
- wiersze 3–6: bean [simpleJdbcInsertProduit]
- jest powiązany ze źródłem danych bazy [dbproduitscategories] (wiersz 3) oraz z tabelą [ConfigJdbc.TAB_PRODUITS] tego źródła (wiersz 4);
- klucz główny tej tabeli jest generowany w kolumnie [ConfigJdbc.TAB_PRODUITS_ID] (wiersz 5);
- wartości przypisuje się wyłącznie w kolumnach [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] (wiersz 6);
Metoda [updateProduits], która aktualizuje produkty (wiersz 20 w [saveEntities]), wygląda następująco:
private void updateProduits(List<Produit> updateProduits) {
try {
// przeszukiwanie produktów
for (Produit produit : updateProduits) {
// aktualizacja produktu w bazie
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_PRODUITS,
new BeanPropertySqlParameterSource(produit));
// czy operacja się powiodła?
Long idProduit = null;
if (nbLignes == 0) {
// nie udało się – szukamy przyczyny
// wyszukiwanie produktu w bazie
idProduit = produit.getId();
List<Produit> produitsInBd = getShortEntitiesById(idProduit);
if (produitsInBd.size() == 0) {
// produkt nie istnieje
throw new RuntimeException(String.format("Erreur de mise à jour. Le produit de clé [%s] n'existe pas",
idProduit));
} else {
// wersja była nieprawidłowa
throw new RuntimeException(String.format(
"Erreur de mise à jour. Le produit de clé [%s] n'a pas la bonne version", idProduit));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
Jest ona analogiczna do tej, która aktualizuje kategorie (por. punkt 4.9.10.3). W wierszu 23 polecenie SQL [ConfigJdbc.UPDATE_PRODUITS], wykonane w celu aktualizacji produktów, ma następującą postać:
public final static String UPDATE_PRODUITS = "UPDATE PRODUITS SET VERSIONING=VERSIONING+1, NOM=:nom, PRIX=:prix, CATEGORIE_ID=:idCategorie, DESCRIPTION=:description WHERE ID=:id AND VERSIONING=:version";
Nazwy parametrów [:id,:version,:nom,:prix,:idCategorie,:description] są jednocześnie nazwami pól klasy [Produit], co pozwala na wykorzystanie instrukcji z wierszy 6–7 do aktualizacji bieżącego produktu.
4.11. Warstwa testowa
![]() |
![]() |
Warstwa testowa składa się z trzech klas testowych:
- [JUnitTestCheckArguments]: testy tej klasy wywołują różne metody warstwy [DAO] z niedozwolonymi argumentami i sprawdzają, czy reagują one poprawnie;
- [JUnitTestDao]: testy tej klasy wywołują różne metody warstwy [DAO] i sprawdzają, czy działają one zgodnie z oczekiwaniami;
- [JUnitTestPushTheLimits] nie służy do testowania warstwy [DAO], lecz do pomiaru jej wydajności;
Ta warstwa testowa odgrywa ważną rolę w niniejszym dokumencie. Jest ona bowiem wspólna dla wszystkich implementacji interfejsu [IDao<T>]. Jest ich sześć na SGBD (1 implementacja JDBC, 3 implementacje JPA, 1 implementacja Spring MVC, 1 zabezpieczona implementacja Spring MVC), co daje łącznie 36 dla sześciu testowanych SGBD. Warstwa testowa pozwala nam sprawdzić, czy wszystkie implementacje reagują w ten sam sposób.
4.11.1. Test [JUnitTestCheckArguments]
Klasa testowa [JUnitTestCheckArguments] zawiera 48 metod, które sprawdzają reakcję metod warstwy [DAO] w przypadku wywołania ich z nieprawidłowymi argumentami. Jej szkielet wygląda następująco:
package spring.jdbc.tests;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestCheckArguments {
// warstwa [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// dane lokalne
private Iterable<String> names1 = null;
private Iterable<String> names2 = Lists.newArrayList(new String[0]);
private String[] names3 = null;
private String[] names4 = new String[0];
private Iterable<Long> ids1 = null;
private Iterable<Long> ids2 = Lists.newArrayList(new Long[0]);
private Long[] ids3 = null;
private Long[] ids4 = new Long[0];
private Iterable<Categorie> categories1 = null;
private Iterable<Categorie> categories2 = Lists.newArrayList(new Categorie[0]);
private Categorie[] categories3 = null;
private Categorie[] categories4 = new Categorie[0];
private Iterable<Produit> produits1 = null;
private Iterable<Produit> produits2 = Lists.newArrayList(new Produit[0]);
private Produit[] produits3 = null;
private Produit[] produits4 = new Produit[0];
...
}
- wiersz 19: test JUnit zostanie przeprowadzony w ramach integracji z frameworkiem Spring;
- wiersz 18: przed rozpoczęciem testów zostaną utworzone instancje beanów zdefiniowanych w klasie [AppConfig] projektu;
- wiersze 23–26: wstrzyknięcie instancji każdego z dwóch interfejsów warstwy [DAO];
- wiersze 29–44: nieprawidłowe parametry wywołania metod warstwy [DAO];
- wiersz 29: wskaźnik null typu [Iterable<String>] jako lista nazw;
- wiersz 30: pusta lista typu [Iterable<String>] jako lista nazw;
- wiersz 29: wskaźnik null typu String[] jako tablica nazw;
- wiersz 30: pusta tablica typu String[] jako tablica nazw;
- ...
W przypadku pola [names1] przeprowadzamy na przykład następujący test:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName1() {
daoProduit.getShortEntitiesByName(names1);
}
- wiersz 1: określa się, że test [getShortProduitsByName1] musi wygenerować wyjątek typu [MyIllegalArgumentException]
W przypadku pola [names2] przeprowadzamy na przykład następujący test:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName2() {
daoCategorie.getLongEntitiesByName(names2);
}
W polu [names3] przeprowadza się na przykład następujący test:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName3() {
daoCategorie.getLongEntitiesByName(names3);
}
W przypadku pola [names4] przeprowadzamy na przykład następujący test:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName4() {
daoProduit.getShortEntitiesByName(names4);
}
W ten sposób przeprowadza się 48 testów, aby sprawdzić wszystkie możliwe przypadki. Uruchamia się konfigurację wykonania o nazwie [spring-jdbc-generic-04-JUnitTestCheckArguments] [1]. Uzyskany wynik to [2]:
![]() |
4.11.2. Test [JUnitTestDao]
Test [JUnitTestDao] wywołuje metody warstwy [DAO] z prawidłowymi argumentami i sprawdza, czy metody działają zgodnie z oczekiwaniami. Łącznie istnieje 74 testów sprawdzających operacje wstawiania, wybierania, aktualizowania i usuwania encji, kategorii lub produktów. Łącznie jest to ponad 1000 linii kodu. Przyjrzymy się tylko niektórym z tych metod.
4.11.2.1. Szkielet testu
Klasa [JUnitTestDao] ma następujący szkielet:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {
// kontekst Spring
@Autowired
private ApplicationContext context;
// warstwa [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// stałe
private final int NB_PRODUITS = 5;
private final int NB_CATEGORIES = 2;
// lokalne
// lokalne
private Map<Long, Categorie> mapCategories = new HashMap<Long, Categorie>();
private Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
@Before
public void clean() {
// przed każdym testem czyścimy bazę danych
log("Vidage de la base de données", 1);
// opróżnia się tabelę [CATEGORIES], a następnie kaskadowo tabelę [PRODUITS]
daoCategorie.deleteAllEntities();
// opróżnia się słowniki
for (Long id : mapCategories.keySet()) {
mapCategories.remove(id);
}
for (Long id : mapProduits.keySet()) {
mapProduits.remove(id);
}
}
...
}
- wiersze 27–28: podobnie jak w przypadku testu [JUnitTestCheckArguments], mamy do czynienia z testem zintegrowanym ze Springiem i skonfigurowanym przez klasę [AppConfig] z projektu;
- wiersze 32–33: wstrzyknięcie kontekstu Springa, który zapewnia dostęp do wszystkich jego beanów;
- wiersze 35–36: wstrzyknięcie instancji interfejsu [IDao<Produit>] testowanego przez klasę;
- wiersze 37–38: wstrzyknięcie instancji interfejsu [IDao<Categorie>] testowanego przez klasę;
- wiersze 41–42: gdy test będzie wymagał danych z bazy, wygenerowana zostanie baza [NB_CATEGORIES] „kategorie”, z których każda będzie zawierała bazę [NB_PRODUITS] „produkty”. W ten sposób w tabeli [CATEGORIES] znajdą się kategorie [NB_CATEGORIES], a w tabeli [PRODUITS] – produkty [NB_CATEGORIES] * [NB_PRODUITS];
- wiersze 46–47: dwa słowniki, w których będą przechowywane produkty i kategorie;
- wiersze 49–62: metoda [clean] jest uruchamiana przed każdym testem (wiersz 49). W wierszu 54 opróżnia się tabelę [CATEGORIES]. Należy tutaj pamiętać, że tabela [PRODUITS] posiada klucz główny [CATEGORIE_ID] w kolumnie ID tabeli [CATEGORIES], a ten jest zdefiniowany w następujący sposób:
![]() |
- (ciąg dalszy)
- w [1-3], klucz obcy [CATEGORIE_ID] z tabeli [PRODUITS]. Odnosi się on do kolumny [ID] w tabeli [CATEGORIES] [4-5];
- gdy usunięta zostanie kategoria, usunięte zostaną również wszystkie powiązane z nią produkty [6]. Należy zwrócić uwagę na tę kwestię, ponieważ jest ona wykorzystywana przy tworzeniu warstwy [DAO] opartej na bazie [dbproduitscategories];
Zatem po usunięciu zawartości tabeli [CATEGORIES] zawartość tabeli [PRODUITS] również zostanie usunięta.
- wiersze 56–58: opróżnia się słownik kategorii;
- wiersze 59–61: wykonuje się to samo z słownikiem produktów;
Należy pamiętać, że przed każdym testem w bazie danych znajdują się puste tabele, a w pamięci – puste słowniki.
4.11.2.2. Metoda [verifyClean]
Metoda [verifyClean] sprawdza, czy po wykonaniu metody [clean] tabele są puste:
@Test
public void verifyClean() {
log("verifyClean", 1);
List<Categorie> categories = daoCategorie.getAllShortEntities();
Assert.assertEquals(0, categories.size());
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.2.3. Metoda [fillDataBase]
Ta metoda sprawdza, czy baza danych została poprawnie wypełniona danymi testowymi:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// wypełnianie bazy danych i słowników
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// wyświetlanie
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// kilka sprawdzeń
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// słowniki muszą być już opróżnione
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
Test ten wykorzystuje kilka metod prywatnych:
- [fill], wiersz 4, która wypełnia bazę danymi testowymi;
- [registerCategories], wiersz 4, który wypełnia słowniki danymi zwróconymi przez metodę [fill]. Te dwa słowniki reprezentują zapisaną w bazie dane;
- [showDataBase], wiersz 6, który odczytuje dwie tabele [CATEGORIES] i [PRODUITS] oraz zwraca odczytane dane;
- [checkShortCategorie], wiersz 13, weryfikuje kategorię odczytaną przez [showDataBase]. Sprawdza, czy skrócona nazwa tej kategorii odpowiada tej zapisanej w słowniku kategorii;
- [checkShortProduit], wiersz 16, wykonuje tę samą czynność dla produktów;
- gdy dana jednostka zostanie znaleziona w słowniku, jest ona z niego usuwana. Wiersze 19–20 sprawdzają, czy oba słowniki są puste. Jeśli oba te stwierdzenia są prawdziwe, oznacza to, że:
- wszystkie wartości odczytane przez [showDataBase] zostały rzeczywiście znalezione w słownikach;
- słowniki te nie zawierają żadnych innych jednostek poza tymi, które zostały odczytane;
Prywatna metoda [fill] wygląda następująco:
private List<Categorie> fill(int nbCategories, int nbProduits) {
// wypełnianie tabel
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// dodanie kategorii – produkty również zostaną dodane kaskadowo
// dodane
categories = daoCategorie.saveEntities(categories);
// wynik
return categories;
}
- wiersze 3–12: tworzy się listę kategorii [nbCategories], z których każda zawiera produkty [nbProduits];
- wiersz 15: ta lista kategorii jest zapisywana w bazie danych. Widzieliśmy, że metoda [daoCategorie.saveEntities] zapisuje również produkty z kategorii, jeśli takie istnieją;
- wiersz 17: zwracana jest zapisana lista kategorii. Zapisane encje (kategorie i produkty) mają teraz klucz główny w polu [id];
Prywatna metoda [registerCategories] umieści te encje w obu słownikach:
private void registerCategories(List<Categorie> categories) {
// słowniki
for (Categorie categorie : categories) {
mapCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
mapProduits.put(produit.getId(), produit);
}
}
}
Kluczem dostępu do każdego słownika jest klucz podstawowy tych encji.
Po wykonaniu tej czynności wcześniej wypełniona baza danych zostanie odczytana i wyświetlona przez następującą metodę prywatną [showDataBase]:
private Object[] showDataBase() throws BeansException, JsonProcessingException {
// lista kategorii
log("Liste des catégories", 2);
List<Categorie> categories = daoCategorie.getAllShortEntities();
affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
// lista produktów
log("Liste des produits", 2);
List<Produit> produits = daoProduit.getAllShortEntities();
affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
// wynik
return new Object[] { categories, produits };
}
- wiersze 4 i 8: pobierane są skrócone nazwy kategorii i produktów;
- wiersz 11: zwracana jest tablica zawierająca dwie listy pobranych encji;
- wiersze 5 i 9: listy elementów są wyświetlane za pomocą następującej prywatnej metody [affiche]:
// wyświetlanie listy elementów typu T
private <T> void affiche(List<T> elements, ObjectMapper mapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, mapper);
}
}
// wyświetlanie elementu typu T
private <T> void affiche(T element, ObjectMapper mapper) throws JsonProcessingException {
System.out.println(mapper.writeValueAsString(element));
}
Entities są wyświetlane przez mapper jSON (wiersz 10). Ten mapper jest drugim parametrem metody [affiche], wiersz 2. Kontekst Spring definiuje cztery mappery jSON w pliku [ConfigJdbc] zależności Maven [mysql-config-jdbc]:
// filtry jSON -------------------------------------
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
- te mapery jSON (wiersze 7–9, 16–18, 26–28, 35–37) posiadają atrybut
[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)]
, dzięki czemu są one instancjonowane przy każdym żądaniu skierowanym do kontekstu Spring. To nowość. Wszystkie dotychczasowe bean’y Spring były singletonami: tworzono ich tylko jeden egzemplarz i to właśnie on był zwracany za każdym razem, gdy żądano od kontekstu Spring odniesienia do niego. Skąd ta zmiana? W rzeczywistości cztery bean’y [jsonMapperShortCategorie, jsonMapperLongCategorie, jsonMapperShortProduit , jsonMapperLongProduit] konfigurują jedyny mapper jSON (ten jest rzeczywiście singletonem), zdefiniowany w wierszach 2–5. Musi on być rekonfigurowany przy każdym wywołaniu jednego z czterech poprzednich bean’ów, a nie tylko raz podczas inicjalizacji kontekstu. Gdybyśmy zdecydowali się na cztery różne mapery jSON, po jednym dla każdego z czterech beanów, wówczas mogłyby one być singletonami. Było to całkowicie możliwe. W takim przypadku w wierszach 10, 19, 29 i 38 napisalibyśmy:
ObjectMapper jsonMapper = new ObjectMapper();
- cztery mapery json służą do konfiguracji filtrów jSON dla encji [Produit] i [Categorie]. W rzeczywistości napisaliśmy (por. akapity 4.6 i 4.6) następujące rzeczy:
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
oraz
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
Reprezentacja jSON jednostki [Categorie] jest kontrolowana przez filtr jSON [jsonFilterCategorie], a reprezentacjaentytetu [produit] – przez filtr jSON [jsonFilterProduit]. Cztery mapery jSON z kontekstu Spring konfigurują te dwa filtry w następujący sposób:
- mapper [jsonMapperShortCategorie] konfiguruje filtr jSON [jsonFilterCategorie] dla skróconej wersji kategorii: pole [produits] nie zostanie uwzględnione w reprezentacji kategorii jSON;
- mapper [jsonMapperLongCategorie] konfiguruje filtr jSON [jsonFilterCategorie] dla długiej wersji kategorii: pole [produits] zostanie uwzględnione w reprezentacji kategorii jSON;
- mapper [jsonMapperShortProduit] konfiguruje filtr jSON [jsonFilterProduit] dla skróconej wersji produktu: pole [categorie] nie zostanie uwzględnione w reprezentacji produktu o nazwie jSON;
- mapper [jsonMapperLongProduit] konfiguruje filtr jSON [jsonFilterProduit] dla długiej wersji produktu: pole [categorie] zostanie uwzględnione w reprezentacji produktu jSON;
Zakończyliśmy omówienie metody prywatnej [showDataBase]. Wróćmy do kodu testowego [fillDataBase]:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// wypełnianie bazy i słowników
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// wyświetlanie
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// kilka sprawdzeń
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// słowniki muszą być wyczerpane
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
- wiersze 6–8: pobieramy skrócone wersje produktów i kategorii odczytanych z bazy danych;
- wiersze 10–11: wstępne weryfikacje;
- wiersze 12–14: każda kategoria zwrócona przez metodę [showDataBase] jest sprawdzana przez następującą prywatną metodę [checkShortCategorie]:
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
// nie można przetestować pola [produits] w sposób przenośny przy użyciu implementacji jPA
}
- wiersz 1: [Categorie actual] to kategoria odczytana z bazy danych, która musi być identyczna z kategorią znajdującą się w słowniku [mapCategories];
- wiersz 2: pobierany jest klucz podstawowy odczytanej kategorii;
- wiersz 3: pobierana jest kategoria zapisana z tym kluczem głównym w słowniku kategorii;
- wiersz 4: klucz jest usuwany ze słownika, aby upewnić się, że żadna inna odczytana kategoria nie używa tego samego klucza;
- wiersz 5: sprawdzamy, czy obie kategorie mają tę samą nazwę;
Skrócona wersja produktów pobranych metodą [showDataBase] jest weryfikowana za pomocą następującej prywatnej metody [checkShortProduit]:
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(actual.getIdCategorie(), expected.getIdCategorie());
// nie można przetestować pola [categorie] w sposób przenośny przy użyciu implementacji jPA
}
- wiersz 1: [Produit actual] to skrócony produkt odczytany z bazy;
- wiersze 2–3: z słownika trwałych produktów pobierany jest produkt o tym samym kluczu głównym;
- wiersz 4: usuwa się znaleziony wpis ze słownika;
- wiersze 5–8: sprawdzamy, czy oba produkty mają te same wartości pól;
4.11.2.4. Metoda [getLongCategoriesByName3]
Test ten wygląda następująco:
@Test
public void getLongCategoriesByName3() {
// wypełnianie bazy
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// test
log("getLongCategoriesByName3", 1);
List<Categorie> categories2 = daoCategorie.getLongEntitiesByName("categorie[0]", "categorie[1]");
Assert.assertEquals(2, categories2.size());
registerCategories(Lists.newArrayList(categories.get(0), categories.get(1)));
for (Categorie categorie : categories) {
checkLongCategorie(categorie);
}
Assert.assertEquals(0, mapCategories.size());
}
- wiersz 4: wypełnia się bazę danych i pobiera listę zachowanych kategorii i produktów;
- wiersz 7: testujemy metodę [daoCategorie.getLongEntitiesByName(Iterable<String> names)] warstwy [DAO]. Żądamy listy dwóch produktów oznaczonych ich pełnymi nazwami;
- wiersz 8: sprawdzamy, czy lista zwrócona przez metodę [daoCategorie.getLongEntitiesByName(Iterable<String> names)] zawiera rzeczywiście dwa elementy;
- wiersz 9: dwa elementy zapisane w wierszu 4 są umieszczane w słowniku kategorii;
- wiersze 10–12: sprawdzamy, czy dwa odczytane elementy to rzeczywiście te, które zostały zapisane;
- wiersz 13: sprawdzamy, czy słownik kategorii jest pusty, co oznacza zarówno, że wszystkie odczytane kategorie zostały znalezione w słowniku, jak i że nie zawiera on wartości, które nie zostały odczytane;
W wierszu 11 metoda [checkLongCategorie] sprawdza pełną wersję kategorii:
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
- w wierszu 6 sprawdzane jest, czy pole [produits] kategorii nie ma wartości null. W rzeczywistości odczytanie kategorii w formacie rozszerzonym zawsze zwraca ją z polem [produits], które nie jest równoznaczne z null. Jeśli kategoria nie zawiera żadnych produktów, wówczas pole [produits] jest pustą, ale istniejącą listą;
4.11.2.5. Metoda [updateDataBase1]
@Test
public void updateDataBase1() {
// wypełnianie
fill(NB_CATEGORIES, NB_PRODUITS);
// test
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = daoCategorie.getLongEntitiesByName("categorie[1]").get(0);
List<Produit> produits = categorie1.getProduits();
Map<Produit, Long> versions = new HashMap<Produit, Long>();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
versions.put(produit, produit.getVersion());
}
daoProduit.saveEntities(produits);
// korekta
List<Produit> produitsInBd = daoCategorie.getLongEntitiesByName("categorie[1]").get(0)
.getProduits();
Assert.assertEquals(produits.size(), produitsInBd.size());
// kontrola
for (Produit produit2 : produitsInBd) {
Produit produit = findProduitByName(produit2.getNom(), produits);
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
Assert.assertEquals(produit2.getVersion().longValue(), versions.get(produit) + 1);
}
}
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
Metoda [updateDataBase1] podnosi ceny produktów z kategorii o nazwie categorie[1] o 10% i sprawdza dwie rzeczy:
- czy cena bazowa rzeczywiście uległa zmianie;
- czy wersja zaktualizowanego produktu została zwiększona o 1;
Kod wykonuje następujące czynności:
- wiersz 4: uzupełnienie bazy danych;
- wiersz 7: pobieranie z bazy kategorii o nazwie „categorie[1]”;
- wiersze 8–13: zwiększa się o 10% cenę wszystkich produktów tej kategorii (wiersz 11). Ponadto tworzy się słownik powiązujący produkt z jego wersją (wiersze 9 i 12);
- wiersz 14: wywoływana jest metoda [daoProduit.saveEntities]. Dokona ona aktualizacji produktów;
- wiersz 16: pobierane są z bazy produkty z kategorii o nazwie „categorie[1]”;
- wiersze 20–24: dla wszystkich produktów z tej kategorii sprawdzane jest, czy cena została rzeczywiście zmieniona (wiersz 22) oraz czy wersja została zwiększona o 1 (wiersz 23);
4.11.2.6. Metoda [deleteProduitsByProduit1]
Metoda [deleteProduitsByProduit1] usuwa produkty z tabeli [PRODUITS]:
@Test
public void deleteProduitsByProduit1() {
// wypełnianie
fill(NB_CATEGORIES, NB_PRODUITS);
// usunięcie
daoProduit.deleteEntitiesByEntity(daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]"));
// weryfikacja
List<Produit> produits = daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]");
Assert.assertEquals(0, produits.size());
}
- wiersz 6: usuwa się dwa produkty;
- wiersze 8–9: sprawdzane jest, czy produkty te nie znajdują się już w bazie danych;
4.11.2.7. Metoda [getLongProduitsById3]
@Test
public void getLongProduitsById3() {
// wypełnianie
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// test
log("getLongProduitsById3", 1);
List<Produit> produits = daoProduit.getLongEntitiesByName("produit[0,3]", "produit[1,4]");
Assert.assertEquals(2, produits.size());
registerProduits(Lists.newArrayList(categories.get(0).getProduits().get(3), categories.get(1).getProduits().get(4)));
produits = daoProduit.getLongEntitiesById(produits.get(0).getId(), produits.get(1).getId());
for (Produit produit : produits) {
checkLongProduit(produit);
}
Assert.assertEquals(0, mapProduits.size());
}
- wiersz 4: uzupełnia się bazę danych i pobiera listę zachowanych kategorii;
- wiersz 7: pobiera się z bazy danych pełne opisy dwóch produktów zidentyfikowanych na podstawie ich nazw;
- wiersz 9: produkty [produit[0,3], produit[1,4]] znajdujące się na liście kategorii z wiersza 4 są umieszczane w słowniku produktów;
- wiersz 10: te same dwa produkty są wyszukiwane w bazie danych na podstawie ich kluczy głównych;
- wiersze 11–14: sprawdzane jest, czy odczytane dane są identyczne z danymi zapisanymi w słowniku;
Metoda prywatna [checkLongProduit] wygląda następująco:
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertNotNull(actual.getCategorie());
}
4.11.2.8. Conclusion
Na tym zakończymy. Obecnie mamy 74 testy i można by dodać kolejne, ponieważ prawdopodobnie pominąłem pewne przypadki, które należałoby przetestować. Nawet jeśli nie są one wyczerpujące, testy te pozwoliły wykryć wiele błędów, zazwyczaj przypadków granicznych, których nie przewidziano podczas pierwotnego pisania warstwy [DAO]. Faza wyczerpujących testów jest niezbędna w każdym projekcie.
Aby uruchomić test, można skorzystać z zaimportowanej konfiguracji uruchomieniowej o nazwie [spring-jdbc-generic-04.JUnitTestDao].
![]() | ![]() |
4.11.3. Test [JUnitTestPushTheLimits]
Test [JUnitTestPushTheLimits] jest testem wydajnościowym. Wykorzystujemy fakt, że testy JUnit wyświetlają swój czas wykonania, aby zmierzyć wydajność warstwy [DAO]. Wyniki te zostaną następnie porównane z wynikami implementacji JPA warstwy [DAO].
4.11.3.1. Squelette
Szkielet klasy [JUnitTestPushTheLimits] wygląda następująco:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestPushTheLimits {
// warstwa [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// stałe
private final int NB_CATEGORIES = 2500;
private final int NB_PRODUITS = 2;
// lokalne
private Map<Long, Categorie> hCategories;
private Map<Long, Produit> hProduits;
@Before
public void clean() {
// opróżnianie tabeli [CATEGORIES]
daoCategorie.deleteAllEntities();
// słowniki
hCategories = new HashMap<Long, Categorie>();
hProduits = new HashMap<Long, Produit>();
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// wypełnianie tabel
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, 0L, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, 0L, String.format("produit[%d,%d]", i, j), 0L,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// dodanie kategorii – produkty również zostaną dodane kaskadowo
categories = daoCategorie.saveEntities(categories);
// słowniki
for (Categorie categorie : categories) {
hCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
hProduits.put(produit.getId(), produit);
}
}
// wynik
return categories;
}
....
// -------------------- metody prywatne
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
Assert.assertNotNull(actual.getCategorie());
}
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
boolean erreur = false;
try {
actual.getCategorie().getNom();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
boolean erreur = false;
try {
actual.getProduits().size();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
}
Znajdujemy tu szkielet klasy [JUnitTestDao]. Wszystkie te metody znamy już z poprzednich przykładów. Test opiera się na bazie zawierającej 2500 kategorii, z których każda ma po 2 produkty (wiersze 32–33). Tabela [CATEGORIES] będzie zatem zawierała 2500 wierszy, a tabela [PRODUITS] – 5000 wierszy. Można było wprowadzić więcej wierszy, ale test trwa już prawie minutę. Wybrano zatem wartości akceptowalne dla użytkownika, który czeka na zakończenie testu.
Łącznie jest 18 testów. Są one wykonywane przy użyciu konfiguracji uruchomieniowej [1]. Czasy wykonania są przedstawione w [2]:
![]() |
4.11.3.2. doNothing [0,114]
Metoda [doNothing] nie wykonuje żadnych czynności. Służy ona do pomiaru czasu trwania metody [clean], która jest uruchamiana przed każdym testem i oczyszcza bazę danych. Jak widać powyżej, czas trwania tej operacji jest nieznaczny w porównaniu z pozostałymi.
@Test
public void doNothing() {
// clean
}
4.11.3.3. perf01 [4,179]
Test [perf01] służy do pomiaru czasu wypełniania bazy danych:
@Test
public void perf01() {
// wstaw
fill(NB_CATEGORIES, NB_PRODUITS);
}
4.11.3.4. perf02 [7,624]
Metoda [perf02]:
- napełnia bazę;
- następnie zmienia nazwy wszystkich kategorii i ceny wszystkich produktów.
@Test
public void perf02() {
// aktualizacja
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
for (Categorie categorie : categories) {
categorie.setNom(categorie.getNom() + "*");
for (Produit produit : categorie.getProduits()) {
produit.setPrix(produit.getPrix() * 1.1);
}
}
// aktualizacja
daoCategorie.saveEntities(categories);
}
4.11.3.5. perf03[3,911]
Metoda [perf03]:
- wypełnia bazę danych
- , a następnie usuwa wszystkie kategorie po kolei. Produkty są również usuwane z powodu zależności kaskadowej istniejącej między tabelą [CATEGORIES] a tabelą [PRODUITS].
Można się tu zdziwić, że operacja ta trwa krócej ([3,911 s]) niż operacja [perf01] [4,179 s], która wykonuje mniej czynności.
@Test
public void perf03() {
// usuwanie kategorii i, w sposób kaskadowy, produktów
daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}
Jeśli przyjrzymy się kodowi metody [daoCategorie.deleteEntitiesByEntity], zauważymy, że zostanie wykonana metoda [PreparedStatement] z 2500 parametrami (liczba kategorii). W tym momencie wkracza bean [maxPreparedStatementParameters], który podzieli zlecenie SQL na kilka zleceń [PreparedStatement], z liczbą parametrów, którą jest w stanie obsłużyć konkretny używany bean SGBD.
4.11.3.6. perf04[2,426]
Metoda [perf04]:
- wypełnia bazę danych;
- następnie pobiera rozszerzone opisy wszystkich kategorii;
@Test
public void perf04() {
// wybierz
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
ids.add(categorie.getId());
}
daoCategorie.getLongEntitiesById(ids);
}
4.11.3.7. perf05 [3,507]
Metoda [perf05]:
- wypełnia bazę danych;
- następnie usuwa 5000 produktów na podstawie ich kluczy głównych (możemy więc potencjalnie uzyskać [PreparedStatement] z 5000 parametrami);
- sprawdza, czy tabela produktów jest teraz pusta;
@Test
public void perf05() {
// usunięcie produktów
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
for (Produit p : categorie.getProduits()) {
ids.add(p.getId());
}
}
daoProduit.deleteEntitiesById(ids);
// weryfikacja
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.3.8. Résultats
Nie będziemy dalej przedstawiać poszczególnych testów. Wskażemy jedynie, na czym polegają i ile trwają. Czasy te mają znaczenie jedynie w porównaniu między sobą. Ich wartości zależą bowiem od wykorzystanego środowiska testowego (sprzętu i konfiguracji oprogramowania). Jednak uzyskane w tym samym środowisku mogą być porównywane.
Całkowity czas trwania testu: 59,995 sekund
rola | ||
uzupełnia bazę o 2500 kategorii i 5000 produktów | ||
wypełnia, a następnie modyfikuje bazę danych | ||
wypełnia bazę, a następnie usuwa wszystkie kategorie i związane z nimi produkty | ||
wypełnia bazę danych i pobiera pełne opisy wszystkich kategorii | ||
wypełnia bazę i usuwa 5000 produktów pojedynczo na podstawie ich kluczy głównych | ||
wypełnia bazę danych i usuwa 5000 produktów pojedynczo na podstawie ich nazw | ||
wypełnia bazę danych i usuwa 5000 produktów pojedynczo na podstawie ich numerów katalogowych | ||
wypełnia bazę danych i pobiera skrócone nazwy wszystkich produktów na podstawie ich nazw | ||
wypełnia bazę i pobiera pełne nazwy wszystkich produktów na podstawie ich nazw | ||
wypełnia bazę danych i pobiera skrócone nazwy wszystkich produktów na podstawie ich kluczy głównych | ||
wypełnia bazę i pobiera pełne wersje wszystkich produktów na podstawie ich kluczy głównych | ||
wypełnia bazę, a następnie usuwa wszystkie kategorie (a tym samym powiązane z nimi produkty) pojedynczo na podstawie ich nazw | ||
wypełnia bazę, a następnie usuwa wszystkie kategorie (a tym samym powiązane z nimi produkty) pojedynczo na podstawie ich numerów katalogowych | ||
wypełnia bazę danych i pobiera skrócone nazwy wszystkich kategorii na podstawie ich nazw | ||
wypełnia bazę danych i pobiera pełne wersje wszystkich kategorii na podstawie ich nazw | ||
wypełnia bazę i pobiera skrócone nazwy wszystkich kategorii na podstawie ich kluczy głównych | ||
wypełnia bazę i pobiera pełne wersje wszystkich kategorii na podstawie ich kluczy głównych |
Wyniki te bywają zaskakujące:
- szybciej uzyskano pełną wersję produktów (perf09) niż ich skróconą wersję (perf08), mimo że pełna wersja wymaga połączenia dwóch tabel;
- czas trwania pierwszego wypełnienia (perf01) znacznie przewyższa czas wszystkich kolejnych wypełnień;
- pobieranie skróconej wersji produktów na podstawie ich nazw (perf08) trwa dłużej niż pobieranie ich na podstawie kluczy głównych (perf10). Wydaje się to dość logiczne. Jednak w przypadku wersji pełnych sytuacja jest odwrotna (perf09, perf11);
Nie będziemy więc się więc nad tymi wynikami rozwodzić. Będą one jednak przydatne do porównania tego rozwiązania [Spring JDBC] z rozwiązaniami:
- [Spring JDBC] oraz pięciu pozostałych SGBD;
- [Spring JPA], które pojawią się w dalszej części;





























