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]:
![]() |
Ви повинні отримати такі результати у консолі:
У наведених нижче прикладах читач зможе:
- або працювати безпосередньо з раніше завантаженими проектами;
- або самостійно створити проекти;
3.2. Етапи роботи з базою даних
![]() |
У наведеній вище архітектурі робота з базою даних за допомогою консольної програми включає такі етапи:
- завантаження драйвера JDBC бази даних;
- відкриття з'єднання з базою;
- виконання команди SQL у базі даних та обробка результатів команди SQL;
- закриття з’єднання;
Крок 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 така:
Цей метод намагається перейти до наступного рядка [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] доступні такі методи:
для отримання стовпця з назвою «labelColi» з поточного рядка, а отже, стовпця з файлу [SELECT], що має цей міток. Type позначає тип поля coli. Можна використовувати такі методи [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Замість імені стовпця можна використовувати його позицію у виконаному запиті [SELECT]:
де 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] є третім параметром конструктора. Ідея полягає в тому, щоб запам’ятати, де саме у коді користувача сталося виключення. Код користувача інкапсулює виключення таким чином:
- рядок 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]:

Ми будемо використовувати джерело даних, реалізоване класом [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):
- [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, є таким:
У коді методу m містяться дані типу T. Метод m екземпляра c класу C можна викликати таким чином:
де T1 — це фактичний тип, який замінить формальний тип T методу m. У більшості випадків компілятор здатний вивести тип T1 на основі аргументів методу m. Тому попередню інструкцію найчастіше спрощують до:
Повернемося до методу [affiche]. Він виводить список елементів типу T. Це можливо, оскільки мапер jSON, який використовується в рядку 4, здатний відтворити представлення jSON для будь-якого типу об’єкта. У цьому конкретному прикладі єдиним типом T, що використовується, буде тип [Produit].
Метод [affiche] також можна було б написати таким чином:
// відображення списку товарів
private void affiche(Object o) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(o));
}
Оскільки фактичним параметром є список товарів, у рядку 3 було б записано представлення jSON цього списку. Це не те саме, що записувати по одному представлення кожного з його елементів.
Результат, отриманий у результаті тесту [getProduits], виглядає так:
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 секунди;
- отже, найресурсомісткішою операцією є введення;



















































