4. Вступ до Spring JDBC
У цьому розділі ми розглянемо таку архітектуру:
![]() |
Отже, це та сама архітектура, що й раніше. Ми внесемо дві зміни:
- база даних матиме дві таблиці, пов’язані відношенням із зовнішнім ключем;
- рівень [DAO] буде реалізовано за допомогою бібліотеки [Spring JDBC], що спрощує управління API та JDBC;
4.1. Налаштування робочого середовища
За допомогою STS імпортуйте проект [spring-jdbc-04], який знаходиться у папці [<exemples>/spring-database-generic/spring-jdbc]
![]() |
Крім того, нам потрібно створити нову базу даних MySQL за допомогою клієнта [MyManager] (див. розділ 3.1):
![]() |
- у [3]; наведені нижче приклади працюють із базою MySQL, яка має назву [dbproduitscategories];
![]() |
- у [9] введіть пароль користувача root (у цьому документі цей пароль — root);
![]() |
![]() |
- у [18] база даних [dbproduitscategories] була створена порожньою. Створюються таблиці та заповнюються за допомогою скрипта SQL [19-20];
![]() |
- у [21] перейдіть до папки [<exemples>/spring-database-config/mysql/databases];
![]() |
- у [25] переконайтеся, що ви перебуваєте на базі [dbproduitscategories], а не на базі [dbproduits];
- у [29] скрипт SQL створив п’ять таблиць. Таблиці [ROLES, USERS, USERS_ROLES] будуть використовуватися лише під час забезпечення безпеки веб-сервісу, створеного для публікації бази даних [dbproduitscategories] у мережі Інтернет;
4.2. База даних [dbproduitscategories]
База даних [dbproduitscategories] є розширенням раніше розглянутої бази [dbproduits]. Якщо в таблиці [PRODUITS] товар мав категорію, позначену номером, який не мав особливого значення, то тут цей номер буде зовнішнім ключем у таблиці [CATEGORIES].
Таблиця [PRODUITS] має такий вигляд:
![]() |
- [ID]: автоінкрементний первинний ключ таблиці [2];
- [NOM]: унікальна назва товару [4];
- [PRIX]: ціна товару;
- [DESCRIPTION] — опис товару;
- [VERSIONING] — номер версії товару. Його початкова версія — 1 [3]. Кожного разу, коли товар буде змінено, його номер версії збільшуватиметься на одиницю за допомогою коду, що використовує цю таблицю;
- [CATEGORIE_ID]: зовнішній ключ у таблиці [CATEGORIES] для позначення категорії, до якої належить продукт;
![]() |
- у [1-3] — зовнішній ключ [CATEGORIE_ID] з таблиці [PRODUITS]. Вона стосується стовпця [ID] таблиці [CATEGORIES] [4-5];
- коли категорія видаляється, видаляються також усі пов’язані з нею товари [6]. Цей момент важливо відзначити, оскільки він використовується при побудові шару [DAO], що використовує базу [dbproduitscategories];
Таблиця категорій [CATEGORIES] має такий вигляд:
![]() |
- [ID]: первинний ключ з автоінкрементом;
- [VERSIONING]: номер версії категорії;
- [NOM]: унікальна назва категорії;
4.3. Проєкт Eclipse
![]() |
Проєкт [spring-jdbc-04] реалізує таку архітектуру:
![]() |
Проєкт [spring-jdbc-04] — це проєкт 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-04</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-04</name>
<description>Demo project for Spring JdbcTemplate</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- пошук батьківського елемента в репозиторії -->
</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>
<!-- Spring JdbcTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- рядки 28–32: проект базується на проекті [mysql-config-jdbc], який налаштовує рівень JDBC;
- рядки 34–37: артефакт [spring-boot-starter-jdbc] імпортує бібліотеки Spring JDBC;
У підсумку залежності виглядають наступним чином:
![]() |
4.4. Конфігурація Spring
![]() |
Клас [AppConfig], який налаштовує проект Spring, має такий вигляд:
package spring.jdbc.config;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
@EnableTransactionManagement
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
// джерело даних
@Bean
public DataSource dataSource() {
// джерело даних TomcatJdbc
DataSource dataSource = new DataSource();
// конфігурація доступу JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// спочатку відкриті з'єднання
dataSource.setInitialSize(5);
// результат
return dataSource;
}
// Менеджер транзакцій
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// JdbcTemplate
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
// додавання товару
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_PRODUITS).usingGeneratedKeyColumns(
ConfigJdbc.TAB_PRODUITS_ID);
}
// додавання категорії
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES).usingGeneratedKeyColumns(
ConfigJdbc.TAB_CATEGORIES_ID);
}
}
- рядок 16: клас є класом конфігурації Spring;
- рядок 17: пакет [spring.jdbc.dao] буде проскановано на наявність інших компонентів Spring, крім тих, що містяться у класі [AppConfig]. У ньому буде знайдено компонент, що реалізує рівень [DAO];
- рядок 18: ми не будемо самостійно керувати транзакціями, а доручимо це Spring JDBC. Єдине, що потрібно зробити, — це позначити методи, які мають виконуватися в рамках транзакції, анотацією Spring [@Transactional]. Рядок 18 гарантує, що ця анотація буде оброблена, а не проігнорована. Управління транзакціями забезпечується однією із залежностей проекту Spring JDBC, імпортованого файлом [pom.xml];
- рядок 19: імпортуються біни, вже визначені в класі [generic.jdbc.config.ConfigJdbc] проекту [mysql-config-jdbc];
- рядки 23–36: джерело даних [tomcat-jdbc], введене у прикладі [spring-jdbc-02];
- рядки 40–42: менеджер транзакцій, пов’язаний із раніше визначеним джерелом даних. Бін обов’язково має називатися [transactionManager], оскільки саме ця назва використовується анотацією [@EnableTransactionManagement]. Менеджер [DataSourceTransactionManager] надається бібліотекою Spring JDBC (рядок 12);
- рядки 45–48: бін [namedParameterJdbcTemplate], на якому базуватиметься реалізація шару [DAO]. Цей bean надається бібліотекою Spring JDBC (рядок 10). Цей bean також пов’язаний із джерелом даних, визначеним раніше (рядок 47);
- рядки 51–55: бін [simpleJdbcInsertProduit] (ім’я можна вибрати довільно) використовуватиметься для вставки товару в таблицю [PRODUITS] та отримання згенерованого первинного ключа. Використовуються такі параметри:
- [dataSource]: джерело даних [tomcat-jdbc] із рядків 24–36;
- [ConfigJdbc.TAB_PRODUITS]: таблиця [PRODUITS];
- [ConfigJdbc.TAB_CATEGORIES_ID]: стовпець первинного ключа таблиці [PRODUITS]. Нагадуємо, що для PostgreSQL назва цього стовпця має бути написана малими літерами;
- рядки 58–62: бін [simpleJdbcInsertCategorie] буде використовуватися для вставки категорії в таблицю [CATEGORIES] та отримання згенерованого первинного ключа;
4.5. Винятки проекту
![]() |
Ми вже розглядали класи [UncheckedException, DaoException, ShortException] у проєкті [spring-jdbc-03]. Додаємо ще один:
package spring.jdbc.infrastructure;
public class MyIllegalArgumentException extends UncheckedException {
private static final long serialVersionUID = 1L;
// виробники
public MyIllegalArgumentException() {
super();
}
public MyIllegalArgumentException(int code, Throwable e, String className) {
super(code, e, className);
}
}
- клас [MyIllegalArgumentException] походить від класу [UncheckedException] і, отже, є неконтрольованим класом. Він використовуватиметься для сигналізації про виклик методу шару [DAO] з неправильними аргументами. Його не назвали [IllegalArgumentException], оскільки це виключення вже існує в JDK, і це іноді призводило до того, що компілятор генерував некоректний [import];
4.6. Елементи проекту
![]() |
Класи пакета [spring.jdbc.entities] є відображеннями рядків таблиць бази даних [dbproduitscategories]. Наразі ми проігноруємо відображення таблиць [USERS, ROLES, USERS_ROLE].
Усі об’єкти успадковують батьківський клас [AbstractCoreEntity]:
package spring.jdbc.entities;
public abstract class AbstractCoreEntity {
// характеристики
protected Long id;
protected Long version;
// виробники
public AbstractCoreEntity() {
}
public AbstractCoreEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
public AbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
public void setAbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
// ------------------------------------------------------------
// перевизначення [equals] та [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
return id != null && other.id != null && id.equals(other.id);
}
// методи отримання та встановлення
...
}
- рядок 5: поле [id] буде пов’язане зі стовпцем [ID], первинним ключем таблиць;
- рядок 6: поле [version] буде пов’язане зі стовпцем [VERSIONING] таблиць;
- рядки 8–26: різні конструктори та методи для створення або ініціалізації об’єкта [AbstractCoreEntity];
- рядки 35–47: метод [equals] визначає, що два об’єкти [AbstractCoreEntity] є рівними, якщо вони мають однакове поле [id]. Тут слід пам’ятати, що об’єкти [AbstractCoreEntity] будуть відображеннями рядків таблиць, де [id] є первинним ключем і де, отже, не може бути двох рядків з однаковим [id];
- рядки 30–33: пропозиція [hashCode];
Клас [Produit] буде відображенням одного рядка таблиці [PRODUITS]:
package spring.jdbc.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
// властивості
private String nom;
private Long idCategorie;
private double prix;
private String description;
private Categorie categorie;
// конструктори
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
super(id, version);
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// сигнатура
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// геттери та сеттери
...
}
- рядок 6: клас [Produit] розширює клас [AbstractCoreEntity];
- рядки 8–12: поля [id, version, nom, idCategorie, prix, description] є відображеннями стовпців [ID, VERSIONING, NOM, CATEGORIE_ID, PRIX, DESCRIPTION] таблиці [PRODUITS];
- рядок 12: об’єкт типу [Categorie] з первинним ключем [idCategorie]. Це поле може бути заповнене або ні, залежно від випадку. Якщо воно заповнене, то мова йтиме про продукт у повній версії [LongProduit], інакше — про продукт у скороченій версії [ShortProduit];
- рядок 5: фільтр jSON. Нагадуємо, що проект [mysql-config-jdbc] містить бібліотеку jSON. Необхідність у фільтрі зумовлена тим, що поле [categorie] може бути заповнене або ні. У цьому випадку представлення продукту jSON відрізняється. Щоб врахувати обидва ці випадки, слід налаштувати фільтр [jsonFilterProduit] у рядку 5. Фільтр jSON дозволяє динамічно вказувати поля, які слід виключити з представлення jSON. Як тільки стане відомо, що поле [categorie] не заповнено, його виключать із представлення продукту jSON;
Клас [Categorie] є відображенням рядка таблиці [CATEGORIES]:
package spring.jdbc.entities;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
// властивості
private String nom;
public List<Produit> produits;
// конструктори
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
super(id, version);
this.nom = nom;
this.produits = produits;
}
// сигнатура
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// методи
public void addProduit(Produit produit) {
// додавання товару
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// додаємо товар
produits.add(produit);
// визначаємо його категорію
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// гетери та сеттери
...
}
- рядок 9: клас [Categorie] є розширенням класу [AbstractCoreEntity];
- рядок 12: поля [id, version, nom] є відображенням стовпців [ID, VERSIONING, NOM] таблиці [CATEGORIES];
- рядок 13: поле [produits] містить перелік товарів категорії. Це поле не завжди заповнюється. Якщо воно не заповнене, мова йде про категорію у скороченому варіанті [ShortCategorie], в іншому випадку — про категорію у повному варіанті [LongCategorie];
- рядки 32–44: метод [addProduit] дозволяє додати товар до категорії (рядок 39) та встановити для доданого товару характеристики його категорії (idCategorie та категорія);
- рядок 8: фільтр jSON. Коли бібліотека jSON повинна буде серіалізувати/десеріалізувати об’єкт [Categorie], їй потрібно буде вказати, як обробляти фільтр із назвою [jsonFilterCategorie];
4.7. Інтерфейс Idao<T>
![]() |
![]() |
Інтерфейс [IDao] рівня [DAO] має такий підпис:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.AbstractCoreEntity;
public interface IDao<T extends AbstractCoreEntity> {
// список усіх сутностей T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// окремих сутностей — коротка версія
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// окремих сутностей — розширена версія
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// оновлення декількох сутностей
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// видалення всіх об’єктів
public void deleteAllEntities();
// видалення декількох об’єктів
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
- рядок 7: тут ми маємо інтерфейс [IDao], який параметризується типом T з умовою: цей тип повинен розширювати клас [AbstractCoreEntity] або реалізовувати інтерфейс [AbstractCoreEntity]. Ключове слово [extends] використовується в обох випадках. Тут T буде інстанційовано або типом [Produit], або типом [Categorie]. Дійсно, досить швидко стає зрозуміло, що ми виконуємо однакові операції (вставлення, зміна, видалення, вибір) над типами [Produit] та [Categorie]. Тож логічно об’єднати ці методи в один загальний інтерфейс;
- залежно від випадку терміни [LongEntity] та [ShortEntity] позначають різні ситуації:
- коли T є типом [Produit]:
- [ShortEntity] — це продукт без заповненого поля [Categorie categorie];
- [LongEntity] — це продукт із заповненим полем [Categorie categorie];
- коли T — це тип [Categorie]:
- [ShortEntity] — це категорія, у якій поле [List<Produit> produits] не заповнено;
- [LongEntity] — це товар із заповненим полем [List<Produit> produits];
- коли T є типом [Produit]:
Отже, ми маємо інтерфейс, що містить 19 методів. Більшість методів існують у подвійному варіанті. Візьмемо для прикладу метод [getShortEntitiesById]:
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
- рядки 1 і 3: параметром є список первинних ключів сутностей, для яких потрібно отримати скорочену версію. Цей список представлено у двох різних формах:
- рядок 1: список, що реалізує інтерфейс [Iterable<Long>]. Тип [List<Long>] реалізує цей інтерфейс, але існує ще багато інших. Якби ми вказали [List<Long> ids], цього було б достатньо для наших прикладів, але ми змусили б користувача наших прикладів виконувати перетворення, якщо його параметр не був би саме того типу, що очікується;
- рядок 3: на жаль, тип Long[] не реалізує інтерфейс [Iterable<Long>]. У цьому випадку ми будемо використовувати варіант із рядка 3. Формальний параметр [Long... ids] (3 крапки) може приймати значення як масиву, так і послідовності ідентифікаторів: getShortEntitiesById(id1, id2, ...);
Саме цей інтерфейс IDao<T> буде реалізовано за допомогою такої архітектури:
![]() |
де між рівнем [JPA] (Java Persistence API) та драйвером JDBC SGBD буде вставлений рівень [DAO]. Це дозволить нам мати спільний для обох архітектур рівень тестування. В обох випадках рівень [DAO] матиме два інтерфейси:
- IDao<Продукт> для доступу до таблиці [PRODUITS];
- IDao<Категорія> для доступу до таблиці [CATEGORIES];
4.8. Реалізація інтерфейсу IDao<T>
![]() |
- інтерфейс IDao<Продукт> реалізовано класом [DaoProduit];
- інтерфейс IDao<Категорія> реалізовано класом [DaoCategorie];
Класи [DaoProduit] та [DaoCategorie] обидва успадковують абстрактний клас [AbstractDao] , що має такий вигляд:
package spring.jdbc.dao;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.transaction.annotation.Transactional;
import spring.jdbc.entities.AbstractCoreEntity;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
public abstract class AbstractDao<T extends AbstractCoreEntity> implements IDao<T> {
// вставки
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// локальний
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// правильність аргументу
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// отримання частинами
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// результат
return entities;
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// дійсність аргументу
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// результат
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(String... names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Iterable<Long> ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Long... ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(String... names) {
...
}
@Override
@Transactional
public List<T> saveEntities(Iterable<T> entities) {
...
}
@Override
@Transactional
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities) {
...
}
@Override
public void deleteEntitiesById(Iterable<Long> ids) {
...
}
@Override
public void deleteEntitiesById(Long... ids) {
...
}
@Override
public void deleteEntitiesByName(Iterable<String> names) {
...
}
@Override
public void deleteEntitiesByName(String... names) {
...
}
@Override
public void deleteEntitiesByEntity(Iterable<T> entities) {
...
}
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities) {
...
}
protected void deleteEntitiesByEntity(List<T> entities) {
...
}
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
// приватні методи ----------------------------------------------
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
...
}
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
// захищені методи ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
}
- рядок 15: клас [AbstractDao] є абстрактним (ключове слово abstract). Тому він не може бути інстанційований. Він може бути лише похідним. Цей клас виконує кілька функцій:
- визначення характеру транзакції, в рамках якої виконується кожен метод;
- зробити якомога більше спільного для обох реалізацій інтерфейсів [IDao<Produit>] та [IDao<Categorie>]. В основному це стосується перевірки валідності аргументів. Не приймаються аргументи типу null, а також порожні списки;
- уніфікувати типи параметрів T... params та Iterable<T> params в один: List<T> params;
- делегувати роботу дочірнім класам, щойно вона стає специфічною для одного з двох інтерфейсів;
Завдяки уніфікації параметрів різних методів, здійсненій класом [AbstractDao], дочірні класи [DaoProduit] та [DaoCategorie] матимуть лише 10 методів для реалізації замість 19:
// методи, реалізовані дочірніми класами ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
Розглянемо деякі методи класу [AbstractDao].
Метод [getShortEntitiesById]
Цей метод призначений для отримання скороченої версії сутностей, для яких вказано первинні ключі.
// ін'єкції
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// локальні
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- рядки 2–4: вводиться бін [maxPreparedStatementParameters], визначений у файлі конфігурації [ConfigJdbc], який налаштовує рівень JDBC для конкретного SGBD:
// максимальна кількість параметрів [PreparedStatement]
public final static int MAX_PREPAREDSTATEMENT_PARAMETERS = 10000;
@Bean(name = "maxPreparedStatementParameters")
public int maxPreparedStatementParameters() {
return MAX_PREPAREDSTATEMENT_PARAMETERS;
}
- рядки 1–7: визначають bean [maxPreparedStatementParameters], який встановлюватиме максимальну кількість параметрів, які можна передати типу [PreparedStatement]. Такої потреби не виникало з SGBD та MySQL, які приймали 10 000 параметрів для типу [PreparedStatement]. Під час тестування серверів SGBD та SQL було згенеровано виняток, який вказував, що максимальна кількість параметрів для типу [PreparedStatement] становить 2100. Тому це число стало параметром конфігурації різних SGBD. Отже, його слід вказати у проекті конфігурації [sgbd-config-jdbc] кожного SGBD;
Повернемося до коду методу [getShortEntitiesById]:
// ін'єкції
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// локальний
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- рядок 7: ім’я класу. Використовується як параметр одного з конструкторів класу винятків [DaoException];
- рядок 10: анотація [@Transactional(readOnly = true)] вказує, що метод повинен виконуватися в транзакції тільки для читання. Можна засумніватися в доцільності такої транзакції, оскільки метод виконує лише операції читання, а отже, у разі збою скасовувати нічого немає. Це радить автор бібліотеки [Spring Data] і пояснює, чому. Я дотримався його поради;
Тіло методу виглядає так:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// дійсність аргументу
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
...
}
- рядок 5: правильність параметра [ids] перевіряється за допомогою такого методу:
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
// елементи null?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"), simpleClassName);
}
// порожні елементи?
if (!elements.iterator().hasNext()) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),
simpleClassName);
} else {
return new ArrayList<T>();
}
}
// результат за замовчуванням
return null;
}
- рядок 1: метод [checkNullOrEmptyArgument] є загальним методом, параметризованим типом <T2>. T2 — це тип елементів, що передаються як другий параметр методу. Це може бути [Long, String, AbstractCoreEntity];
- рядок 1: метод [checkNullOrEmptyArgument] приймає два параметри:
- [Iterable<T2> elements]: параметр, що перевіряється;
- [checkEmpty]: приймає значення «true», якщо потрібно перевірити, чи попередній параметр є непорожнім списком;
- рядки 4–6: перевіряється, чи параметр [elements] не є null. Якщо це не так, генерується виняток типу [MyIllegalArgumentException];
- рядки 8–15: якщо список порожній, а потрібно було перевірити, чи він не порожній, генерується виняток типу [MyIllegalArgumentException];
- рядок 13: якщо список порожній і не потрібно перевіряти, чи він не порожній, то повертається порожній список елементів типу T. Інтерфейс [Iterable<T2>] має метод [iterator()], який дозволяє перебирати елементи списку, що реалізують цей інтерфейс. Корисні два методи цього ітератора:
- [itérateur].hasNext() : повертає true, якщо у списку ще є елемент для обробки, інакше — false;
- [iterateur].next(): повертає поточний елемент списку та просуває курсор на один елемент вперед;
- у підсумку,
- якщо аргумент [T2... elements] дорівнює null або є порожнім, генерується виняток типу [MyIllegalArgumentException];
- якщо аргумент [T2... elements] є порожнім списком і це було допустимо, то повертається порожній список елементів типу T;
Аналогічний метод існує, коли аргумент, що перевіряється, має тип [T2... elements]:
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
Повернемося до коду методу [getShortEntitiesById]:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// дійсність аргументу
List<T> entities = checkNullOrEmptyArgument(true, ids);
// отримання частинами
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// результат
return entities;
}
- рядок 7: якщо ми дійшли до цього місця, значить аргумент [Iterable<Long> ids] є дійсним;
- рядки 7–14: пізніше ми побачимо, що метод [getShortEntitiesById] буде реалізовано типом [PreparedStatement], який матиме як параметри список первинних ключів, що підлягають пошуку. Наприклад:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
: ids — це параметр, фактичним значенням якого буде тип List<Long>. Кожен елемент цього списку стане об’єктом параметра ? у типі [PreparedStatement]. Однак ми вже зазначали, що цей тип приймає максимальну кількість параметрів, яка визначається полем [maxPreparedStatementParameters] класу;
- рядок 7: список об’єктів T, який буде повернений методом [getShortEntitiesById]. Цей список буде побудовано частинами по [maxPreparedStatementParameters] елементів;
- рядок 9: на основі аргументу [Iterable<Long> ids] створюється тип [List<Long> listIds]. Клас [Lists] — це клас із бібліотеки Google Guava, який надає численні статичні методи для роботи з колекціями об’єктів. Бібліотека Google Guava була імпортована (pom.xml) проектом Maven [mysql-config-jdbc]:
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- рядок 10: кількість об’єктів T, які потрібно знайти в базі даних;
- рядки 11–13: пошук здійснюється групами по [taille = maxPreparedStatementParameters] елементів;
- рядок 12: обчислення, що запобігає виходу за межі списку [listIds];
- рядок 13: об’єкти T отримуються за допомогою виклику [getShortEntitiesById(listIds.subList(i, limit))]. Цей метод визначено в класі таким чином:
abstract protected List<T> getShortEntitiesById(List<Long> ids);
Отже, саме дочірній клас буде шукати об’єкти T у базі даних:
- [DaoProduit], якщо T має тип [Produit];
- [DaoCategorie], якщо T має тип [Categorie];
Ця робота батьківського класу має подвійну користь:
- сигнатура методу [getShortEntitiesById] у дочірньому класі є унікальною: його аргумент має тип [List<Long> ids];
- дочірньому класу не потрібно вирішувати проблему параметрів [maxPreparedStatementParameters] для [PreparedStatement]. Його батьківський клас вже вирішив це за нього;
- рядок 13: об’єкти, повернуті дочірнім класом, додаються до списку об’єктів, який буде повернений батьківським класом (рядок 16);
Тепер розглянемо реалізацію іншого методу [getShortEntitiesById] цього класу:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// правильність аргументу
List<T> entities = checkNullOrEmptyArgument(true, ids);
// результат
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
- рядок 3: тип аргументу змінився: Long... ids;
- рядок 5: перевіряється допустимість цього аргументу;
- рядок 7: викликається метод [getShortEntitiesById], який ми щойно описали. І тут знову використовується клас [Lists] із бібліотеки [Google Guava]. Зверніть увагу, що ми змушені виконати явне приведення типу до типу [Iterable<Long>], щоб допомогти компілятору вибрати правильний метод, оскільки метод [getShortEntitiesById] має три сигнатури в класі:
- List<T> getShortEntitiesById(Long... ids);
- List<T> getShortEntitiesById(Iterable<Long> ids);
- List<T> getShortEntitiesById(List<Long> ids), який є абстрактним і реалізується дочірнім класом;
Ми не будемо детальніше зупинятися на абстрактному класі [AbstractDao], який є батьківським для класів [DaoProduit] та [DaoCategorie]. Зазначимо лише, що іноді доцільно винести спільні для кількох класів функції в батьківський клас, який може бути як абстрактним, так і не абстрактним. Після цього дочірнім класам залишається реалізувати лише такі методи:
// методи, реалізовані дочірніми класами ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
Код у розділі 4.8 демонструє різні типи транзакцій, що використовуються для кожного методу. Звернемо увагу на кілька моментів:
- методи, що зчитують базу даних, позначені анотацією [@Transactional(readOnly = true)];
- методи, що змінюють базу даних, позначені анотацією [@Transactional];
- методи з анотацією [delete] не мають анотації і, отже, не виконуються в рамках транзакції. Ідея полягає в тому, що якщо видалення не вдалося, користувач, ймовірно, не захоче скасовувати всі попередні успішні операції;
4.9. Клас [DaoCategorie]
![]() |
![]() |
Клас [DaoCategorie] реалізує інтерфейс [IDao<Categorie>], який забезпечуєдоступ до даних таблиці [CATEGORIES] у базі даних MySQL [dbproduitscategories]. Її структура така:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
// константи
// ін'єкції
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
@Autowired
private IDao<Produit> daoProduit;
@Override
public List<Categorie> getAllShortEntities() {
...
}
@Override
public List<Categorie> getAllLongEntities() {
...
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
...
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
...
}
// --------------------- маппери
class ShortCategorieMapper implements RowMapper<Categorie> {
....
}
class LongCategorieMapper implements RowMapper<Categorie> {
....
}
- рядок 28: клас [DaoCategorie] є компонентом Spring і, як такий, може бути ін'єктований в інші компоненти Spring;
- рядок 29: клас [DaoCategorie] успадковує абстрактний клас [AbstractDao<Categorie>], що робить його реалізацією інтерфейсу [IDao<Categorie>];
- рядки 34–37: ін’єкція бінів, визначених у класі [AppConfig], описаному в розділі 4.4;
- рядки 38–39: ін'єкція посилання на клас [DaoProduit], який реалізує інтерфейс [IDao<Produit>], що керує доступом до даних таблиці [PRODUITS];
- рядки 41–89: реалізація інтерфейсу [IDao<Categorie>];
- рядки 95–101: два внутрішні класи, що реалізують інтерфейс [RowMapper<T>];
Розглянемо методи по черзі.
4.9.1. Метод [getAllShortEntities]
Метод [getAllShortEntities] повертає всі категорії з таблиці [CATEGORIES] у скороченому вигляді:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
Усі методи базуються на об’єкті [namedParameterJdbcTemplate], визначеному у файлі конфігурації Spring та наданому бібліотекою Spring JDBC. Цей об’єкт має численні методи. Вище використано такий:
![]()
- [sql] — це команда SQL, яку потрібно виконати;
- [rowMapper] — це екземпляр наступного інтерфейсу [RowMapper<T>]:

Ідея полягає в наступному:
- метод [namedParameterJdbcTemplate].query(String sql, RowMapper<T> rowMapper) виконує команду SQL типу [Select]. Вона обробляє можливі винятки, а також відкриває та закриває з’єднання з SGBD. Єдине, чого вона не може зробити, — цеінкапсулювати елементи [ResultSet] з об’єктів, які вона отримує, у тип [Categorie], оскільки вона не знає зв’язку, що існує між полями типу [Categorie] та стовпцями [Resultset]. Пізніше ми побачимо, що цей зв’язок створюється за допомогою технології JPA, що зробить інкапсуляцію елементів [ResultSet] в екземпляри типу T автоматичною. Наразі другим параметром методу [query] є екземпляр інтерфейсу [RowMapper<T>], здатний здійснити цю інкапсуляцію;
Повернемося до коду:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
Порядок SQL [ConfigJdbc.SELECT_ALLSHORTCATEGORIES] є таким:
public final static String SELECT_ALLSHORTCATEGORIES = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c";
Запит вимагає стовпців [ID, VERSIONING, NOM] з елементів таблиці [CATEGORIES]. Ми будемо систематично використовувати синтаксис:
SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...
Важливим є іменування стовпців, отриманих за допомогою SELECT з атрибутом [as nom_colonne]. Це єдиний спосіб забезпечити сумісність між SGBD, оскільки всі вони мають власний спосіб іменування стовпців, отриманих за допомогою SELECT, у якому стовпці різних таблиць мають однакову назву (наприклад, ID, NOM або VERSIONING у нашому випадку). Таким чином, ми усуваємо цю неоднозначність, самостійно вказуючи імена, які повинні мати ці стовпці.
Внутрішній клас [ShortCategorieMapper] має такий вигляд:
class ShortCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSIONING"), rs.getString("c_NOM"), null);
}
}
- рядок 1: клас [ShortCategorieMapper] реалізує інтерфейс [RowMapper<Categorie>] і, відповідно, повинен реалізувати метод [mapRow] з рядків 4–5, роль якого полягає в інкапсуляції рядка з [ResultSet rs], згенерованого командою [SELECT], у тип [Categorie];
- рядок 5: це інкапсулювання виконано. Слід зауважити, що ім’я, яке використовується методами [rs.getType(nom)], є тим самим ім’ям, що використовується в атрибутах [as nom] стовпців SELECT;
Таким чином, ми отримали список категорій у їх скороченому вигляді без обробки винятків та без підключення. У цьому полягає перевага бібліотеки Spring JDBC, яка обробляє все, що можна факторизувати в управлінні елементами таблиці, і залишає розробнику те, що факторизувати неможливо.
4.9.2. Метод [getAllLongEntities]
Метод [getAllLongEntities] повертає всі категорії з таблиці [CATEGORIES] у повній версії:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
Порядок SQL [ConfigJdbc.SELECT_ALLLONGCATEGORIES] є таким:
public final static String SELECT_ALLLONGCATEGORIES = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID";
Завдання полягає в тому, щоб зіставити категорії з відповідними товарами. Це досягається шляхом з'єднання таблиці [CATEGORIES] з таблицею [PRODUITS] за допомогою зовнішнього ключа [CATEGORIE_ID], якийз'єднує таблицю [PRODUITS] з таблицею [CATEGORIES]. Синтаксис [FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID] дозволяє також повернути категорії, які не мають пов’язаних з ними товарів. У цьому випадку запит SELECT повертає категорію та товар із усіма їхніми стовпцями до таблиці NULL.
Клас [LongCategorieMapper] має такий вигляд:
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// випадок категорії без товарів
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
- рядок 4: метод [mapRow] повинен повернути об’єкт [Categorie] із заповненим полем [produits], на основі рядка з [ResultSet], отриманого з попереднього запиту SELECT;
У підсумку операція:
[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]
поверне список такого типу:
де кожна категорія [ci] матиме поле [produits], яке буде списком товарів, що містить лише один елемент [produitsij]. Однак нам потрібен такий список:
де кожна категорія [ci] матиме поле [produits], яке буде списком товарів [produiti1, produiti2, ...]. Це досягається шляхом передачі отриманого списку категорій до приватного методу [filterCategories]:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
Метод [filterCategories] має такий вигляд:
private List<Categorie> filterCategories(List<Categorie> categories) {
if (categories.size() == 0) {
return categories;
}
// категорії, що підлягають відображенню
List<Categorie> cats = new ArrayList<Categorie>();
// проглядаємо список отриманих категорій
for (Categorie categorie : categories) {
boolean trouve = false;
for (Categorie cat : cats) {
if (categorie.equals(cat)) {
cat.addProduit(categorie.getProduits().get(0));
trouve = true;
break;
}
}
// знайдено?
if (!trouve) {
cats.add(categorie);
}
}
// результат
return cats;
}
- рядок 1: [List<Categorie> categories] — це список категорій, які потрібно відфільтрувати (або згрупувати);
- рядок 6: список категорій, які потрібно повернути викликувачу;
- рядки 8–21: обробляється кожна категорія зі списку для фільтрації;
- рядки 10–16: перевіряється, чи поточна категорія [categorie] вже присутня у списку категорій [cats], який потрібно сформувати (нагадаємо, що дві категорії вважаються рівними, якщо вони мають однаковий первинний ключ, див. параграф 4.6);
- рядки 11–14: якщо це вже так, то товар, що міститься в [categorie], додається до списку товарів [cat];
- рядки 18–20: якщо поточна категорія [categorie] ще не присутня у списку категорій [cats], що підлягають створенню, то її додають туди разом із її списком товарів, який містить єдиний елемент;
Розглянемо випадок, коли запит SQL Select повертає категорії без пов’язаних з ними товарів. Яка суть повертає клас [LongCategorieMapper]?
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// випадок категорії без товарів
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
У випадку, коли запит SQL Select повернув категорію без товарів, стовпці товару, повернутого разом із категорією, містять значення SQL NULL. Цей випадок обробляється у рядках 7–9:
- рядок 7: отримуємо первинний ключ товару у вигляді довгого цілого числа;
- рядок 9: перевіряється, чи зчитане значення дорівнювало SQL NULL (rs.wasNull). Якщо це не так, додаємо товар до списку в рядку 6, інакше нічого не додається, і список товарів залишається порожнім.
Слід зауважити, що в будь-якому випадку повертається категорія з полем [produits], яке не є null.
4.9.3. Метод [getShortEntitiesById]
Метод [getShortEntitiesById] аналогічний методу [getAllShortEntities], за винятком того, що він повертає лише об’єкти, первинні ключі яких вказані у списку:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
- у рядку 4 підпис використовуваного методу [query] має такий вигляд:

Перший параметр — це налаштована команда SQL [Select]. Другий — це словник, що пов’язує кожен із параметрів із певним значенням. Третій — це екземпляр класу, який здійснює інкапсуляцію рядка [ResultSet], отриманого в результаті виконання [Select], в об’єкт типу T;
- рядок 4: налаштований запит SQL [Select] має такий вигляд:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
Ця команда витягує з таблиці [CATEGORIES] категорії, первинні ключі яких містяться у списку: ids.
- рядок 5: другим параметром методу [query] тут є словник, що пов’язує ключ «ids» (1-й параметр) зі списком [ids], переданим у рядку 1 як параметр методу [getShortEntitiesById]. Клас [Collections] належить до бібліотеки [Google Guava], про яку ми вже згадували. [Collections.singleMap] повертає словник, що містить один елемент;
- рядок 5: клас, відповідальний за інкапсуляцію рядка з [ResultSet], отриманого в результаті роботи [Select], в об’єкт типу [Categorie], — це вже розглянутий клас [ShortCategorieMapper];
Саме тут, як правило, вступає в дію бін [maxPreparedStatementParameters]. Дійсно, параметр [:ids] замовлення SQL, який представляє список первинних ключів, може містити від 1 до кількох тисяч параметрів. Існує обмеження на цю кількість, яке залежить від кожного SGBD. Для MySQL вдалося передати 10 000 параметрів без помилок, і ми не тестували значення, що перевищують цю межу. Для сервера SQL офіційне обмеження становить 2100. Для Firebird навіть 1000 було забагато. Ми зменшили кількість до 100. Загалом, ми не тестували максимальне обмеження цієї кількості для різних SGBD.
4.9.4. Метод [getLongEntitiesById]
Метод [getLongEntitiesById] аналогічний методу [getShortEntitiesById], за винятком того, що він скорочує повні назви категорій:
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
У рядку 4 запит SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYID] має такий вигляд:
public final static String SELECT_LONGCATEGORIE_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.ID in (:ids)";
4.9.5. Метод [getShortEntitiesByName]
Метод [getShortEntitiesByName] аналогічний методу [getShortEntitiesById], за винятком того, що категорії шукаються за їхніми назвами, а не за первинними ключами:
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
У рядку 4 порядок SQL [ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME] є таким:
public final static String SELECT_SHORTCATEGORIE_BYNAME = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.NOM in (:noms)";
4.9.6. Метод [getLongEntitiesByName]
Метод [getLongEntitiesByName] аналогічний методу [getShortEntitiesByName], за винятком того, що категорії шукаються у їхніх повних версіях:
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(215, e, simpleClassName);
}
}
У рядку 4 порядок SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME] є таким:
public final static String SELECT_LONGCATEGORIE_BYNAME = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.NOM in(:noms)";
4.9.7. Метод [deleteAllEntities]
Метод [deleteAllEntities] видаляє всі категорії з таблиці [CATEGORIES]:
@Override
public void deleteAllEntities() {
try {
// видаляються всі категорії та, відповідно, всі товари
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_ALLCATEGORIES, (Map<String, Object>) null);
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
- рядок 4: метод [namedParameterJdbcTemplate.update], що використовується, має такий підпис:
![]()
Перший параметр — це налаштована команда оновлення SQL (INSERT, UPDATE, DELETE). Другий параметр — це словник, що пов’язує значення з різними параметрами наказу SQL. Метод повертає кількість рядків, оновлених наказом SQL.
- рядок 4: наказ SQL [ConfigJdbc.DELETE_ALLCATEGORIES] має такий вигляд:
public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";
Отже, це не налаштований запит. Саме тому другий параметр методу [update] має значення null.
4.9.8. Метод [deleteAllEntitiesById]
Метод [deleteAllEntitiesById] видаляє категорії з таблиці [CATEGORIES], первинні ключі якої передаються:
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYID, Collections.singletonMap("ids", ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
У рядку 4 порядок SQL [ConfigJdbc.DELETE_CATEGORIESBYID] є таким:
public final static String DELETE_CATEGORIESBYID = "DELETE FROM CATEGORIES WHERE ID in (:ids)";
4.9.9. Метод [deleteAllEntitiesByName]
Метод [deleteAllEntitiesByName] видаляє категорії з таблиці [CATEGORIES], імена яких передаються:
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYNAME, Collections.singletonMap("noms", names));
} catch (Exception e) {
throw new DaoException(225, e, simpleClassName);
}
}
У рядку 4 порядок SQL [ConfigJdbc.DELETE_CATEGORIESBYNAME] є таким:
public final static String DELETE_CATEGORIESBYNAME = "DELETE FROM CATEGORIES WHERE NOM in (:noms)";
4.9.10. Метод [saveEntities]
4.9.10.1. Код
Сигнатура цього методу така:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
Метод отримує як параметр список категорій. Він виконує над ними такі операції:
- якщо категорія має первинний ключ null, виконується операція SQL INSERT; в іншому випадку виконується операція SQL UPDATE;
- ця операція повторюється для кожного товару категорії;
Метод повертає список збережених або оновлених категорій. Повернутий список є точним відображенням категорій та товарів, що містяться в таблицях, з урахуванням версій: вони, насправді, не змінюються в оновлених об’єктах, хоча їхні версії були збільшені в базі даних.
Це, безперечно, найскладніший метод. Його код такий:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// --------------------------------------------- категорії
List<Categorie> insertCategories = new ArrayList<Categorie>();
List<Categorie> updateCategories = new ArrayList<Categorie>();
// скануємо категорії
for (Categorie categorie : entities) {
// додати чи оновити?
if (categorie.getId() == null) {
insertCategories.add(categorie);
} else {
updateCategories.add(categorie);
}
}
// додавання категорій
if (insertCategories.size() > 0) {
insertCategories(insertCategories);
}
// оновлення категорій
if (updateCategories.size() > 0) {
updateCategories(updateCategories);
}
// --------------------------------------------- товари
// оновлюються товари в категоріях
List<Produit> allProduits = new ArrayList<Produit>();
for (Categorie categorie : entities) {
List<Produit> produits = categorie.getProduits();
Long idCategorie = categorie.getId();
if (produits != null) {
// додаємо до списку всіх товарів
allProduits.addAll(produits);
// товари скануються по одному, щоб прив’язати їх до відповідної категорії
for (Produit produit : produits) {
// товар прив’язується до своєї категорії
produit.setIdCategorie(idCategorie);
produit.setCategorie(categorie);
}
}
}
// вставлення/оновлення товарів
daoProduit.saveEntities(allProduits);
// результат
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(207, e, simpleClassName);
}
}
- рядки 5–23: вставлення або оновлення категорій;
- рядки 26–43: вставлення або оновлення товарів;
- рядки 35–39: цей код пов’язує кожен товар із його категорією. На попередньому етапі додавання категорій їм було присвоєно первинний ключ, який потрібно вказати у полі [idCategorie] товару (рядок 37). Крім того, рядки 37–38 дають змогу виправити ситуації, коли користувач неправильно пов’язав кожен товар із його категорією. Щоб це зв’язування було правильним, слід використовувати метод [Categorie] .add(Продукт p), але ніщо не заважає користувачеві додати продукт безпосередньо до списку продуктів категорії, оминаючи цей метод, ризикуючи тим самим, що поля [idCategorie, categorie] продукту p будуть заповнені неправильно;
- рядок 43: передаємо екземпляру інтерфейсу [IDao<Produit>] завдання збереження / оновлення товарів. Нагадаємо, що цей екземпляр був введений у клас [DaoCategorie]:
@Autowired
private IDao<Produit> daoProduit;
4.9.10.2. Вставлення категорій
Категорії вставляються в таблицю [CATEGORIES] за допомогою наступного приватного методу [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// категорії, які потрібно додати
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// зберігаємо первинний ключ
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// все це OK — присвоюємо первинні ключі збереженим категоріям
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// результат
return categories;
}
- рядок 6: використовується bean [simpleJdbcInsertCategorie], який вводиться в клас за допомогою таких рядків:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
Цей bean визначено у класі [AppConfig] проекту наступним чином:
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_CATEGORIES_ID)
.usingColumns(ConfigJdbc.TAB_CATEGORIES_NOM);
}
- у рядку 5 клас [SimpleJdbcInsert] є класом бібліотеки Spring JDBC (рядок 1):
- параметр конструктора [SimpleJdbcInsert] — це джерело даних, з яким виконуються операції;
- клаузула [withTableName] дозволяє вказати таблицю, в яку потрібно вставити елемент, у даному випадку таблицю [CATEGORIES];
- клаузула [usingGeneratedKeyColumns] дозволяє вказати стовпець автоматично згенерованого первинного ключа, в даному випадку стовпець [ID];
- клаузула [usingColumns] дозволяє обмежити вставку певними стовпцями. Тут виключаються стовпець [ID], який автоматично генерується за допомогою SGBD, та стовпець [VERSIONING], яке має значення за замовчуванням 1;
Повернемося до коду методу [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// категорії, які потрібно додати
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// запам'ятовуємо первинний ключ
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// все це OK — присвоюємо первинні ключі збереженим категоріям
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// результат
return categories;
}
- рядок 6: використовується метод [simpleJdbcInsertCategorie.executeAndReturnKey]:
![]()
Метод очікує як параметр словник, що встановлює зв’язки між стовпцями таблиці та значеннями, які потрібно в них ввести. Він повертає як результат первинний ключ у вигляді типу [Number]. Метод [Number.longValue()] дозволяє отримати первинний ключ у вигляді типу [Long].
Метод [getMapForCategorie] є наступним приватним методом:
private Map<String, ?> getMapForCategorie(Categorie categorie) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_CATEGORIES_NOM, categorie.getNom());
return map;
}
Ключі словника — це назви стовпців, які потрібно заповнити ([NOM]), а значення словника — це значення, які потрібно ввести в ці стовпці.
- рядок 8 [insertCategories]: отриманий первинний ключ зберігається у словнику. Ми почекаємо, поки не переконаємося, що всі сутності були вставлені, перш ніж присвоювати їм первинні ключі. Адже у разі виникнення винятку всі вставки будуть скасовані, і ми хочемо, щоб тоді сутності [categories] із рядка 1 також залишилися без змін;
- рядки 14–17: тепер, коли ми впевнені, що все пройшло успішно, присвоюємо згенеровані первинні ключі категоріям;
- рядок 19: повертаємо список категорій із їхніми первинними ключами;
4.9.10.3. Оновлення категорій
Категорії оновлюються за допомогою такого приватного методу [updateCategories]:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// оновлення категорії в базі даних
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// чи вдалося?
Long idCategorie = null;
if (nbLignes == 0) {
// не вдалося — шукаємо причину
// шукаємо категорію в базі даних
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// категорія не існує
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// версія була неправильною
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
Оновлення категорії C1 у базі даних категорією C2, що знаходиться в пам'яті, дозволено лише в тому випадку, якщо категорії C1 та C2 мають однакову версію. Цей номер версії слугує для запобігання одночасному оновленню об’єкта двома різними користувачами: два користувачі U1 та U2 читають об’єкт E з номером версії, що дорівнює V1. U1 змінює E та зберігає цю зміну в базі даних: номер версії тоді змінюється на V1+1. U2, у свою чергу, змінює E та зберігає цю зміну в базі даних: він отримає виняток, оскільки має версію (V1), відмінну від тієї, що є в базі даних (V1+1).
- рядки 2–29: блок try має два блоки catch:
- перший, у рядку 25, призначений для пропускання можливого винятку типу [DaoException], що генерується кодом у рядку 13;
- другий, у рядку 27, призначений для обробки інших типів винятків;
- рядок 3: скануються всі категорії, які потрібно оновити;
- рядок 4: оновлюємо поточну категорію за допомогою методу [namedParameterJdbcTemplate.update]:

- проаналізуємо інструкцію:
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES, new BeanPropertySqlParameterSource(categorie));
Порядок SQL [ConfigJdbc.UPDATE_CATEGORIES] такий:
public final static String UPDATE_CATEGORIES = "UPDATE CATEGORIES SET VERSIONING=VERSIONING+1, NOM=:nom WHERE ID=:id AND VERSIONING=:version";
Команда має три параметри (:id, :version, :nom), значення яких містяться у полях з такими самими іменами у зміненому об’єкті [categorie]. Цю особливість використовують, передаючи як другий параметр [new BeanPropertySqlParameterSource(categorie)], що означає: «значення параметрів містяться у полях з такими самими іменами цього Java-біна»;
Результатом цієї операції, якщо вона проходить нормально, є кількість змінених рядків, тобто 0 або 1.
Повернемося до розглянутого коду:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// оновлення категорії в базі даних
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// чи вдалося?
Long idCategorie = null;
if (nbLignes == 0) {
// не вдалося — шукаємо причину
// шукаємо категорію в базі даних
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// категорія не існує
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// версія була неправильною
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
- рядок 9: перевіряється, чи зміна відбулася успішно;
- рядок 10: зміна не відбулася. Оскільки умова [WHERE] охоплює стовпці [ID] та [VERSIONING], шукаємо стовпець, через який не вдалося виконати [WHERE];
- рядки 12–18: перевіряється, чи ключ категорії [id] є в базі даних. Якщо це не так, запускається [RuntimeException] із відповідним повідомленням про помилку;
- рядки 19–22: обробляють випадок, коли неправильною була саме версія;
4.10. Клас [DaoProduit]
![]() |
![]() |
Клас [DaoProduit] реалізує інтерфейс [IDao<Produit>], який забезпечуєдоступ до даних таблиці [PRODUITS] у базі даних MySQL [dbproduitscategories]. Її скелет має такий вигляд:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
// вставки
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
@Override
public List<Produit> getAllShortEntities() {
...
}
@Override
public List<Produit> getAllLongEntities() {
....
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
....
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
....
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
}
// --------------------- маппери
class ShortProduitMapper implements RowMapper<Produit> {
...
}
class LongProduitMapper implements RowMapper<Produit> {
...
}
Код дуже схожий на код класу [DaoCategorie]. Ми розглянемо лише кілька методів.
4.10.1. Метод [getShortEntitiesById]
Метод [getShortEntitiesById] повертає скорочену версію продуктів, для яких передаються первинні ключі:
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTPRODUIT_BYID,
Collections.singletonMap("ids", ids), new ShortProduitMapper());
} catch (Exception e) {
throw new DaoException(109, e, simpleClassName);
}
}
- рядок 4: порядок SQL Select [ConfigJdbc.SELECT_SHORTPRODUIT_BYID] такий:
public final static String SELECT_SHORTPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSIONING, p.NOM as p_NOM, p.CATEGORIE_ID as p_CATEGORIE_ID, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION FROM PRODUITS p WHERE p.ID in (:ids)";
- рядок 4: клас [ShortProduitMapper], який відповідає за інкапсуляцію [ResultSet] у список продуктів, має такий вигляд:
class ShortProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSIONING"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), null);
}
}
4.10.2. Метод [getLongEntitiesByName]
Метод [getShortEntitiesById] повертає розгорнуту версію продуктів, імена яких передаються йому:
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
- рядок 4: порядок SQL Select [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] є таким:
public final static String SELECT_LONGPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p, CATEGORIES c WHERE p.ID in (:ids) AND p.CATEGORIE_ID=c.ID";
- рядок 4: клас [LongProduitMapper], який відповідає за інкапсуляцію елементів [ResultSet] у товари (повна версія), має такий вигляд:
class LongProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSION"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null));
}
}
4.10.3. Метод [saveEntities]
Метод [saveEntities] використовується як для вставлення нових товарів (id==null), так і для оновлення існуючих товарів (id!=null):
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
try {
// товари для додавання
List<Produit> insertProduits = new ArrayList<Produit>();
// товари, які потрібно оновити
List<Produit> updateproduits = new ArrayList<Produit>();
// сканування списку отриманих об’єктів
for (Produit produit : entities) {
Long id = produit.getId();
if (id == null) {
insertProduits.add(produit);
} else {
updateproduits.add(produit);
}
}
// додавання
insertProduits(insertProduits);
// зміни
updateProduits(updateproduits);
// результат
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
У рядку 18 товари додаються за допомогою такого приватного методу [insertProduits]:
private List<Produit> insertProduits(List<Produit> produits) {
Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
try {
// товари, які потрібно додати
for (Produit produit : produits) {
Number newId = simpleJdbcInsertProduit.executeAndReturnKey(getMapForProduit(produit));
// фіксується первинний ключ
mapProduits.put(newId.longValue(), produit);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// все є OK — первинні ключі присвоюються збереженим товарам
for (Long id : mapProduits.keySet()) {
Produit produit = mapProduits.get(id);
produit.setId(id);
}
// результат
return produits;
}
private Map<String, ?> getMapForProduit(Produit produit) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_PRODUITS_NOM, produit.getNom());
map.put(ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, produit.getIdCategorie());
map.put(ConfigJdbc.TAB_PRODUITS_PRIX, produit.getPrix());
map.put(ConfigJdbc.TAB_PRODUITS_DESCRIPTION, produit.getDescription());
return map;
}
Цей метод аналогічний методу [insertCategories], розглянутому в розділі 4.9.10.3.
- рядок 4: використовується bean [simpleJdbcInsertProduit], який було введено в клас:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
Цей bean було визначено в класі [AppConfig], який налаштовує проект:
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource)
.withTableName(ConfigJdbc.TAB_PRODUITS)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_PRODUITS_ID)
.usingColumns(ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION,ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID);
}
- рядки 3–6: bean [simpleJdbcInsertProduit]
- пов'язаний із джерелом даних бази [dbproduitscategories] (рядок 3) та з таблицею [ConfigJdbc.TAB_PRODUITS] цього джерела (рядок 4);
- первинний ключ цієї таблиці генерується у стовпці [ConfigJdbc.TAB_PRODUITS_ID] (рядок 5);
- значення присвоюються лише стовпцям [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] (рядок 6);
Метод [updateProduits], який оновлює продукти (рядок 20 таблиці [saveEntities]), має такий вигляд:
private void updateProduits(List<Produit> updateProduits) {
try {
// скануємо товари
for (Produit produit : updateProduits) {
// оновлення продукту в базі даних
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_PRODUITS,
new BeanPropertySqlParameterSource(produit));
// чи вдалося?
Long idProduit = null;
if (nbLignes == 0) {
// не вдалося — шукаємо причину
// шукаємо товар у базі даних
idProduit = produit.getId();
List<Produit> produitsInBd = getShortEntitiesById(idProduit);
if (produitsInBd.size() == 0) {
// товар не існує
throw new RuntimeException(String.format("Erreur de mise à jour. Le produit de clé [%s] n'existe pas",
idProduit));
} else {
// версія була неправильною
throw new RuntimeException(String.format(
"Erreur de mise à jour. Le produit de clé [%s] n'a pas la bonne version", idProduit));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
Вона аналогічна тій, що оновлює категорії (див. параграф 4.9.10.3). У рядку 23 наказ SQL [ConfigJdbc.UPDATE_PRODUITS], виконаний для оновлення товарів, має такий вигляд:
public final static String UPDATE_PRODUITS = "UPDATE PRODUITS SET VERSIONING=VERSIONING+1, NOM=:nom, PRIX=:prix, CATEGORIE_ID=:idCategorie, DESCRIPTION=:description WHERE ID=:id AND VERSIONING=:version";
Імена параметрів [:id,:version,:nom,:prix,:idCategorie,:description] також є іменами полів класу [Produit], що дозволяє використовувати інструкцію з рядків 6–7 для оновлення поточного продукту.
4.11. Рівень тестування
![]() |
![]() |
Тестовий рівень складається з трьох тестових класів:
- [JUnitTestCheckArguments]: тести цього класу викликають різні методи шару [DAO] із недопустимими аргументами та перевіряють, чи вони реагують належним чином;
- [JUnitTestDao]: тести цього класу викликають різні методи шару [DAO] і перевіряють, чи вони виконують очікувані дії;
- [JUnitTestPushTheLimits] призначений не для тестування шару [DAO], а для вимірювання його продуктивності;
Цей рівень тестування відіграє важливу роль у цьому документі. Він є спільним для всіх реалізацій інтерфейсу [IDao<T>]. Їх є шість на SGBD (1 реалізація JDBC, 3 реалізації JPA, 1 реалізація Spring MVC, 1 реалізація Spring MVC із захистом), отже, загалом 36 для шести протестованих SGBD. Рівень тестування дозволяє нам перевірити, чи всі реалізації реагують однаково.
4.11.1. Тест [JUnitTestCheckArguments]
Тестовий клас [JUnitTestCheckArguments] містить 48 методів, які перевіряють реакцію методів шару [DAO] при їх виклику з неправильними аргументами. Його структура така:
package spring.jdbc.tests;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestCheckArguments {
// шар [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// локальні дані
private Iterable<String> names1 = null;
private Iterable<String> names2 = Lists.newArrayList(new String[0]);
private String[] names3 = null;
private String[] names4 = new String[0];
private Iterable<Long> ids1 = null;
private Iterable<Long> ids2 = Lists.newArrayList(new Long[0]);
private Long[] ids3 = null;
private Long[] ids4 = new Long[0];
private Iterable<Categorie> categories1 = null;
private Iterable<Categorie> categories2 = Lists.newArrayList(new Categorie[0]);
private Categorie[] categories3 = null;
private Categorie[] categories4 = new Categorie[0];
private Iterable<Produit> produits1 = null;
private Iterable<Produit> produits2 = Lists.newArrayList(new Produit[0]);
private Produit[] produits3 = null;
private Produit[] produits4 = new Produit[0];
...
}
- рядок 19: тест JUnit буде виконано в інтеграції з фреймворком Spring;
- рядок 18: перед тестуванням будуть інстанційовані біни, визначені в класі [AppConfig] проекту;
- рядки 23–26: ін'єкція екземпляра кожного з двох інтерфейсів шару [DAO];
- рядки 29–44: неправильні параметри виклику методів шару [DAO];
- рядок 29: покажчик null типу [Iterable<String>] як список імен;
- рядок 30: порожній список типу [Iterable<String>] як список імен;
- рядок 29: покажчик null типу String[] як масив імен;
- рядок 30: порожній масив типу String[] як масив імен;
- ...
З полем [names1], наприклад, проводимо такий тест:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName1() {
daoProduit.getShortEntitiesByName(names1);
}
- рядок 1: вказується, що тест [getShortProduitsByName1] повинен викликати виняток типу [MyIllegalArgumentException]
З полем [names2], наприклад, проводимо такий тест:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName2() {
daoCategorie.getLongEntitiesByName(names2);
}
За допомогою поля [names3], наприклад, виконується такий тест:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName3() {
daoCategorie.getLongEntitiesByName(names3);
}
З полем [names4], наприклад, проводимо такий тест:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName4() {
daoProduit.getShortEntitiesByName(names4);
}
Таким чином проводиться 48 тестів для перевірки всіх можливих випадків. Виконується конфігурація виконання з назвою [spring-jdbc-generic-04-JUnitTestCheckArguments] [1]. Отримано такий результат: [2]:
![]() |
4.11.2. Тест [JUnitTestDao]
Тест [JUnitTestDao] викликає методи шару [DAO] з правильними аргументами та перевіряє, чи методи виконують очікувані дії. Загалом існує 74 тести, які перевіряють операції вставки, вибору, оновлення та видалення об’єктів, категорій або товарів. Загалом налічується понад 1000 рядків коду. Ми розглянемо лише деякі з цих методів.
4.11.2.1. Структура тесту
Клас [JUnitTestDao] має таку структуру:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {
// контекст Spring
@Autowired
private ApplicationContext context;
// шар [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// константи
private final int NB_PRODUITS = 5;
private final int NB_CATEGORIES = 2;
// локальні
// локальні
private Map<Long, Categorie> mapCategories = new HashMap<Long, Categorie>();
private Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
@Before
public void clean() {
// очищення бази даних перед кожним тестом
log("Vidage de la base de données", 1);
// очищається таблиця [CATEGORIES], а також, за каскадним принципом, таблиця [PRODUITS]
daoCategorie.deleteAllEntities();
// очищаються словники
for (Long id : mapCategories.keySet()) {
mapCategories.remove(id);
}
for (Long id : mapProduits.keySet()) {
mapProduits.remove(id);
}
}
...
}
- рядки 27–28: як і в тесті [JUnitTestCheckArguments], ми маємо справу з тестом, інтегрованим із Spring та налаштованим класом [AppConfig] цього проєкту;
- рядки 32–33: ін'єкція контексту Spring, що надає доступ до всіх його бінів;
- рядки 35–36: ін'єкція екземпляра інтерфейсу [IDao<Produit>], який тестується класом;
- рядки 37–38: ін’єкція екземпляра інтерфейсу [IDao<Categorie>], який тестується класом;
- рядки 41–42: коли тесту будуть потрібні дані з бази, буде створено базу категорій [NB_CATEGORIES], кожна з яких міститиме продукти [NB_PRODUITS]. Таким чином, у таблиці [CATEGORIES] буде [NB_CATEGORIES] категорій, а у таблиці [PRODUITS] — [NB_CATEGORIES] * [NB_PRODUITS] товарів;
- рядки 46–47: два словники, у яких будуть зберігатися товари та категорії;
- рядки 49–62: метод [clean] виконується перед кожним тестом (рядок 49). У рядку 54 очищується таблиця [CATEGORIES]. Тут слід пам’ятати, що таблиця [PRODUITS] має первинний ключ [CATEGORIE_ID] у стовпці ID таблиці [CATEGORIES], який визначено таким чином:
![]() |
- (продовження)
- у [1-3] — чужий ключ [CATEGORIE_ID] з таблиці [PRODUITS]. Він вказує на стовпець [ID] таблиці [CATEGORIES] [4-5];
- коли категорія видаляється, видаляються також усі пов’язані з нею товари [6]. Цей момент важливо відзначити, оскільки він використовується при побудові шару [DAO], що використовує базу [dbproduitscategories];
Отже, коли видаляється вміст таблиці [CATEGORIES], вміст таблиці [PRODUITS] також буде видалено.
- рядки 56–58: очищаємо словник категорій;
- рядки 59–61: те саме робимо зі словником товарів;
Слід пам’ятати, що перед кожним тестом у базі даних є порожні таблиці, а в пам’яті — порожні словники.
4.11.2.2. Метод [verifyClean]
Метод [verifyClean] перевіряє, чи після виконання методу [clean] таблиці є порожніми:
@Test
public void verifyClean() {
log("verifyClean", 1);
List<Categorie> categories = daoCategorie.getAllShortEntities();
Assert.assertEquals(0, categories.size());
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.2.3. Метод [fillDataBase]
Цей метод перевіряє правильність заповнення бази даних тестовими даними:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// заповнення бази даних та словників
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// виведення на екран
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// деякі перевірки
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// словники мають бути вичерпані
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
Цей тест використовує кілька приватних методів:
- [fill], рядок 4, який заповнює базу тестовими даними;
- [registerCategories], рядок 4, який заповнює словники даними, повернутими методом [fill]. Ці два словники представляють збережені сутності;
- [showDataBase], рядок 6, який зчитує обидві таблиці [CATEGORIES] та [PRODUITS] і повертає зчитані дані;
- [checkShortCategorie], рядок 13, перевіряє категорію, зчитану [showDataBase]. Вона перевіряє, чи коротка версія цієї категорії відповідає тому, що було записано у словнику категорій;
- [checkShortProduit], рядок 16, робить те саме для товарів;
- коли об’єкт знайдено у словнику, його видаляють зі словника. Рядки 19–20 перевіряють, чи обидва словники порожні. Якщо ці два твердження справджуються, це означає, що:
- усі значення, зчитані [showDataBase], дійсно були знайдені у словниках;
- що вони не містять інших сутностей, крім тих, що були зчитані;
Приватний метод [fill] має такий вигляд:
private List<Categorie> fill(int nbCategories, int nbProduits) {
// заповнюємо таблиці
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// додавання категорії — каскадним способом товари також будуть
// додані
categories = daoCategorie.saveEntities(categories);
// результат
return categories;
}
- рядки 3–12: будується список категорій [nbCategories], у кожній з яких містяться продукти [nbProduits];
- рядок 15: цей список категорій зберігається. Ми бачили, що метод [daoCategorie.saveEntities] також зберігає товари з категорій, якщо такі є;
- рядок 17: повертається збережений список категорій. Збережені сутності (категорії та товари) тепер мають первинний ключ у полі [id];
Приватний метод [registerCategories] додасть ці об’єкти до обох словників:
private void registerCategories(List<Categorie> categories) {
// словники
for (Categorie categorie : categories) {
mapCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
mapProduits.put(produit.getId(), produit);
}
}
}
Ключем доступу до кожного словника є первинний ключ сутностей.
Після цього раніше заповнена база даних буде прочитана та відображена за допомогою наступного приватного методу [showDataBase]:
private Object[] showDataBase() throws BeansException, JsonProcessingException {
// список категорій
log("Liste des catégories", 2);
List<Categorie> categories = daoCategorie.getAllShortEntities();
affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
// список товарів
log("Liste des produits", 2);
List<Produit> produits = daoProduit.getAllShortEntities();
affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
// результат
return new Object[] { categories, produits };
}
- рядки 4 та 8: отримуються короткі назви категорій та товарів;
- рядок 11: повертається масив, що містить обидва списки отриманих сутностей;
- рядки 5 та 9: списки об’єктів відображаються за допомогою наступного приватного методу [affiche]:
// відображення списку елементів типу T
private <T> void affiche(List<T> elements, ObjectMapper mapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, mapper);
}
}
// відображення елемента типу T
private <T> void affiche(T element, ObjectMapper mapper) throws JsonProcessingException {
System.out.println(mapper.writeValueAsString(element));
}
Об’єкти відображаються за допомогою маппера jSON (рядок 10). Цей мапер є другим параметром методу [affiche], рядок 2. Контекст Spring визначає чотири мапери jSON у файлі [ConfigJdbc] залежності Maven [mysql-config-jdbc]:
// фільтри jSON -------------------------------------
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
- ці маппери jSON (рядки 7–9, 16–18, 26–28, 35–37) мають атрибут
[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)]
, завдяки якому вони стають бінами, що інстанціюються при кожному запиті до контексту Spring. Це нове. Усі біни Spring, які ми бачили досі, були синглтонами: створювався лише один екземпляр, і саме він повертався щоразу, коли запитували посилання на нього в контексті Spring. Чому відбулася ця зміна? Насправді ці чотири біни [jsonMapperShortCategorie, jsonMapperLongCategorie, jsonMapperShortProduit , jsonMapperLongProduit] налаштовують єдиний мапер jSON (ось це і є синглтон), визначений у рядках 2–5. Його потрібно переналаштовувати під час кожного виклику одного з чотирьох вищезазначених бінів, а не лише один раз під час ініціалізації контексту. Якби ми вирішили створити чотири різні маппери jSON — по одному для кожного з чотирьох бінів, — то вони могли б бути синглтонами. Це було цілком можливо. Тоді в рядках 10, 19, 29, 38 ми б написали:
ObjectMapper jsonMapper = new ObjectMapper();
- чотири маппери json слугують для налаштування фільтрів jSON для об’єктів [Produit] та [Categorie]. Дійсно, ми написали (див. параграфи 4.6 та 4.6) таке:
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
та
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
Представлення jSON об’єкта [Categorie] контролюється фільтром jSON [jsonFilterCategorie], а представленняоб’єкта [produit] — фільтром jSON [jsonFilterProduit]. Чотири маппери jSON контексту Spring налаштовують ці два фільтри таким чином:
- маппер [jsonMapperShortCategorie] налаштовує фільтр jSON [jsonFilterCategorie] для скороченої версії категорії: поле [produits] не буде включено до представлення категорії jSON;
- маппер [jsonMapperLongCategorie] налаштовує фільтр jSON [jsonFilterCategorie] для розширеної версії категорії: поле [produits] буде включено до представлення категорії jSON;
- маппер [jsonMapperShortProduit] налаштовує фільтр jSON [jsonFilterProduit] для короткої версії товару: поле [categorie] не буде включено до представлення jSON товару;
- маппер [jsonMapperLongProduit] налаштовує фільтр jSON [jsonFilterProduit] для повної версії продукту: поле [categorie] буде включено до представлення продукту jSON;
Ми завершили роботу з приватним методом [showDataBase]. Повернемося до коду тесту [fillDataBase]:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// заповнення бази даних та словників
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// відображення
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// деякі перевірки
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// словники мають бути вичерпані
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
- рядки 6–8: отримуємо скорочені версії продуктів і категорій, зчитаних із бази даних;
- рядки 10–11: перші перевірки;
- рядки 12–14: кожна категорія, отримана методом [showDataBase], перевіряється наступним приватним методом [checkShortCategorie]:
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
// неможливо протестувати поле [produits] у переносимому режимі з реалізаціями jPA
}
- рядок 1: [Categorie actual] — це категорія, зчитана з бази даних, яка має збігатися з категорією, що міститься у словнику [mapCategories];
- рядок 2: отримується первинний ключ категорії, що зчитується;
- рядок 3: отримується категорія, зареєстрована з цим первинним ключем у словнику категорій;
- рядок 4: ключ видаляється зі словника, щоб упевнитися, що жодна інша прочитана категорія не використовує цей самий ключ;
- рядок 5: перевіряється, чи обидві категорії мають однакову назву;
Скорочена версія продуктів, отриманих за допомогою методу [showDataBase], перевіряється за допомогою наступного власного методу [checkShortProduit]:
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(actual.getIdCategorie(), expected.getIdCategorie());
// неможливо протестувати поле [categorie] у переносимому режимі за допомогою реалізацій jPA
}
- рядок 1: [Produit actual] — це скорочений запис, зчитаний із бази даних;
- рядки 2–3: із словника збережених продуктів вибирається продукт із тим самим первинним ключем;
- рядок 4: видаляється знайдений запис зі словника;
- рядки 5–8: перевіряється, чи обидва продукти мають однакові значення полів;
4.11.2.4. Метод [getLongCategoriesByName3]
Цей тест виглядає так:
@Test
public void getLongCategoriesByName3() {
// заповнення бази
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// тест
log("getLongCategoriesByName3", 1);
List<Categorie> categories2 = daoCategorie.getLongEntitiesByName("categorie[0]", "categorie[1]");
Assert.assertEquals(2, categories2.size());
registerCategories(Lists.newArrayList(categories.get(0), categories.get(1)));
for (Categorie categorie : categories) {
checkLongCategorie(categorie);
}
Assert.assertEquals(0, mapCategories.size());
}
- рядок 4: заповнюється база даних і отримується список збережених категорій та товарів;
- рядок 7: тестуємо метод [daoCategorie.getLongEntitiesByName(Iterable<String> names)] шару [DAO]. Запитуємо список із двох товарів, позначених їхніми повними назвами;
- рядок 8: перевіряємо, чи список, повернутий методом [daoCategorie.getLongEntitiesByName(Iterable<String> names)], дійсно містить два елементи;
- рядок 9: обидва елементи, збережені в рядку 4, додаються до словника категорій;
- рядки 10–12: перевіряється, чи два прочитані елементи дійсно є тими, що були збережені;
- рядок 13: перевіряється, чи словник категорій порожній, що означає, що всі прочитані категорії були знайдені у словнику і що він не містить значень, які не були прочитані;
У рядку 11 метод [checkLongCategorie] перевіряє повну версію категорії:
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
- у рядку 6 перевіряється, чи поле [produits] категорії не є null. Дійсно, зчитування категорії у розширеному форматі завжди повертає її з полем [produits], яке не дорівнює null. Якщо категорія не містить товарів, то поле [produits] є порожнім, але існує;
4.11.2.5. Метод [updateDataBase1]
@Test
public void updateDataBase1() {
// заповнення
fill(NB_CATEGORIES, NB_PRODUITS);
// тест
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = daoCategorie.getLongEntitiesByName("categorie[1]").get(0);
List<Produit> produits = categorie1.getProduits();
Map<Produit, Long> versions = new HashMap<Produit, Long>();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
versions.put(produit, produit.getVersion());
}
daoProduit.saveEntities(produits);
// перевірка
List<Produit> produitsInBd = daoCategorie.getLongEntitiesByName("categorie[1]").get(0)
.getProduits();
Assert.assertEquals(produits.size(), produitsInBd.size());
// перевірки
for (Produit produit2 : produitsInBd) {
Produit produit = findProduitByName(produit2.getNom(), produits);
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
Assert.assertEquals(produit2.getVersion().longValue(), versions.get(produit) + 1);
}
}
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
Метод [updateDataBase1] підвищує ціну товарів категорії з назвою categorie[1] на 10% і перевіряє дві речі:
- чи дійсно змінилася базова ціна;
- чи версія оновленого товару збільшилася на 1;
Код виконує наступні дії:
- рядок 4: заповнення бази даних;
- рядок 7: витягується з бази категорія з назвою «categorie[1]»;
- рядки 8–13: ціна всіх цих товарів підвищується на 10 % (рядок 11). Крім того, створюється словник, що пов’язує товар із його версією (рядки 9 та 12);
- рядок 14: викликається метод [daoProduit.saveEntities]. Він здійснить оновлення товарів;
- рядок 16: з бази даних витягуються товари категорії з назвою «categorie[1]»;
- рядки 20–24: для всіх товарів цієї категорії перевіряється, чи ціна була змінена (рядок 22) і чи версія була збільшена на 1 (рядок 23);
4.11.2.6. Метод [deleteProduitsByProduit1]
Метод [deleteProduitsByProduit1] видаляє товари з таблиці [PRODUITS]:
@Test
public void deleteProduitsByProduit1() {
// заповнення
fill(NB_CATEGORIES, NB_PRODUITS);
// видалення
daoProduit.deleteEntitiesByEntity(daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]"));
// перевірка
List<Produit> produits = daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]");
Assert.assertEquals(0, produits.size());
}
- рядок 6: видаляються два товари;
- рядки 8–9: перевіряється, чи їх більше немає в базі даних;
4.11.2.7. Метод [getLongProduitsById3]
@Test
public void getLongProduitsById3() {
// заповнення
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// тест
log("getLongProduitsById3", 1);
List<Produit> produits = daoProduit.getLongEntitiesByName("produit[0,3]", "produit[1,4]");
Assert.assertEquals(2, produits.size());
registerProduits(Lists.newArrayList(categories.get(0).getProduits().get(3), categories.get(1).getProduits().get(4)));
produits = daoProduit.getLongEntitiesById(produits.get(0).getId(), produits.get(1).getId());
for (Produit produit : produits) {
checkLongProduit(produit);
}
Assert.assertEquals(0, mapProduits.size());
}
- рядок 4: заповнюємо базу даних і отримуємо список збережених категорій;
- рядок 7: отримуємо з бази даних повну інформацію про два товари, ідентифіковані за їхніми назвами;
- рядок 9: товари [produit[0,3], produit[1,4]], що містяться у списку категорій у рядку 4, додаються до словника товарів;
- рядок 10: ці два самі товари шукаються в базі даних за їх первинними ключами;
- рядки 11–14: перевіряється, чи зчитані дані збігаються з даними, записаними у словнику;
Приватний метод [checkLongProduit] виглядає наступним чином:
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertNotNull(actual.getCategorie());
}
4.11.2.8. Conclusion
На цьому зупинимося. Наразі є 74 тести, і їх можна було б додати ще, оскільки я, ймовірно, пропустив деякі випадки, які слід перевірити. Навіть не будучи вичерпними, ці тести дозволили виявити чимало помилок, як правило, граничних випадків, які не були передбачені під час початкового написання шару [DAO]. Етап вичерпного тестування є необхідним для будь-якого проєкту.
Для виконання тесту можна скористатися імпортованою конфігурацією виконання під назвою [spring-jdbc-generic-04.JUnitTestDao].
![]() | ![]() |
4.11.3. Тест [JUnitTestPushTheLimits]
Тест [JUnitTestPushTheLimits] — це тест продуктивності. Ми використовуємо той факт, що тести JUnit відображають час свого виконання, щоб виміряти продуктивність шару [DAO]. Потім ці показники будуть порівняні з показниками реалізацій JPA рівня [DAO].
4.11.3.1. Squelette
Структура класу [JUnitTestPushTheLimits] така:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestPushTheLimits {
// шар [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// константи
private final int NB_CATEGORIES = 2500;
private final int NB_PRODUITS = 2;
// локальний
private Map<Long, Categorie> hCategories;
private Map<Long, Produit> hProduits;
@Before
public void clean() {
// очищення таблиці [CATEGORIES]
daoCategorie.deleteAllEntities();
// словники
hCategories = new HashMap<Long, Categorie>();
hProduits = new HashMap<Long, Produit>();
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// заповнюємо таблиці
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, 0L, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, 0L, String.format("produit[%d,%d]", i, j), 0L,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// додавання категорії — каскадним способом також будуть вставлені товари
categories = daoCategorie.saveEntities(categories);
// словники
for (Categorie categorie : categories) {
hCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
hProduits.put(produit.getId(), produit);
}
}
// результат
return categories;
}
....
// -------------------- приватні методи
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
Assert.assertNotNull(actual.getCategorie());
}
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
boolean erreur = false;
try {
actual.getCategorie().getNom();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
boolean erreur = false;
try {
actual.getProduits().size();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
}
Тут ми бачимо структуру класу [JUnitTestDao]. Ми вже знайомі з усіма цими методами. Тест працює з базою даних, що містить 2500 категорій, кожна з яких має по 2 товари (рядки 32–33). Отже, таблиця [CATEGORIES] міститиме 2500 рядків, а таблиця [PRODUITS] — 5000 рядків. Можна було б додати більше рядків, але тест і так триває вже майже хвилину. Тому ми обрали прийнятні значення для користувача, який чекає на завершення тесту.
Всього є 18 тестів. Вони виконуються з конфігурацією виконання [1]. Тривалість виконання показано в [2]:
![]() |
4.11.3.2. doNothing [0,114]
Метод [doNothing] нічого не робить. Він дозволяє виміряти тривалість виконання методу [clean], який запускається перед кожним тестом і очищає базу даних. З наведеного вище видно, що тривалість цієї операції є незначною порівняно з іншими.
@Test
public void doNothing() {
// очищення
}
4.11.3.3. perf01 [4,179]
Тест [perf01] призначений для вимірювання часу заповнення бази даних:
@Test
public void perf01() {
// вставити
fill(NB_CATEGORIES, NB_PRODUITS);
}
4.11.3.4. perf02 [7,624]
Метод [perf02]:
- заповнює базу даних;
- потім змінює назви всіх категорій та ціни всіх товарів.
@Test
public void perf02() {
// update
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
for (Categorie categorie : categories) {
categorie.setNom(categorie.getNom() + "*");
for (Produit produit : categorie.getProduits()) {
produit.setPrix(produit.getPrix() * 1.1);
}
}
// оновлення
daoCategorie.saveEntities(categories);
}
4.11.3.5. perf03[3,911]
Метод [perf03]:
- заповнює базу даних
- , а потім видаляє всі категорії одну за одною. Товари також видаляються через каскадну залежність між таблицею [CATEGORIES] та таблицею [PRODUITS].
Тут може викликати подив той факт, що ця операція триває менше часу, ніж операція [perf01] [4,179 s], яка виконує менше дій.
@Test
public void perf03() {
// видалення категорій та, відповідно, товарів
daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}
Якщо подивитися на код методу [daoCategorie.deleteEntitiesByEntity], то можна побачити, що буде виконано [PreparedStatement] з 2500 параметрами (кількість категорій). Саме тут вступає в дію бін [maxPreparedStatementParameters], який розбиває замовлення SQL на кілька [PreparedStatement], кількість параметрів яких є прийнятною для конкретного використовуваного SGBD.
4.11.3.6. perf04[2,426]
Метод [perf04]:
- заповнює базу даних;
- потім запитує розгорнуту версію всіх категорій;
@Test
public void perf04() {
// вибрати
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
ids.add(categorie.getId());
}
daoCategorie.getLongEntitiesById(ids);
}
4.11.3.7. perf05 [3,507]
Метод [perf05]:
- заповнює базу даних;
- потім видаляє 5000 товарів за їх первинними ключами (тож потенційно ми маємо [PreparedStatement] з 5000 параметрами);
- перевіряє, чи таблиця товарів після цього порожня;
@Test
public void perf05() {
// видалити товари
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
for (Produit p : categorie.getProduits()) {
ids.add(p.getId());
}
}
daoProduit.deleteEntitiesById(ids);
// перевірка
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.3.8. Résultats
Ми не будемо далі описувати окремі тести. Ми просто вкажемо, що вони роблять, та їхню тривалість. Ці показники цікаві лише у порівнянні між собою. Адже їхні значення залежать від використовуваного тестового середовища (апаратного забезпечення та конфігурації програмного забезпечення). Але якщо вони отримані в одному й тому ж середовищі, їх можна порівнювати.
Загальна тривалість тесту: 59,995 секунд
роль | ||
заповнює базу даними з 2500 категоріями та 5000 товарами | ||
заповнює, а потім змінює базу даних | ||
заповнює базу даних, а потім видаляє всі категорії та їхні товари | ||
заповнює базу даних і запитує повну версію всіх категорій | ||
заповнює базу даних і видаляє 5000 товарів по одному за їх первинними ключами | ||
заповнює базу даних і видаляє 5000 товарів по одному за їхніми назвами | ||
заповнює базу даних і видаляє 5000 товарів по одному за їхніми артикулами | ||
заповнює базу даних і запитує короткі назви всіх товарів за їхніми назвами | ||
заповнює базу даних і запитує повні назви всіх продуктів за їхніми назвами | ||
заповнює базу даних і запитує скорочені назви всіх продуктів за їх первинними ключами | ||
заповнює базу даних і запитує повну версію всіх продуктів за їх первинними ключами | ||
заповнює базу даних, а потім видаляє всі категорії (а отже, і пов’язані з ними товари) одну за одною за їхніми назвами | ||
заповнює базу даних, а потім видаляє всі категорії (а отже, і пов’язані з ними товари) одну за одною за їхніми кодами | ||
заповнює базу даних і запитує короткі назви всіх категорій за їхніми назвами | ||
заповнює базу даних і запитує повну версію всіх категорій за їхніми назвами | ||
заповнює базу даних і запитує короткі назви всіх категорій за їх первинними ключами | ||
заповнює базу даних і запитує повну версію всіх категорій за їх первинними ключами |
Ці результати іноді дивують:
- отримати повну версію продуктів (perf09) було швидше, ніж їхню скорочену версію (perf08), хоча повна версія вимагає з'єднання двох таблиць;
- тривалість першого заповнення (perf01) значно перевищує тривалість усіх інших заповнень, що відбудуться згодом;
- запит короткої версії продуктів за їхніми назвами (perf08) займає більше часу, ніж запит за первинними ключами (perf10). Це здається цілком логічним. Але для довгих версій все навпаки (perf09, perf11);
Тому ми не будемо детально зупинятися на цих результатах. Проте вони стануть нам у нагоді для порівняння цього рішення [Spring JDBC] із такими рішеннями:
- [Spring JDBC] та п’ятьма іншими SGBD;
- [Spring JPA], які будуть розглянуті далі;





























