Skip to content

3. Вступ до API JDBC

3.1. Налаштування робочого середовища

Ми будемо працювати з базою даних MySQL5.

Необхідно мати:

  • встановити JDK (Java Development Kit) (розділ 23.1);
  • встановити менеджер залежностей Maven (параграф 23.2);
  • встановити IDE Spring Tool Suite (STS) (параграф 23.3);
  • встановив SGBD MySQL5 (параграф 23.4) та його клієнт EMS MyManager (параграф 23.5);
  • завантажив коди з документа [http://tahe.developpez.com/java/spring-database];

Далі припускаємо, що адміністратор MySQL5 є користувачем root із паролем root. Запустіть SGBD, MySQL5 та його клієнт [MyManager]. За допомогою [MyManager] ми створюємо базу даних [dbproduits] [1-34]:

  • у [3] база даних має називатися [dbproduits];
  • в [8-9], root з паролем root (чого не видно на знімку екрана вище);
  • на [14a], пароль знову root (чого не видно на скріншоті);
  • у [15] було створено базу даних [dbproduits];
  • у [20] зверніть увагу на вибрану базу даних. Це має бути база [dbproduits];
  • на [22], папка — <exemples>/spring-database-config/mysql/databases, де <exemples> — це папка із завантаженими прикладами;
  • у [23] виберіть скрипт SQL [dbproduits.sql]. Він створить таблицю [PRODUITS] у базі даних [dbproduits];
  • у [30] було створено таблицю [produits];
  • у [33] — стовпці таблиці [produits];
  • у [34], вона спочатку порожня;

Тепер за допомогою STS імпортуйте наступні проекти (дотримуйтесь процедури, використаної для проектів у папці <exemples>/spring-core):

  • у [2], проект [mysql-config-jdbc] буде знайдено у папці [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];

Цей проект налаштовує рівень JDBC у наведеній нижче архітектурі:

Потім знову імпортуйте наступні три проекти:

  • у [2]; проекти будуть знайдено у папці [<exemples>/spring-database-config/spring-jdbc] [1];

Ці три проекти — це проекти Maven, які використовують проект Maven [mysql-config-jdbc]. Останній проект генерує такий артефакт Maven (див. pom.xml):


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

Цей самий артефакт буде згенеровано проектом [oracle-config-jdbc, db2-config-jdbc, ...]. Щоб переконатися, що проекти [spring-generic-jdbc-*], які наразі завантажені в STS, дійсно використовують проект [mysql-config-jdbc]:

  • переконайтеся, що інший проект [sgbd-config-jdbc] не завантажено одночасно. Це може спричинити помилки, які важко зрозуміти;
  • оновіть конфігурацію Maven завантажених проектів наступним чином:

Щоб перевірити конфігурацію, запустіть конфігурацію виконання [spring-jdbc-generic-01.IntroJdbc01] [1-3]:

Ви повинні отримати такі результати у консолі:

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

У наведених нижче прикладах читач зможе:

  • або працювати безпосередньо з раніше завантаженими проектами;
  • або самостійно створити проекти;

3.2. Етапи роботи з базою даних

У наведеній вище архітектурі робота з базою даних за допомогою консольної програми включає такі етапи:

  1. завантаження драйвера JDBC бази даних;
  1. відкриття з'єднання з базою;
  2. виконання команди SQL у базі даних та обробка результатів команди SQL;
  3. закриття з’єднання;

Крок 1 виконується лише один раз. Кроки 2–4 виконуються повторно. Слід зауважити, що з’єднання не залишається відкритим. Його закривають, щойно воно більше не потрібне.

3.2.1. етап 1 — завантаження драйвера JDBC у пам’ять

Код


        // завантаження драйвера JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // обробка винятку
}

Операція в рядку 3 має на меті завантажити в пам'ять драйвер JDBC із бази даних. Цю операцію потрібно виконати лише один раз. Однак її повторення не спричиняє помилки. Клас драйвера JDBC шукається в Classpath проекту. Тому в проєкті Eclipse файл [jar], що містить клас драйвера JDBC, має бути включений до Classpath проєкту.

3.2.2. Крок 2 — встановлення з’єднання

Після встановлення драйвера JDBC йому потрібно відкрити з’єднання з BD:

Код


package spring.jdbc;

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

public class IntroJdbc01 {

...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // обробка винятку
            ...
        } finally {
         // закриття з'єднання
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // обробити виняток
                ...
            }
         }
}
  • рядки 3–7: класи реалізації інтерфейсу JDBC знаходяться у пакеті [java.sql]. Крім того, у разі помилки всі вони генерують виняток типу [SQLException] (рядки 19, 27). Цей виняток походить від класу [Exception] і є так званим контрольованим винятком: для його обробки обов’язково потрібно використовувати блок try/catch або, як альтернатива, не обробляти його та вказати, що метод дозволяє винятковій ситуації вийти, доповнивши сигнатуру методу [throws SQLException];
  • рядок 17, [DriverManager.getConnection] — це статичний метод, який очікує три параметри:
    • [url]: URL з бази даних. Це рядок символів, що залежить від використовуваного BD. Для MySQL вона має вигляд [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: власник з’єднання;
    • [passwd]: його пароль;
  • рядки 24–30: з’єднання має бути закрите в блоці [finally], щоб воно закривалося незалежно від того, чи виникне виняток.

3.2.3. Етап 3 — відправлення команд SQL та [SELECT]

Після встановлення з’єднання можна надсилати команди SQL. Спосіб обробки команд читання [SELECT] відрізняється від того, що використовується для операцій оновлення [UPDATE, INSERT, DELETE]. Почнемо з команд SQL та [SELECT]:

Код


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі тільки для читання
            connexion.setReadOnly(true);
            // читання таблиці [PRODUITS]
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
             doCatchException(connexion,e1);
        } finally {
            // обробка блоку finally
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • рядки 8, 10: відкриття транзакції (рядок 8) у режимі «тільки для читання» (рядок 10). Транзакція — це послідовність команд SQL, які або всі виконуються успішно, або всі завершуються невдачею. Отже, у транзакції, що містить N команд SQL, якщо команда I+1 завершиться невдало, то попередні I команд будуть скасовані. Для операції читання транзакція не потрібна. Проте створення транзакції «тільки для читання» може дозволити деяким командам SGBD здійснити певні оптимізації;
  • рядок 12: використання [PreparedStatement]. [PreparedStatement] зазвичай має параметри, позначені символом ?. Тут їх немає. [PreparedStatement] — це команда, підготовлена SGBD. Ця підготовка має свою вартість і виконується лише один раз. Потім ця підготовлена команда виконується SGBD із різними фактичними параметрами, які замінять формальні параметри «?». Слід зауважити, що краще вказувати потрібні стовпці, а не використовувати символ * для отримання всіх стовпців. Вказавши назви стовпців, можна потім отримати їхні значення на основі їхнього положення у запиті SELECT;
  • рядок 13: виконання запиту [PreparedStatement]. Отримуємо об’єкт типу [ResultSet];

Об’єкт типу [ResultSet] представляє таблицю, тобто сукупність рядків і стовпців. У певний момент часу доступний лише один рядок таблиці, який називається поточним рядком. Під час початкового створення [ResultSet] поточного рядка немає. Щоб її отримати, потрібно виконати операцію [ResultSet.next()]. Сигнатура методу next така:

    boolean next()

Цей метод намагається перейти до наступного рядка [ResultSet] і повертає true у разі успіху, а в іншому випадку — false. У разі успіху наступний рядок стає новим поточним рядком. Попередній рядок втрачається, і повернутися назад, щоб його відновити, неможливо.

Таблиця [ResultSet] має стовпці з іменами labelCol1, labelCol2, ..., які вказані у виконаному запиті [SELECT]. За допомогою запиту:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • стовпець [ID] буде перенесено до стовпця таблиці [ResultSet] з назвою [myId];
  • стовпець [NOM] буде переміщено до стовпця таблиці [ResultSet] з назвою [myNom];
  • ...

У наведеному вище прикладі ідентифікатори [myCol] називаються мітками стовпців. За відсутності цих міток імена стовпців таблиці [ResultSet] залежать від таблиці SGBD. Коли [SELECT] оперує з однією таблицею, мітками стовпців за замовчуванням будуть імена стовпців, запитувані SELECT. Проблема виникає, коли [SELECT] працює з кількома таблицями, у яких є стовпці з однаковими іменами, як у наведеному нижче прикладі:

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

припустимо, що таблиця [PRODUITS] має зовнішній ключ до таблиці [CATEGORIES], що символізується відношенням [Produits].CATEGORIE_ID --> [CATEGORIES].ID, а таблиці [PRODUITS] і [CATEGORIES] обидві мають поле [NOM]. У цьому випадку імена, надані в таблиці [ResultSet] стовпцям [PRODUITS.NOM] та [CATEGORIES.NOM], залежать від таблиці SGBD. Отже, для перенесення даних між файлами SGBD тут слід використовувати мітки стовпців, і ми напишемо:


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

Для обробки різних полів поточного рядка [ResultSet] доступні такі методи:

Type getType("labelColi") 

для отримання стовпця з назвою «labelColi» з поточного рядка, а отже, стовпця з файлу [SELECT], що має цей міток. Type позначає тип поля coli. Можна використовувати такі методи [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Замість імені стовпця можна використовувати його позицію у виконаному запиті [SELECT]:

Type getType(i) 

де i — індекс потрібного стовпця (i>=1).

  • рядки 15–17: отримання значень, зчитаних у BD;
  • рядок 19: транзакція підтверджується (це також називають «фіксацією»). Це завершує її та звільняє ресурси, які SGBD задіяв для неї;
  • рядок 25: ресурси звільняються у [finally]. Ця процедура викликає наступний метод [doFinally]:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // закриття ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // закриття [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // закрити з'єднання
                connexion.close();
            } catch (SQLException e3) {
                 // обробити виняток
            }
        }
    }
  • рядки 3–9: закриття [ResultSet];
  • рядки 11–17: закриття [PreparedStatement];
  • рядки 18–27: закриття з’єднання;

Закриття у рядках 3–17 здаються зайвими, оскільки з’єднання закривається у рядках 18–25. Насправді в деяких випадках це не так, і рекомендується залишити їх [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • рядок 22: виняток обробляється наступним методом [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // скасування транзакції
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // обробити виняток
        }
}
  • рядки 4–6: транзакція скасовується. Це завершує її, і SGBD зможе звільнити ресурси, задіяні для неї;

3.2.4. етап 3 — видача команд SQL та [INSERT, UPDATE, DELETE]

Команди SQL та [INSERT, UPDATE, DELETE] є операціями оновлення: вони змінюють базу даних, але не повертають жодного рядка. Єдина інформація, що повертається, — це кількість рядків, на які вплинула операція оновлення.

Код


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // відкрити з'єднання
            connexion = DriverManager.getConnection(url, user, passwd);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // оновлення таблиці
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // категорія 1
            ps.setInt(1, 10);
            // виконання
            int nbLignes=ps.executeUpdate();
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException(connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, ps, connexion);
        }
    }
  • рядок 9: з’єднання використовується для читання та запису;
  • рядок 11: операція [PreparedStatement] з 1 параметром (позначеним символом ?). Параметрів може бути кілька. Вони нумеруються, починаючи з 1;
  • рядок 13: єдиному параметру присвоюється значення. Перший параметр [setType] — це позиція параметра в [PreparedStatement] (1, 2, ...), а другий — значення, яке йому присвоюється. Можна використовувати методи [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • рядок 15: використовується метод [executeUpdate], а не [executeQuery], який зарезервовано для команд SELECT. Цей метод повертає кількість рядків, на які вплинула операція. Може дорівнювати 0.
  • рядок 17: транзакція підтверджена;

3.2.5. етап 4 — закриття з'єднання

У багатокористувацькому середовищі з’єднання слід закривати якомога швидше, оскільки SGBD допускає обмежену кількість відкритих з’єднань. У наведених вище прикладах з’єднання закривалося в розділі [finally] операції SQL, щоб воно закривалося незалежно від того, чи стався виняток.

3.3. Налаштування шару JDBC для SGBD та MySQL5

Ми розглянемо проект [mysql-config-jdbc], який налаштовує наведений нижче рівень JDBC:

3.3.1. Проєкт Eclipse

 

3.3.2. Налаштування Maven

Файл [pom.xml] цього проєкту має такий вигляд:


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

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

    <dependencies>
        <!-- залежні змінні ********************************************** -->
        <!-- драйвер JDBC від SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- постійні залежності ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- бібліотека jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Тестування Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- журнали -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

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

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

</project>

У цій конфігурації Maven було об’єднано низку архівів, необхідних або для проекту [mysql-config-jdbc], або для проектів, які будуть на ньому базуватися:

  • рядки 4–6: артефакт Maven, згенерований проектом. Як уже зазначалося, усі проекти типу [*-config-jdbc] генерують цей самий артефакт. Тому не слід завантажувати одночасно два проекти типу [*-config-jdbc];
  • рядки 9–13: батьківський проект Maven цього проекту. Він визначає версії великої кількості бібліотек, що використовуються екосистемою Spring. Це дозволяє уникнути їхнього окремого зазначення в проектах, що походять від нього;
  • рядки 18–21: архів драйвера JDBC для SGBD та MySQL5. Це єдиний архів, необхідний для проекту [spring-jdbc-01];
  • рядки 24–27: артефакт [tomcat-jdbc] містить архів, необхідний для проєктів JDBC та [spring-jdbc-02 à 04];
  • рядки 29–36: містять бібліотеки, необхідні для роботи з jSON. Використовуються майже у всіх проектах цього документа;
  • рядки 38–42: Google Guava — це бібліотека для управління колекціями. Використовується майже у всіх проектах цього документа;
  • рядки 43–52: бібліотеки, що дозволяють писати тести з використанням Spring та JUnit. Використовуються майже у всіх проектах цього документа;
  • рядки 54–57: бібліотеки для ведення журналів. Використовуються практично у всіх проектах, описаних у цьому документі;
  • рядки 67–71: плагін, що дозволяє встановити артефакт проєкту [mysql-config-jdbc] у локальне сховище Maven;

3.3.3. Клас конфігурації [ConfigJdbc]

  

Клас [ConfigJdbc] має такий вигляд:


package generic.jdbc.config;

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

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

public class ConfigJdbc {

    // параметри підключення
    public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
    public final static String USER_DBPRODUITS = "root";
    public final static String PASSWD_DBPRODUITS = "root";
...
    // команди SQL [jdbc-01, jdbc-02]
    public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
    //public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
    public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

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

...

}

Клас [ConfigJdbc] слугує для налаштування шару JDBC чотирьох проєктів [spring-jdbc-01 à 04]. Більша частина конфігурації стосується проєкту [spring-jdbc-04]. Ми розглянемо цю частину під час вивчення цього проєкту. Вище наведено лише конфігурацію проектів [spring-jdbc-01 à 03].

  • рядки 14–17: параметри підключення до бази даних MySQL5 [dbproduits];
  • рядки 20–25: команди SQL, що використовуються в проектах [spring-jdbc-01 et 02];
  • рядки 28–34: команди SQL, що використовуються в проєкті [spring-jdbc-03];

Ці команди SQL використовують таблицю [PRODUITS] з бази даних MySQL5 [dbproduits], структура якої така:

 
  • [ID]: первинний ключ у режимі AUTO_INCREMENT (якщо первинний ключ не вказано, SGBD генерує його);
  • [NOM]: назва товару — унікальна;
  • [CATEGORIE]: номер категорії;
  • [PRIX]: його ціна;
  • [DESCRIPTION]: опис товару;

3.3.4. Клас [Produit]

  

Клас [Produit] — це зображення рядка таблиці [PRODUITS]:


package generic.jdbc.entities.dbproduits;

public class Produit {

    // поля
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // конструктори
    public Produit() {

    }

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

    // методи getter та setter
...
}

Пізніше нам знадобиться порівняти два товари, щоб з’ясувати, чи вони однакові. Ми вважатимемо, що два продукти є однаковими, якщо всі їхні поля збігаються. Для цього ми переопределимо метод [equals] класу [Object], від якого походить клас [Produit]:


    // метод рівності
    @Override
    public boolean equals(Object o) {
        // прості випадки
        if (o == null || o.getClass() != this.getClass()) {
            return false;
        }
        Produit p = (Produit) o;
        return this == o
                || (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
                        && Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
  • рядок 3: метод [equals] отримує об’єкт o, який він повинен порівняти з об’єктом this;
  • рядки 5–7: прості випадки, коли можна відразу сказати, що ці два об’єкти нерівні. [Object].getClass() повертає екземпляр типу [Class] — типу, що представляє фактичний клас об’єкта;
  • рядок 8: об’єкт o перетворюється на продукт p;
  • рядок 9: якщо обидва посилання o та p на товар є рівними, то фізично це один і той самий товар;
  • рядок 9: якщо o та p — це дві різні посилання на два продукти, що мають однакові поля, то вважатиметься, що вони рівні. Оскільки ціна має тип [double], а в інформатиці немає точного представлення дійсних чисел, ми вважатимемо, що дві ціни є ідентичними, якщо вони збігаються з точністю до 10⁻⁶;

Крім того, ми перевизначимо метод [hasCode] класу [Object]:


    // хеш-код
    @Override
    public int hashCode() {
        return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}

Значення hashCode двох продуктів мають бути однаковими, якщо метод [equals] визнав ці два продукти рівними. Це значення hashCode використовується для розподілу об’єктів у наборах, таких як словники. Отже, якщо два продукти є ідентичними, вони матимуть однакове значення hashCode.

3.3.5. Виняток [UncheckedException]

  

Розглянемо таку архітектуру:

  • рівень [JDBC] генерує винятки типу [SQLException]. Цей виняток повинен поширюватися вгору по рівнях, доки не досягне найвищого рівня, в даному випадку рівня тестування;

Рівень [DAO] міг би просто пропустити [SQLException] до рівня тестування. Але оскільки це неконтрольоване виключення (випливає безпосередньо з [Exception]), це означало б, що інтерфейс [IDao] шару [DAO] мав би бути таким:


public interface IDao {

    // додавання товарів
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;

    // список усіх товарів
    public List<Produit> getAllProduits() throws SQLException;

    // конкретний товар
    public Produit getProduitById(int id) throws SQLException;

    public Produit getProduitByName(String name) throws SQLException;

    // оновлення декількох товарів
    public int updateProduits(List<Produit> produits) throws SQLException;

    // видалення всіх товарів
    public int deleteAllProduits() throws SQLException;

    // видалення декількох товарів
    public int deleteProduits(int[] ids) throws SQLException;
}

І це дуже неприємно, оскільки це заважає нам реалізувати інтерфейс [IDao] за допомогою класу, який би генерував інше виключення. Щоб обійти цю проблему, рівень [DAO] буде генерувати неконтрольоване виключення [DaoException] (похідне від [RuntimeException]), що дозволяє нам уникнути використання клаузули [throws] у сигнатурі методів інтерфейсу. Таким чином, цей інтерфейс зможе бути реалізований будь-яким класом, який також генерує неконтрольоване виключення, що може відрізнятися від виключення [DaoException]. Наша архітектура тепер виглядає наступним чином:

Щоб спростити створення неконтрольованих винятків для різних рівнів додатка, ми створюємо для них батьківський клас [UncheckedException]:

  

package generic.jdbc.infrastructure;

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

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

// загальний клас винятків
// виняток є неконтрольованим

public class UncheckedException extends RuntimeException {

    // згенерований серійний номер ID
    private static final long serialVersionUID = -2924871763340170310L;

    // властивості
    private int code;
    private String trace;
    private List<ShortException> exceptions;

    // конструктори
    public UncheckedException() {
        super();
    }

    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // локальний
        this.code = code;
        this.exceptions = getErreursForException(e);
        // трасування
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
    }

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

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

    // список повідомлень про помилки винятку
    private List<ShortException> getErreursForException(Throwable th) {
        // отримуємо елементи стека винятку
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // отримання поточного винятку
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // наступне виключення
            cause = cause.getCause();
        }
        return exceptions;
    }

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

    // методи getter та setter
...
}
  • рядок 12: клас походить від [RuntimeException] і, отже, є типом неконтрольованого винятку. Він слугуватиме для інкапсуляції контрольованого винятку (SQLException) у тип неконтрольованого винятку (UncheckedException);
  • щоб розрізнити винятки типу [UncheckedException] між собою, їм можна присвоїти код, який буде збережено у приватному полі рядка 18. Код Java, що перехоплює виняток типу [UncheckedException], матиме доступ до цього коду помилки завдяки методу [getCode] (рядки 80 і далі);
  • рядок 20: зберігає повідомлення про помилки зі стека інкапсульованого винятку;
  • рядки 23–43: різні способи створення об’єкта типу [UncheckedException];
  • рядки 56–67: приватний метод, що дозволяє створити список помилок із рядка 20 на основі об’єкта типу [Throwable] або його похідних, зокрема типу [Exception];
  • рядки 69–78: метод [toString] повертає рядок, що представляє виняток. Для відображення списку помилок у рядку 20 він використовує бібліотеку jSON. Ця бібліотека присутня у залежностях Maven проекту:

        <!-- бібліотека jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
</dependency>
  • рядки 45–48: перевизначають метод [getMessage] батьківського класу [RuntimeException]. Цей метод тут повертає сигнатуру [toString] класу;
  • рядки 50–53: перевизначають метод [printStackTrace] батьківського класу [RuntimeException]. Буде відображено сигнатуру класу [toString];

Клас [UncheckedException] записує у поле рядка 20 список винятків, описаних наступним типом [ShortException]:


package pam.dao.exceptions;

public class ShortException {

    // властивості
    private String className;
    private String errorMessage;

    // конструктори
    public ShortException() {

    }

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

    // методи getter та setter
...
}
  • рядок 6: назва класу винятку, що стався;
  • рядок 7: відповідне повідомлення про помилку;

Розглянемо наступний конструктор класу [UncheckedException]:


    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // локальні
        this.code = code;
        this.exceptions = getErreursForException(e);
        // трасування
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
}
  • рядок 1, параметри такі:
    • [code]: код помилки;
    • [e]: виняток, який інкапсулюється. [Throwable] є батьківським класом класу [Exception] і безпосередньо походить від класу [Object]. Це батьківський клас усіх класів C, з якими можна записати [throw c;], де c — екземпляр класу C;
    • [simpleClassName]: просте ім’я класу користувацького коду, в якому було виявлено виняток e;
  • рядок 4: записується код помилки;
  • рядок 5: список [ShortException] будується на основі [Throwable e], переданого як параметр;
  • рядки 7–16: аналізуються так звані сліди винятку. Початковий виняток виникає в певному місці коду, потім простежується до методу, який викликав той метод, де стався виняток, і так далі, доки його не зупинить блок try/catch. Під час цього просування вгору початковий виняток залишає сліди, що зберігаються в масиві [e.stackTrace] винятку e. Вони отримуються тут у рядку 8 на основі [Throwable e], переданого як параметр. Кожен елемент типу [StackTraceElement] є об’єктом, що містить серед своїх полів такі:
    • [fileName]: ім’я Java-файлу, у якому сталося виключення;
    • [lineNumber]: номер рядка у цьому файлі, де сталося виключення;
    • [methodName]: назва методу в цьому файлі, у якому сталося виключення;
  • у рядках 10–16 у масиві слідів винятку, переданого як параметр, шукається перше входження умови [trace.fileName==simpleClassName.java], де [simpleClassName] є третім параметром конструктора. Ідея полягає в тому, щоб запам’ятати, де саме у коді користувача сталося виключення. Код користувача інкапсулює виключення таким чином:
1
2
3
4
5
6
7
try{
// код, який може викликати кероване виключення
...
}catch(UnTypeDexception e){
// кероване виключення e інкапсулюється в некероване виключення
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • рядок 13: створюється рядок типу [fileName, methodName, lineNumber], що характеризує місце в коді користувача, де було зупинено виняток;

Тепер розглянемо код, який записує список винятків зі стека винятків винятку [Throwable th], інкапсульованого попереднім конструктором:


    // список повідомлень про помилки винятку
    private List<ShortException> getErreursForException(Throwable th) {
        // отримуємо елементи стека винятку
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // отримання поточного винятку
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // наступне виключення
            cause = cause.getCause();
        }
        return exceptions;
}

Під час просування вгору до методу, який перехопив її за допомогою try/catch, початкова виняток e могла бути інкапсульована в іншу виняток. Саме ця виняток потім просувається вгору до методу, який остаточно її перехопить. Отже, вона також може піддатися інкапсуляції. У підсумку, коли метод вирішує перехопити виняток th і обробити його, він знайде початковий виняток e, захований у глибині стека винятків. Таким чином, у наведеному вище прикладі параметр [Throwable th] є лише верхівкою айсберга винятків. Його атрибут [th.cause] дозволяє дізнатися, яке саме виключення він інкапсулює. І так далі. Коли виключення e має вигляд [e.getCause()==null], це означає, що e є початковим виключенням.

  • рядок 8: для кожного винятку зі стека винятків [Throwable th] зберігаються дві інформації:
    • [getClass().getName()]: повна назва винятку;
    • [getMessage()]: відповідне повідомлення про помилку;

3.4. Exemple-01

3.4.1. Архітектура проєкту

У цьому прикладі консольна програма використовує інтерфейс рівня [JDBC].

3.4.2. Проєкт Eclipse

Ми створюємо проект Spring / Maven [spring-jdbc-01], дотримуючись інструкцій, наведених у розділі 2.5.2.1.

  

Цей проєкт є проєктом Maven, визначеним у файлі [pom.xml], що наведено нижче:


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

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

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- пошук батьківського елемента в репозиторії -->
    </parent>

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

    <dependencies>
        <!-- конфігурація JDBC для SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

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

</project>
  • рядки 28–32: проект використовує артефакт [generic-config-jdbc] з проекту [mysql-config-jdbc], який ми щойно розглянули. Отже, проект [spring-jdbc-01] має доступ до всіх елементів проекту [mysql-config-jdbc];

Цей останній момент можна побачити двома способами, перевіривши залежності Maven проекту:

  • у [2] видно, що проект [mysql-config-jdbc] входить до залежностей Maven цього проекту. Оскільки ці залежності знаходяться у Classpath проекту, це означає, що проект [mysql-config-jdbc] також знаходиться у цьому Classpath і, отже, його класи та інтерфейси доступні у проекті [spring-jdbc-01];

Проект Maven [mysql-config-jdbc] не обов’язково має бути присутнім у вкладці [Package Explorer], щоб його могли використовувати інші проекти Maven. Достатньо, щоб він був у локальному репозиторії Maven. На відміну від IDE, як-от NetBeans, у Eclipse це не відбувається автоматично. Це потрібно зробити вручну:

У розділі 2.3.5 ми розглянули умови, за яких можливе це генерування. Після його завершення можна видалити проект [mysql-config-jdbc] з вкладки [Package Explorer]:

  • не слід ставити галочку біля [3], оскільки це фізично видаляє проєкт з диска, роблячи його неможливим для відновлення;

Ця операція запускає перерахунок залежностей Maven для проектів, які залежать від проекту, вилученого з [Package Explorer]. Це змінює гілку [Maven Dependencies] цих проектів. Наприклад, для проєкту [spring-jdbc-01] гілка [Maven Dependencies] набуває такого вигляду:

Цього разу залежність більше не пов’язана з проектом, а з його артефактом Maven, у даному випадку — артефактом [generic-config-jdbc] [1]. Бачимо, що ми маємо доступ до всіх класів та інтерфейсів цього артефакту. Як уже зазначалося, цей артефакт буде згенеровано всіма проектами [*-config-jdbc]. Щоб уникнути помилок, ми:

  • завжди зберігатимемо лише один проєкт [*-config-jdbc] у вкладці [Package Explorer];
  • оновлюємо конфігурацію Maven для всіх проєктів у вкладці [Package Explorer] (Alt-F5), щоб у їхніх залежностях Maven з’явився проєкт [*-config-jdbc], який використовується;

3.4.3. Скелет головного класу

  

Скелет головного класу [IntroJdbc01] має такий вигляд:


package spring.jdbc;

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

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

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

public class IntroJdbc01 {

    // константи
    final static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) {
        // завантаження драйвера JDBC з SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // очищення таблиці [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // заповнення таблиці
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // зчитування
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // оновлення
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // вивід
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // очищення таблиці [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // вивести
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion з двох однакових елементів
        // операція INSERTion має завершитися невдало, і жоден з двох елементів не вставляється через транзакцію
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // перевіряємо
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // завершено
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // список товарів
    private static void select() {
    ...
    }

    // jSON — відображення об’єкта
    private static void affiche(Object object) {
...
    }

    // видалення товарів
    public static void delete() {
...
    }

    // додавання товарів
    public static void insert() {
...
    }

    // додавання 2 товарів з однаковими первинними ключами
    public static void insert2() {
...
    }

    // оновлення деяких товарів
    public static void update() {
...
    }

    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // закриття ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // закриття [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // закрити з'єднання
                connexion.close();
            } catch (SQLException e3) {
                // відображаються повідомлення про помилки
                show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        getErreursFromThrowable(e3));
            }
        }
    }

    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // відображаються повідомлення про помилки
        show(title, getErreursFromThrowable(th));
        // скасування транзакції
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // вивести повідомлення про помилки
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }

    private static List<String> getErreursFromThrowable(Throwable th) {
        // отримано список повідомлень про помилки винятку
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // повідомлення про помилку, пов'язане з об'єктом типу throwable
            erreurs.add(th.getMessage());
            // перехід до причини винятку
            th = th.getCause();
        }
        // результат
        return erreurs;
    }

    private static void show(String title, List<String> messages) {
        // заголовок
        System.out.println(String.format("%s : ", title));
        // повідомлення
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • рядки 23–29: завантаження драйвера JDBC з SGBD. У рядку 25 використовується константа [ConfigJdbc.DRIVER_CLASSNAME], визначена в проєкті [mysql-config-jdbc];
  • рядки 136–147: метод [getErreursFromThrowable] повертає список повідомлень про помилки, інкапсульованих в об’єкт типу [Throwable], який є батьківським класом класу [Exception]. Одне виключення може містити інше, яке отримується за допомогою методу [Throwable].getCause(). Таким чином, переглядаються всі виключення, інкапсульовані в об’єкті [Throwable];
  • рядки 149–156: метод [show(String title, List<String> messages)] виводить повідомлення, перед якими стоїть текст [title];
  • рядки 122–134: метод [doCatchException(String title, Connection connexion, Throwable th))] обробляє винятки, що виникають у методах класу. Оброблюваний виняток представлений параметром [Throwable th]. Мета методу полягає в тому, щоб:
    • скасувати поточну транзакцію об’єкта [Connection connexion] (рядки 127–129);
    • записати повідомлення про помилки, інкапсульовані у винятку [Throwable th] (рядки 124, 132);
  • рядки 93–120: метод [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] обробляє гілку [finally] методів доступу до SGBD. Його метою є звільнення ресурсів, задіяних під час з’єднання;

3.4.4. Видалення вмісту таблиці продуктів

Метод [delete] видаляє вміст таблиці:


    // видалення товарів
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // очищення таблиці [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, ps, connexion);
        }
}

У рядку 7 використовуються такі константи класу [ConfigJdbc]:


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

У рядку 13 підготовлено замовлення SQL наступного змісту:


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

Метод [delete] використовує транзакції. Транзакція дозволяє об’єднати замовлення SQL, які мають бути або всі виконані, або всі скасовані. Необхідно знати чотири операції:

  • початок транзакції: [connexion.setAutoCommit(false)];
  • завершення транзакції з успішним результатом: [connexion.commit()]. У цьому випадку всі операції, виконані за транзакцією BD під час транзакції, вважаються успішними;
  • завершення транзакції з помилкою: [connexion.rollback()]. У цьому випадку всі операції, виконані з BD під час транзакції, скасовуються;

У наших прикладах щоразу, коли виникає виняток, ми скасовуємо транзакцію в методі [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // виводимо повідомлення про помилки
        Static.show(title, Static.getErreursFromThrowable(th));
        // скасування транзакції
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // виведення повідомлень про помилки
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

3.4.5. Створення вмісту таблиці товарів

Метод [insert] створює вміст таблиці:


public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // заповнення таблиці
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // підготовка
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // виконання
                ps.executeUpdate();
            }
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, ps, connexion);
        }
    }

У рядку 12 підготовлена команда SQL має такий вигляд:


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

3.4.6. Відображення вмісту таблиці товарів

Метод [select] відображає вміст таблиці:


// список товарів
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі «тільки для читання»
            connexion.setReadOnly(true);
            // зчитування таблиці [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(rs, ps, connexion);
        }
    }

У рядку 14 підготовлена команда SQL має такий вигляд:


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

Метод [affiche] (рядок 18) має такий вигляд:


    // виведення об'єкта jSON
    private static void affiche(Object object) {
        try {
            System.out.println(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Вона відображає представлення jSON об’єкта, переданого як параметр (див. jSON, параграф 23.12).

3.4.7. Оновлення вмісту таблиці

Метод [update] оновлює певні продукти:


    // оновлення деяких продуктів
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // оновлення таблиці
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // категорія 1
            ps.setInt(1, 1);
            // виконання
            ps.executeUpdate();
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(null, ps, connexion);
        }
}

Рядок 13, підготовлена команда SQL має такий вигляд:


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

3.4.8. Роль транзакції

Метод [insert2] вставляє в таблицю два продукти з однаковим первинним ключем, що є неможливим. Оскільки ми перебуваємо в транзакції, перше вставлення буде скасовано.


    // додавання 2 товарів з однаковими первинними ключами
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // відкриття з'єднання
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // додано 1 рядок
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // виконання
            ps.executeUpdate();
            // цей самий рядок додається вдруге, тобто з тим самим первинним ключем
            // операція INSERTion має завершитися невдачею, і жоден з цих двох елементів не має бути вставлений через транзакцію
            ps.executeUpdate();
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробляємо виняток
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // обробляється блок finally
            doFinally(null, ps, connexion);
        }
}

Рядок 13, підготовлений запит SQL має такий вигляд:


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

3.4.9. Результати

Виконуємо конфігурацію виконання з назвою [spring-jdbc-generic-01.IntroJdbc01]:

 

Отримуємо такі результати у консолі:


------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé
  • рядок 30: перед вставкою двох продуктів з однаковим первинним ключем таблиця порожня;
  • рядок 35: після вставки двох записів з однаковим первинним ключем таблиця залишається порожньою. Це демонструє роль транзакції:
    • перше вставлення проходить успішно. Немає жодних причин, щоб воно завершилося невдачею;
    • друге вставлення завершується невдало (рядок 32). Отже, оскільки ці два вставлення відбуваються в межах однієї транзакції, усі операції SQL цієї транзакції скасовуються, зокрема й перше вставлення.

3.4.10. Висновок

У наведеному вище коді привертає увагу те, що значну частину займає обробка винятку [SQLException]. Оскільки будь-яка операція JDBC може його викликати, у коді міститься багато блоків try / catch.

3.5. Exemple-02

Ми повернемося до попереднього додатка, використовуючи джерело даних типу [javax.sql.DataSource]:

Image

Ми будемо використовувати джерело даних, реалізоване класом [org.apache.tomcat.jdbc.pool.DataSource]. Цей клас використовує пул з’єднань, тобто набір відкритих з’єднань:

  • під час інстанціювання пулу відкривається певна кількість з’єднань із базою даних. Цю кількість можна налаштувати;
  • коли код Java відкриває з’єднання, воно надається пулом;
  • коли код Java закриває з’єднання, воно повертається до пулу;

У підсумку з’єднання відкриваються лише один раз, що покращує продуктивність доступу до бази даних. Джерело даних буде визначено в класі конфігурації Spring

3.5.1. Архітектура проєкту

У цьому прикладі консольна програма використовує інтерфейс шару [JDBC].

3.5.2. Проєкт Eclipse

Новий проєкт Eclipse можна отримати, скопіювавши попередній [1-6]:

Потім проект [6] перетворюється на [7]:

3.5.3. Конфігурація Maven

Проєкт [7] — це проєкт Maven, визначений таким файлом [pom.xml]:


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

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

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- пошук батьківського елемента в репозиторії -->
    </parent>

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

    <dependencies>
        <!-- конфігурація JDBC для SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

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

</project>
  • рядки 28–33: залежність Maven від проекту [mysql-config-jdbc];

Саме проект [mysql-config-jdbc] містить у своїх залежностях Maven бібліотеку, що забезпечує реалізацію джерела даних типу [javax.sql.DataSource] (див. розділ 3.3.2):


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

3.5.4. Конфігурація Spring

  

Клас конфігурації Spring [AppConfig] має такий вигляд:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;

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

@Configuration
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
    // джерело даних
    @Bean
    public DataSource dataSource() {
        // джерело даних TomcatJdbc
        DataSource dataSource = new DataSource();
        // конфігурація доступу JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // спочатку відкриті з'єднання
        dataSource.setInitialSize(5);
        // результат
        return dataSource;
    }

}
  • рядок 10: [AppConfig] — це клас конфігурації Spring;
  • рядок 11: імпорт класу конфігурації [generic.jdbc.config.ConfigJdbc.class], визначеного в проєкті [mysql-config-jdbc]. Це означає, що доступні всі біни, визначені цим файлом конфігурації;
  • рядки 14–27: бін Spring, що визначає джерело даних;
  • рядок 17: створення джерела даних, яке ще не налаштовано;
  • рядки 19–22: інформація, яка дозволяє джерелу даних підключитися до бази даних;
  • рядок 24: створює пул із 5 з’єднань. Тут нам потрібно лише одне. Одночасних з’єднань ніколи не буває;

3.5.5. Головний клас

Головний клас [IntroJdbc02] має такий вигляд:


package spring.jdbc;

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

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

import javax.sql.DataSource;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class IntroJdbc02 {

    // мапування jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // джерело даних
    private static DataSource dataSource;

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // отримання контексту Spring
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // отримання джерела даних
            dataSource = ctx.getBean(DataSource.class);
            // очищення таблиці [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // завершено
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // список товарів
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // встановлення з'єднання
            connexion = dataSource.getConnection();
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі «тільки для читання»
            connexion.setReadOnly(true);
            // зчитування таблиці [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // фіксація транзакції
            connexion.commit();
        } catch (SQLException e1) {
            // обробка винятку
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // обробка блоку finally
            doFinally(rs, ps, connexion);
        }
    }
...
  • рядок 25: джерело даних. Зауважимо, що воно має тип [javax.sql.DataSource] (рядок 13), який є інтерфейсом;
  • рядок 31: створення екземплярів об’єктів Spring;
  • рядок 32: отримання посилання на джерело даних. Зазначимо, що ніде не згадується клас, який фактично використовується. Отже, тут ніщо не вказує на те, що використовується реалізація [TomcatJdbc];
  • рядок 49: отримання відкритого з’єднання. Саме таким чином різні методи класу [IntroJdbc02] отримують з’єднання з базою даних. Решта коду ідентична коду класу [IntroJdbc01];

3.5.6. Тести

Виконуємо конфігурацію виконання з назвою [spring-jdbc-generic-02.IntroJdbc02]:

 

Отримуємо ті самі результати, що й раніше (параграф 3.4.9).

3.6. Exemple-03

3.6.1. Архітектура проекту

У цьому прикладі методи доступу до даних виокремлені в окремий рівень [dao]. Вони будуть перевірені за допомогою тесту JUnit.

3.6.2. Проєкт Eclipse

Проєкт Eclipse [spring-jdbc-03] — це проєкт Spring / Maven, побудований так само, як і попередній, але доповнений наступним чином:

 

Різні пакети виконують такі функції:

  • [spring.jdbc.config]: конфігурація проекту Spring;
  • [spring.jdbc.dao]: реалізація шару [DAO];
  • [spring.jdbc.infrastructure]: реалізує неконтрольоване виключення [DaoException];

3.6.3. Налаштування Maven

Проєкт Maven налаштовується за допомогою такого файлу [pom.xml]:


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

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

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- пошук батьківського об’єкта в репозиторії -->
    </parent>

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

    <dependencies>
        <!-- конфігурація JDBC для SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

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

</project>

Він ідентичний файлу конфігурації проєкту [spring-jdbc-02]. Зокрема, він використовує залежність Maven від проєкту [mysql-config-jdbc] (рядки 28–32).

3.6.4. Інтерфейс шару [DAO]

  

Рівень [DAO] має такий інтерфейс [IDao]:


package spring.jdbc.dao;

import java.util.List;

import spring.jdbc.entities.Produit;

public interface IDao {

    // додавання товарів
    public List<Produit> addProduits(List<Produit> produits);

    // перелік усіх товарів
    public List<Produit> getAllProduits();

    // конкретний товар
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // оновлення декількох товарів
    public int updateProduits(List<Produit> produits);

    // видалення всіх товарів
    public int deleteAllProduits();

    // видалення декількох товарів
    public int deleteProduits(int[] ids);
}

3.6.5. Клас [DaoException]

Клас [DaoException] просто розширює клас [UncheckedException], представлений у розділі 3.3.5:

  

package spring.jdbc.infrastructure;

public class DaoException extends UncheckedException {

    private static final long serialVersionUID = 1L;

    // виробники
    public DaoException() {
        super();
    }

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

}

3.6.6. Налаштування проекту Spring

  

Клас [AppConfig], який налаштовує проект Spring, ідентичний файлу конфігурації Spring із прикладу [spring-jdbc-02], за винятком 11-го рядка:


package spring.jdbc.config;

import generic.jdbc.config.ConfigJdbc;

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

@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
    // джерело даних
    @Bean
    public DataSource dataSource() {
        // джерело даних TomcatJdbc
        DataSource dataSource = new DataSource();
        // конфігурація доступу JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // спочатку відкриті з'єднання
        dataSource.setInitialSize(5);
        // результат
        return dataSource;
    }
}
  • рядок 11: пакет [spring.jdbc.dao] буде проскановано на наявність інших компонентів Spring, крім тих, що визначені в цьому конфігураційному файлі;

3.6.7. Реалізація шару [DAO]

  

Нагадаємо (параграф 3.6.4), що рівень [DAO] реалізує наступний інтерфейс [IDao]:


package spring.jdbc.dao;

import generic.jdbc.entities.dbproduits.Produit;

import java.util.List;

public interface IDao {

    // додати товари
    public List<Produit> addProduits(List<Produit> produits);

    // список усіх товарів
    public List<Produit> getAllProduits();

    // конкретний товар
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // оновлення декількох товарів
    public int updateProduits(List<Produit> produits);

    // видалення всіх товарів
    public int deleteAllProduits();

    // видалення декількох товарів
    public int deleteProduits(int[] ids);
}

Обидва класи [Dao1, Dao2] реалізують цей інтерфейс. Клас [Dao2] є варіантом класу [Dao1], який вводить нову синтаксичну особливість. Ми зосередимося на класі [Dao1]. Його структура така:


package spring.jdbc.dao;

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

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

import javax.sql.DataSource;

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

import spring.jdbc.infrastructure.DaoException;

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

    // назва класу
    private String simpleClassName = getClass().getSimpleName();
    // джерело даних
    @Autowired
    protected DataSource dataSource;

    // виробник
    public Dao1() {
        System.out.println("building Dao1...");
    }

    // ------------------------------- інтерфейс
    @Override
    public List<Produit> getAllProduits() {
...
    }

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

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

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

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

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

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

    // ---------------------------------------- локальні методи
    // обробка finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }

    // обробка catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
  • рядок 20: клас [Dao] є компонентом Spring із назвою [dao1]. Ця назва є необов’язковою. Якщо її немає, використовується назва класу, у якій перша літера перетворюється на малу;
  • рядок 24: ім’я класу. Не слід вказувати фіксоване значення [Dao], щоб залишити можливість перейменувати клас без необхідності переопределення цього поля, яке таким чином завжди залишається дійсним;
  • рядки 26–27: підстановка джерела даних [tomcat-jdbc], визначеного в класі конфігурації [AppConfig];
  • рядки 36–68: реалізація інтерфейсу [IDao];
  • рядки 78–80: централізоване управління catch для різних методів;
  • рядки 72–75: централізоване управління finally для різних методів;

catch для різних методів управляється наступним чином:


    // обробка catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // скасування транзакції
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            e2.printStackTrace();
        }
        // daoException
        return new DaoException(code, th, simpleClassName);
}
  • рядок 2: метод оголошується як [protected], що дозволяє дочірнім класам використовувати його, не роблячи його при цьому публічним. Він приймає такі параметри:
    • [Connection connexion]: з’єднання з SGBD — можливо, null;
    • [Throwable th]: виняток, що виник і який буде інкапсульовано в тип [DaoException];
    • [int code]: код помилки, який слід використовувати, якщо метод створює новий об’єкт типу [DaoException];
  • рядки 4–7: основне призначення цього методу — скасувати транзакцію, пов’язану з підключенням, переданим у параметрі 1;
  • рядки 8–10: якщо скасування транзакції завершилося невдало, на консоль записується слід винятку. Більше нічого зробити не можна, оскільки у рядку 12 буде згенеровано виняток;

finally різних методів обробляється наступним чином:


// обробка finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // закриття ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // закриття [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // завершення сеансу
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // записувати помилку, якщо це можливо
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // результат
        return daoException;
    }
  • рядок 2: цей метод також оголошений як [protected]. Він отримує такі параметри:
    • [ResultSet rs]: можливий [ResultSet], якщо була виконана операція [SELECT] — можливо, null;
    • [PreparedStatement ps]: операція [PreparedStatement], яка була виконана — можливо, null;
    • [Connection connexion]: зв’язок із SGBD — можливо, null;
    • [int code]: код помилки, який слід використовувати, якщо метод створює новий [DaoException];
    • [DaoException daoException]: можливий [DaoException], якщо перед finally стався виняток — можливо, null;
  • рядки 21–30: основною метою цього методу є закриття з’єднання (рядок 23);
  • рядки 24–29: якщо під час цього закриття виникає виняток, то перевіряється стан параметра [DaoException daoException], який нам передали: якщо [daoException == null], то створюється новий [DaoException] із кодом, переданим у параметрі;
  • рядок 32: як результат повертається старий або новий об’єкт [DaoException];

Ми не будемо розглядати всі методи класу [Dao], а лише деякі з них. Усі вони схожі між собою.

3.6.7.1. Метод [getProduitById]

Метод [getProduitById] повертає запис, первинний ключ якого дорівнює параметру [id], або null в іншому випадку;


@Override
    public Produit getProduitById(int id) {
        // ресурси з'єднання
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // спочатку немає винятків
        DaoException daoException = null;
        // пошукований товар
        Produit produit = null;
        try {
            // відкриття з'єднання
            connexion = dataSource.getConnection();
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі тільки для читання
            connexion.setReadOnly(true);
            // зчитування таблиці [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
            ps.setInt(1, id);
            rs = ps.executeQuery();
            if (rs.next()) {
                produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
            }
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // обробка винятку
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // обробка блоку finally
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // виняток?
        if (daoException != null) {
            throw daoException;
        }
        // результат
        return produit;
    }
  • рядок 10: продукт, що підлягає поверненню, встановлюється як null;
  • рядок 19: замовлення SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] має такий вигляд:

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

  • рядки 22–24: якщо [ResultSet] має рядок, його використовують для створення товару, що підлягає поверненню, інакше товар, що підлягає поверненню, залишається в null;
  • рядок 41: продукт повертається;
  • рядок 8: виняток [DaoException] методу ініціалізується як null;
  • рядок 31: метод [doCatchException] створює виняток [DaoException];
  • рядок 34: параметр [daoException] методу [doFinally] є або null,, або винятком, створеним методом [doCatchException]. Метод [doFinally]:
    • залишає цей параметр без змін, якщо їй вдається закрити з’єднання;
    • залишає цей параметр без змін, якщо не вдається закрити з’єднання і раніше вже було виконано [DaoException];
    • створює новий виняток [DaoException], якщо не вдається закрити з’єднання і раніше не було винятку [DaoException];
  • рядки 37–39: якщо локальне виключення [daoException] не дорівнює null, то його запускають, інакше повертають запитуваний результат (рядок 41);

3.6.7.2. Метод [deleteProduits]

Метод [deleteProduits] видаляє продукти, первинні ключі яких передаються йому як параметр. Він повертає кількість видалених продуктів.


@Override
    public int deleteProduits(int[] ids) {
        // ресурси з'єднання
        PreparedStatement ps = null;
        Connection connexion = null;
        // спочатку винятку немає
        DaoException daoException = null;
        // кількість оновлених товарів
        int nbProduits = 0;
        try {
            // відкриття з'єднання
            connexion = dataSource.getConnection();
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // видалення товарів
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // параметри
                ps.setInt(1, id);
                // виконання
                nbProduits += ps.executeUpdate();
            }
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // обробка винятку
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // обробка блоку finally
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // виняток?
        if (daoException != null) {
            throw daoException;
        }
        // результат
        return nbProduits;
    }
  • рядок 18, порядок SQL [ConfigJdbc.V2_DELETE_PRODUITS] такий:

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

  • рядки 18–24: код видалення товарів. Бачимо, що замовлення SQL готується 1 раз (рядок 18) і виконується n разів (рядки 19–24). У цьому полягає користь об’єкта [PreparedStatement];
  • рядок 23: метод [PreparedStatement].executeUpdate() повертає кількість рядків, на які вплинула операція оновлення;
  • рядок 41: повертається кількість оновлених продуктів;

3.6.7.3. Метод [updateProduits]

Метод [updateProduits] оновлює в базі даних товари, які передаються йому як параметри. Він повертає кількість оновлених товарів.


@Override
    public int updateProduits(List<Produit> produits) {
        // ресурси з'єднання
        PreparedStatement ps = null;
        Connection connexion = null;
        // спочатку винятку немає
        DaoException daoException = null;
        // кількість оновлених товарів
        int nbProduits = 0;
        try {
            // відкриття з'єднання
            connexion = dataSource.getConnection();
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // оновлюється таблиця [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // параметри
                ps.setString(1, produit.getNom());
                ps.setDouble(2, produit.getPrix());
                ps.setInt(3, produit.getCategorie());
                ps.setString(4, produit.getDescription());
                ps.setInt(5, produit.getId());
                // виконання
                nbProduits += ps.executeUpdate();
            }
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // обробка винятку
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // обробка блоку finally
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // виняток?
        if (daoException != null) {
            throw daoException;
        }
        // результат
        return nbProduits;
    }
  • рядок 18: порядок виконання SQL [ConfigJdbc.V2_UPDATE_PRODUITS] такий:

public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
  • рядки 19–28: код оновлення товарів;

3.6.7.4. Метод [addProduits]

Метод [addProduits] додає до бази даних товари, які передаються йому як параметри. Він повертає ці самі товари разом із їхніми первинними ключами (до додавання в базу даних товари не мають первинного ключа).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // ресурси з'єднання
        PreparedStatement ps = null;
        Connection connexion = null;
        // спочатку винятку немає
        DaoException daoException = null;
        try {
            // відкриття з'єднання
            connexion = dataSource.getConnection();
            // у режимі читання/запису
            connexion.setReadOnly(false);
            // початок транзакції
            connexion.setAutoCommit(false);
            // додавання елементів до таблиці [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // параметри
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // виконання команди
                ps.executeUpdate();
                // згенеровано первинний ключ
                ResultSet generatedKeys = ps.getGeneratedKeys();
                if (generatedKeys.next()) {
                    produit.setId(generatedKeys.getInt(1));
                } else {
                    throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
                            produit.getNom()));
                }
            }
            // фіксація транзакції
            connexion.commit();
            // повернення до режиму за замовчуванням
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // обробка винятку
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // обробка блоку finally
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // виняток?
        if (daoException != null) {
            throw daoException;
        }
        // результат
        return produits;
}
  • у рядку 16 порядок SQL [ConfigJdbc.V2_INSERT_PRODUITS] є таким:

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

У наведеному вище прикладі команда вставки товару не містить первинного ключа [ID]. Оскільки первинний ключ бази MySQL має атрибут [AUTOINCREMENT], SGBD буде генерувати первинний ключ для кожного введення. Виникає проблема з отриманням цього ключа. Це важливий момент, оскільки операції з товарами виконуються за допомогою їхніх первинних ключів. Тому необхідно знати ці ключі;

  • рядки 17–33: цикл вставки товарів;
  • рядок 16: особлива форма методу [prepareStatement]. Другий параметр [generatedColumns] — це масив імен стовпців, значення яких потрібно отримати після вставки. У рядку 16 ми вказали, що хочемо отримати значення стовпця [id]. Слід зауважити, що хоча імена стовпців таблиці не чутливі до регістру (великі/малі літери), SGBD PostgreSQL вимагає, щоб це ім’я було написано малими літерами. Це типові проблеми, які виникають під час перенесення коду з одного SGBD на інший;
  • рядок 24: вставка рядка в базу даних;
  • рядок 26: отримуємо список значень стовпців, зазначених у рядку 16 у [ResultSet]. Тут, для 1 вставки, [ResultSet] матиме 1 рядок, і цей рядок матиме 1 єдиний стовпець, що містить первинний ключ;
  • рядок 28: отримуємо первинний ключ, згенерований SGBD;
  • рядки 29–32: якщо не отримано згенерований первинний ключ, запускається [RuntimeException], який буде інкапсульовано в [DaoException] (рядки 38–40);

3.6.8. Клас [Dao2]

  

Клас [Dao2] є варіантом класу [Dao1], що використовує синтаксис, який називається try-with-resource(resource):

1
2
3
4
try(resource){
...
}
...
  • [resource] — це ресурс, що реалізує інтерфейс [java.lang.AutoCloseable]. Усі ресурси, які звільняються за допомогою методу [close], входять до нього. Цей синтаксис гарантує, що в рядку 4 ресурс [resource] буде закритий. Це дозволяє уникнути написання оператора [finally] для виконання цієї операції закриття;

Візьмемо для прикладу метод [getAllProduits] класу [Dao2]:


    @Override
    public List<Produit> getAllProduits() {
        // можливе виключення
        DaoException daoException = null;
        // список товарів
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // початок транзакції
            connexion.setAutoCommit(false);
            // у режимі тільки для читання
            connexion.setReadOnly(true);
            // зчитування таблиці [PRODUITS]
            try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
                    }
                }
                // завершення транзакції
                connexion.commit();
                // повернення до режиму за замовчуванням
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // скасування транзакції
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // обробка винятку
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // виняток?
        if (daoException != null) {
            throw daoException;
        }
        // результат
        return produits;
}
  • рядок 7: блок try з ресурсом [Connection]. У рядку 27 ми впевнені, що цей ресурс закрито;
  • рядок 13: блок try з ресурсом [PreparedStatement]. У рядку 23 гарантується, що цей ресурс закрито;
  • рядок 14: try з ресурсом [ResultSet]. У рядку 19 гарантується, що цей ресурс закрито;
  • рядок 25: транзакція скасовується наступним чином:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // генерація винятку
        return new DaoException(code, e1, simpleClassName);
}

У підсумку ми отримали код, який легше читати.

3.6.9. Реалізація тестового шару

3.6.9.1. Тестові класи

  
  • тест [JUnitTestDao1] є тестом JUnit класу [Dao1];
  • тест [JUnitTestDao2] є тестом JUnit класу [Dao2];
  • [AbstractJUnitTestDao] — це батьківський клас для двох попередніх класів тестів;
  • [MainTestDao1] — це тестовий консольний клас класу [Dao1];
  • [MainTestDao2] — це тестовий консольний клас класу [Dao2];
  • [AbstractMainTestDao] є батьківським класом двох попередніх класів. Він повторює код консольних класів [IntroJdbc01, IntroJdbc02], які вже розглядалися, тому ми не будемо вивчати ці консольні класи;

Клас [JUnitTestDao1] має такий вигляд:


package spring.jdbc.tests;

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

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

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

    // шар [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;

    @Override
    IDao getDao() {
        return dao;
    }

}
  • коментарі до рядків 12–13 були наведені в розділі 2.5.5. Вони дозволяють тесту Junit отримати простий доступ до контексту Spring та його бінів. Цей контекст налаштовується класом [AppConfig] (рядок 12), розглянутим у розділі 2.4.3;
  • рядок 14: клас успадковує клас [AbstractJUnitTestDao], який ми розглянемо далі. Саме в цьому класі містяться методи тестування JUnit;
  • рядки 17–19: бін із назвою [dao1] (рядок 18) вводиться (рядок 17). Отже, тут вводиться екземпляр класу [Dao1];
  • рядки 21–24: метод [getDao] перевизначає однойменний метод у батьківському класі;

Зрештою, мета цього класу полягає в тому, щоб повернути батьківському класу посилання на шар [DAO], який має бути протестований, у даному випадку — екземпляр класу [Dao1]. Аналогічно клас [JUnitTestDao2] повертає батьківському класу [AbstractJUnitTestDao] екземпляр класу [Dao2].

Клас [AbstractJUnitTestDao] є тестовим класом для JUnit:


package spring.jdbc.tests;

import generic.jdbc.entities.dbproduits.Produit;

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

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

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

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

public abstract class AbstractJUnitTestDao {

    // шар [DAO]
    abstract IDao getDao();

    // маппер jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();

    @Before
    public void clean() {
        // перед кожним тестом очищаємо базу даних
        log("Vidage de la base de données", 1);
        getDao().deleteAllProduits();
    }

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

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

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

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

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

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

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

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

    // -------------- приватні методи
...
}
  • у рядку 19 клас [AbstractJUnitTestDao] є абстрактним;
  • рядок 22: абстрактний метод [getDao], який дозволяє отримати посилання на шар [DAO], що тестується. Цей метод реалізується дочірніми класами;
  • рядок 25: мапер jSON, який дозволить нам вивести на консоль значення jSON для продуктів;
  • рядки 27–32: перед кожним тестом (рядок 27) таблиця [PRODUITS] очищується;

3.6.9.2. Приватний метод [fill]

Приватний метод [fill] використовується для внесення товарів до таблиці [PRODUITS].


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // створюємо список товарів
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String name, int category, double price, String description
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // зберігаємо його в базі даних — отримуємо товари за їхнім первинним ключем
        produits = getDao().addProduits(produits);
        // створюємо словник товарів, щоб легше їх знаходити
        // ключем словника є первинний ключ товару в базі даних
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // повертаємо товари
        return produits;
    }
  • рядок 1: метод [fill] вставляє [nbProduits] у таблицю [PRODUITS], яка вважається порожньою;
  • рядки 3–10: створення списку товарів у такому форматі:

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

який використовує конструктор Produit(int id, String nom, int categorie, double prix, String description). Значення першого параметра [id] (первинний ключ таблиці [PRODUITS]) не має значення, оскільки метод [addProduits] у рядку 10 не вставляє його в базу даних і дозволяє SGBD самостійно генерувати його значення;

  • рядок 12: список товарів зберігається в базі даних. Кожен товар у цьому списку доповнюється новим первинним ключем [id]. Метод [addProduits] повертає у результаті свій параметр [produits]. Отже, можна було б не зберігати результат;
  • рядки 15–17: додаємо товари до словника:

    // словник товарів
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

Ключ словника є первинним ключем продукту, а відповідне значення — самим продуктом;

  • рядок 19: повертається список продуктів;

3.6.9.3. Тест [getProduits]

Він має такий вигляд:


    @Test
    public void getProduits() throws JsonProcessingException {
        // заповнення
        fill(10);
        // список продуктів
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // перевіряємо, чи отриманий список і збережений список збігаються
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // усі початкові товари мають зникнути зі словника
        Assert.assertEquals(0, mapProduits.size());
}
}
  • рядок 4: у базу даних заносяться 10 товарів;
  • рядок 7: після цього запитуємо перегляд усіх товарів у базі;
  • рядок 8: вони виводяться на екран. Мета — переконатися, що товари дійсно були збережені та мають первинний ключ;
  • рядки 10–13: перевіряємо, чи знайдені товари збігаються з тими, що були збережені, і чи можна їх знайти у словнику [mapProduits];
  • рядок 11: з словника вибираємо товар, який має той самий первинний ключ, що й той, який було отримано з бази даних. Це показує, що збережені товари дійсно отримали первинний ключ;
  • рядок 12: переконуємося, що обидва продукти є ідентичними. Нагадаємо, що клас [Produit] визначив метод [equals] (див. параграф 3.3.4);
  • рядок 13: знайдений елемент видаляється зі словника;
  • рядок 16: перевіряємо, чи словник початкових продуктів дійсно порожній, що означає, що всі ці початкові продукти були присутні у списку продуктів, отриманих із бази даних;

Метод [affiche] у рядку 8 є таким приватним методом:


    // відображення списку товарів
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • рядок 2: метод [affiche] є загальним методом. Він параметризується типом T, що синтаксично позначається як <T>. Якби він параметризувався двома типами T1 та T2, то записувався б як <T1,T2>. Синтаксис методу m, що має параметр типу T, є таким:
portée <T> type_résultat m(... , T value1, ...){
...
    T value2=...
}

У коді методу m містяться дані типу T. Метод m екземпляра c класу C можна викликати таким чином:

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

де T1 — це фактичний тип, який замінить формальний тип T методу m. У більшості випадків компілятор здатний вивести тип T1 на основі аргументів методу m. Тому попередню інструкцію найчастіше спрощують до:

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

Повернемося до методу [affiche]. Він виводить список елементів типу T. Це можливо, оскільки мапер jSON, який використовується в рядку 4, здатний відтворити представлення jSON для будь-якого типу об’єкта. У цьому конкретному прикладі єдиним типом T, що використовується, буде тип [Produit].

Метод [affiche] також можна було б написати таким чином:


    // відображення списку товарів
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

Оскільки фактичним параметром є список товарів, у рядку 3 було б записано представлення jSON цього списку. Це не те саме, що записувати по одному представлення кожного з його елементів.

Результат, отриманий у результаті тесту [getProduits], виглядає так:

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

3.6.9.4. Тест [getProduitBy]

Його результат такий:


    @Test
    public void getProduitBy() {
        // заповнення
        fill(10);
        log("getProduitBy", 1);
        Produit produit = getDao().getProduitByName("NOM3");
        Produit produit2 = getDao().getProduitById(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
}
  • рядок 6: метод [getProduitByName] інтерфейсу [IDao] використовується для повернення об’єкта з іменем [NOM3];
  • рядок 7: потім використовується метод [getProduitById] інтерфейсу [IDao] для повернення того самого продукту, який цього разу ідентифікується за його первинним ключем;
  • рядки 8–10: перевіряється, чи [produit2] та [produit] мають однакові характеристики;

3.6.9.5. Тест [doInsertsInTransaction]

Він виглядає так:


    @Test
    public void doInsertsInTransaction() {
        log("Ajout de deux produits de même nom", 1);
        // здійснюється вставка
        List<Produit> inserts = new ArrayList<Produit>();
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        boolean erreur = false;
        try {
            getDao().addProduits(inserts);
        } catch (DaoException daoException) {
            erreur = true;
        }
        // перевірка
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • рядки 5–7: створюється список із двох товарів з однаковою назвою [x];
  • рядок 10: ці два товари вставляються в таблицю [PRODUITS], яка є порожньою (метод [clean], анотований за допомогою [@Before]). Перше вставлення відбудеться, але друге — ні, оскільки таблиця [PRODUITS] має обмеження унікальності щодо назв продуктів. Отже, має статися виняток. Це перевіряється у рядку 15;
  • оскільки всі методи інтерфейсу [IDao] виконуються в межах транзакції, невдача другого вставлення призведе до скасування всієї транзакції, а отже, і першого вставлення. У підсумку жодне вставлення не має відбутися в таблиці [PRODUITS];
  • рядки 16–17: це перевіряється шляхом запиту списку товарів, що містяться в таблиці [PRODUITS], та перевірки того, що цей список порожній;

3.6.9.6. Тест [updateProduits]

Він виглядає наступним чином:


    @Test
    public void updateProduits() {
        // заповнення
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // отримання товарів
        List<Produit> produits = getDao().getAllProduits();
        // оновлюємо товари категорії 1
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String name, int category, double price, String description
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // перевірки
        // Assert.assertEquals(nbUpdated, nbProduits); -- не працює з DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • рядок 4: додаємо 10 товарів до бази даних;
  • рядок 7: витягуємо їх;
  • рядки 9–18: ціни на товари категорії № 1 підвищуються на 10 %;
  • рядок 19: ці зміни записуються в базу даних;
  • рядки 22–25: виконується перегляд у пам’яті списку товарів, який використовувався для оновлення. Для кожного з них у базі даних шукається товар із таким самим первинним ключем і перевіряється, чи оновлення ціни відбулося;
  • рядок 19: отримується кількість товарів, оновлених операцією [updateProduits];
  • рядок 21: перевіряється, чи ця кількість відповідає очікуваній. Ця перевірка проходить успішно для всіх операцій SGBD, крім SGBD та DB2. Тому їх було виведено в коментарі;

3.6.9.7. Тест [deleteProduits]

Він виглядає так:


    @Test
    public void deleteProduits() {
        // заповнення
        fill(10);
        log("deleteProduits", 1);
        // перелік товарів
        List<Produit> produits = getDao().getAllProduits();
        // видалення двох товарів
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // перевірки
        // Assert.assertEquals(2, nbDeleted); -- не працює з DB2
        Assert.assertNull(getDao().getProduitById(produit0.getId()));
        Assert.assertNull(getDao().getProduitById(produit5.getId()));
        Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
  • рядок 4: додаємо 10 товарів до бази даних;
  • рядки 7–11: витягуємо всі товари з бази даних і видаляємо з неї товари, витягнуті з позицій 0 і 5;
  • рядки 14–16: перевіряємо, чи обидва товари більше не знаходяться в базі даних і чи в ній стало на два товари менше;
  • тест у рядку 13 не проходить із SGBD DB2. Він проходить з іншими SGBD;

3.6.9.8. Тести продуктивності

У тести було включено три методи, єдиною метою яких є оцінка продуктивності SGBD:


    @Test
    public void perf1() {
        // заповнення
        fill(10000);
    }

    @Test
    public void perf2() {
        // заповнення
        fill(10000);
        // зміна
        List<Produit> produits = getDao().getAllProduits();
        // оновлюються записи категорії 1
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String nom, int categorie, double prix, String description
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }

    @Test
    public void perf3() {
        // заповнення
        fill(10000);
        // видалення
        List<Produit> produits = getDao().getAllProduits();
        // первинні ключі
        int[] keys = new int[produits.size()];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = produits.get(i).getId();
        }
        getDao().deleteProduits(keys);
}
  • рядки 1–5: введення 10 000 товарів;
  • рядки 8–20: введення 10 000 товарів, а потім їхня модифікація за первинними ключами;
  • рядки 23–34: введення 10 000 товарів, а потім їх видалення за первинними ключами;

Для виконання тестів [JUnitTestDao1] та [JUnitTestDao2] можна використовувати такі конфігурації виконання:

Результати тесту [JUnitTestDao1] такі:

У [1] відображаються результати [JUnitTestDao1], а в [2] — результати [JUnitTestDao2]. Між ними немає суттєвих відмінностей. У [1]:

  • тест пройшов успішно;
  • додавання 10 000 товарів займає 3,15 секунди;
  • додавання 10 000 товарів з подальшою їхньою зміною триває 4,80 секунди;
  • додавання 10 000 товарів з подальшим їх видаленням триває 4,40 секунди;
  • отже, найресурсомісткішою операцією є введення;