Skip to content

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];

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:

Image

  • [sql] to polecenie SQL, które ma zostać wykonane;
  • [rowMapper] jest instancją następującego interfejsu [RowMapper<T>]:

Image

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:

1
2
3
4
5
6
7
c1, produits11
c1, produit12
...
c1,produits1n
c2, produits21
c2, produits22
...

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:

c1, produits1
c2, produits2

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:

Image

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ę:

Image

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]:

Image

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]:

Image

  • 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

test
rola
durée (s)
perf01
uzupełnia bazę o 2500 kategorii i 5000 produktów
4,179
perf02
wypełnia, a następnie modyfikuje bazę danych
7,624
perf03
wypełnia bazę, a następnie usuwa wszystkie kategorie i związane z nimi produkty
3,911
perf04
wypełnia bazę danych i pobiera pełne opisy wszystkich kategorii
2,426
perf05
wypełnia bazę i usuwa 5000 produktów pojedynczo na podstawie ich kluczy głównych
3,507
perf06
wypełnia bazę danych i usuwa 5000 produktów pojedynczo na podstawie ich nazw
3,947
perf07
wypełnia bazę danych i usuwa 5000 produktów pojedynczo na podstawie ich numerów katalogowych
3,633
perf08
wypełnia bazę danych i pobiera skrócone nazwy wszystkich produktów na podstawie ich nazw
4,054
perf09
wypełnia bazę i pobiera pełne nazwy wszystkich produktów na podstawie ich nazw
2,643
perf10
wypełnia bazę danych i pobiera skrócone nazwy wszystkich produktów na podstawie ich kluczy głównych
3,463
perf11
wypełnia bazę i pobiera pełne wersje wszystkich produktów na podstawie ich kluczy głównych
2,777
perf12
wypełnia bazę, a następnie usuwa wszystkie kategorie (a tym samym powiązane z nimi produkty) pojedynczo na podstawie ich nazw
3,806
perf13
wypełnia bazę, a następnie usuwa wszystkie kategorie (a tym samym powiązane z nimi produkty) pojedynczo na podstawie ich numerów katalogowych
2,828
perf14
wypełnia bazę danych i pobiera skrócone nazwy wszystkich kategorii na podstawie ich nazw
2,731
perf15
wypełnia bazę danych i pobiera pełne wersje wszystkich kategorii na podstawie ich nazw
2,603
perf16
wypełnia bazę i pobiera skrócone nazwy wszystkich kategorii na podstawie ich kluczy głównych
2,462
perf17
wypełnia bazę i pobiera pełne wersje wszystkich kategorii na podstawie ich kluczy głównych
3,287

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;