Skip to content

7. [TD]: Реалізація шару [DAO] класу TD за допомогою API та JDBC

Ключові слова: реляційні бази даних, API, JDBC, SQLException.

Повернемося до багатошарової архітектури нашого додатка:

Дані, необхідні для проведення виборів, зберігаються в базі даних MySQL [dbelections]

7.1. Support

Папка [support / chap-07] [1] містить:

  • проекти Eclipse з цього розділу [2];
  • скрипт SQL для створення бази даних MySQL, [dbelections], [3];

7.2. База даних [dbelections]


Завдання: створіть базу даних MySQL та [dblelections], дотримуючись процедури, описаної в розділі 6.4.2.


База даних [dbelections] — це база даних MySQL із двома таблицями:

  

Таблиця [conf] містить інформацію про конфігурацію виборів:

 
  • [id]: первинний ключ з автоінкрементом;
  • [version]: номер версії запису — тут можна пропустити;
  • [sap]: кількість місць, що підлягають заповненню;
  • [seuilelectoral]: поріг, нижче якого список виключається;

Його вміст такий:

 

Таблиця [listes] містить списки кандидатів на виборах:

 
  • [id]: первинний ключ з автоінкрементом;
  • [version]: номер версії запису — тут можна пропустити;
  • [nom]: назва списку;
  • [voix]: голос у списку — стає відомим лише після введення користувачем даних у шарі [présentation];
  • [sieges]: кількість отриманих місць — стає відомою лише після обчислення шару [métier];
  • [elimine]: дорівнює 1, якщо список виключено, інакше — 0 — відоме лише після обчислення шару [métier];

Зміст таблиці [listes] такий:

 

Скрипт SQL для генерації бази [dbelections] називається [dbelections.sql] і знаходиться на сервері. Його код такий:


-- phpMyAdmin SQL Дамп
-- версія 4.0.4
-- http://www.phpmyadmin.net
--
-- Клієнт: localhost
-- Створено: середа, 11 березня 2015 р. о 12:20
-- Версія сервера: 5.6.12-log
-- Версія PHP: 5.4.12

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- База даних: `dbelections`
--
CREATE DATABASE IF NOT EXISTS `dbelections` DEFAULT CHARACTER SET utf8 COLLATE utf8_swedish_ci;
USE `dbelections`;

-- --------------------------------------------------------

--
-- Структура таблиці `conf`
--

CREATE TABLE IF NOT EXISTS `conf` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `version` int(11) NOT NULL DEFAULT '1',
  `sap` tinyint(4) NOT NULL,
  `seuilelectoral` double NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=2 ;

--
-- Вміст таблиці `conf`
--

INSERT INTO `conf` (`id`, `version`, `sap`, `seuilelectoral`) VALUES
(1, 1, 6, 0.05);

-- --------------------------------------------------------

--
-- Структура таблиці `listes`
--

CREATE TABLE IF NOT EXISTS `listes` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `version` int(11) NOT NULL DEFAULT '1',
  `nom` varchar(20) COLLATE utf8_swedish_ci NOT NULL,
  `voix` int(11) NOT NULL,
  `sieges` int(11) NOT NULL,
  `elimine` tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `nom` (`nom`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=8 ;

--
-- Вміст таблиці `listes`
--

INSERT INTO `listes` (`id`, `version`, `nom`, `voix`, `sieges`, `elimine`) VALUES
(1, 21, 'A', 10, 1, 0),
(2, 22, 'B', 20, 2, 0),
(3, 21, 'C', 30, 3, 0),
(4, 13, 'D', 40, 3, 0),
(5, 17, 'E', 50, 6, 0),
(6, 18, 'F', 60, 1, 0),
(7, 19, 'G', 70, 2, 0);

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

7.3. Проєкт Eclipse

Проект Eclipse для шару [DAO] матиме такий вигляд:

  
  • пакет [elections.dao.entities] містить об’єкти, з якими працює рівень [DAO];
  • пакет [elections.dao.service] містить реалізацію шару [DAO];
  • пакет [elections.dao.config] містить конфігурацію шару [DAO]
  • пакет [elections.dao.junit] містить тестовий клас JUnit цього проєкту;
  • пакет [elections.dao.console] містить виконуваний тестовий клас;

7.4. Конфігурація проекту Maven

 

Файл [pom.xml], що налаштовує проект Maven, має такий вигляд:


<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>istia.st.elections</groupId>
    <artifactId>elections-dao-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

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

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

    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <!-- Tomcat Jdbc -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- бібліотека jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Тестування Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Ведення журналів у Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <!-- плагіни -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>config.AppConfig</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- для встановлення артефакту проєкту в локальне сховище Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>

Цей файл аналогічний тому, що описано в розділі 6.5.1. У нього внесено такі зміни:

  • рядки 8–12: визначено батьківський проект Maven. Проєкт [spring-boot-starter-parent] (рядок 10) визначає дуже велику кількість залежностей із зазначенням їхніх версій. Коли використовується одна з них (рядки 19–57), немає потреби вказувати її версію, оскільки вона вже визначена в батьківському проєкті Maven;
  • рядки 40–51: залежності, необхідні для тестового класу проєкту. Ці залежності мають атрибут [<scope>test</scope>], що означає, що вони потрібні лише для класів у папці [src / test / java]. Ці залежності не будуть включені до кінцевого архіву проєкту;
  • рядки 53–56: бібліотека [spring-boot-starter-logging] використовуватиметься Spring для виведення логів на екран;
  • рядки 14–17: властивості конфігурації Maven:
    • рядок 15: вказує, що вихідні файли написані на UTF-8;
    • рядок 16: вказує, що слід використовувати компілятор версії 1.8;

7.5. Елементи шару [DAO]

  
  • [ElectionsConfig] — це об’єктна модель, пов’язана з рядком таблиці [CONF];
  • [ListeElectorale] — це об’єктна модель, пов’язана з рядком таблиці [LISTES];
  • [AbstactEntity] — це батьківський клас для двох попередніх класів. Він об’єднує поля [id, version], спільні для обох класів;
  • [ElectionsException] — це клас винятків;

7.5.1. Клас [ElectionsException]

  

Клас [ElectionsException] було описано в розділі 4.3. Наводимо його код ще раз:


package ...;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// клас винятків для додатка «Вибори»
// виняток є неконтрольованим

public class ElectionsException extends RuntimeException implements Serializable {

    // серійний номер ID
    private static final long serialVersionUID = 1L;

    // локальні поля
    private int code;
    private List<String> erreurs;

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

    public ElectionsException(int code, Throwable e) {
        // батьківський
        super(e);
        // локальний
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message, Throwable e) {
        // батьківський
        super(message,e);
        // локальний
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message) {
        // батьківський
        super(message);
        // локальний
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public ElectionsException(int code, List<String> erreurs) {
        // батьківський
        super();
        // локальний
        this.code = code;
        this.erreurs = erreurs;
    }

    // список повідомлень про помилки винятку
    private List<String> getErreursForException(Throwable th) {
        // отримується список повідомлень про помилки винятку
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // отримуємо повідомлення, лише якщо воно !=null і не порожнє
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // наступна причина
            cause = cause.getCause();
        }
        return erreurs;
    }

    // гетери та сеттери
...
}

Об’єкт [ElectionsException] характеризується двома даними:

  • рядок 16: код помилки;
  • рядок 17: список повідомлень про помилки, пов’язаних зі стеком винятків, що виникли;
  • клас має 5 конструкторів (рядки 20, 24, 32, 40, 50);
  • рядки 59–76: метод [getErreursForException] дозволяє отримати повідомлення про помилки зі стека винятків;

7.5.2. Клас [AbstractEntity]

  

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


package elections.dao.entities;

import java.io.Serializable;

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

public abstract class AbstractEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    // поля
    protected Long id;
    protected Long version;

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

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // сигнатура
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // методи getter та setter
...
}

Він зберігає поля [ID, NOM] із рядків таблиць [CONF] та [LISTES] (рядки 8–9).

  • рядок 8: клас має атрибут [Abstract], що вказує на те, що він не може бути інстанційований. Він може бути лише похідним;
  • рядки 26–33: сигнатура об’єкта jSON;
  • рядок 28: повертається рядок jSON з [this]. Якщо під час виконання [this] представляє об’єкт, похідний від [AbstactEntity], то отримується рядок jSON цього похідного об’єкта. Таким чином, похідним класам не потрібно визначати метод [toString]. Достатньо методу батьківського класу;

7.5.3. Клас [ElectionsConfig]

  

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


package elections.dao.entities;


public class ElectionsConfig extends AbstractEntity {

    private static final long serialVersionUID = 1L;
    // поля
    private int nbSiegesAPourvoir;
    private double seuilElectoral;

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

    public ElectionsConfig(Long id, Long version, int nbSiegesAPourvoir, double seuilElectoral) {
        // батьківський об’єкт
        super(id, version);
        // локальні поля
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }

    // геттери та сеттери
...
}
  • рядок 4: клас успадковує клас [AbstractEntity];
  • рядки 8–9: зберігають інформацію зі стовпців [SAP, SEUILELECTORAL] таблиці [CONF];

7.5.4. Клас [ListeElectorale]

  

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


package elections.dao.entities;

public class ListeElectorale extends AbstractEntity {

    // поля
    private String nom;
    private int voix;
    private int sieges;
    private boolean elimine;

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

    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        // батьківський
        super();
        // локальні поля
        initNom(nom);
        initVoix(voix);
        initSieges(sieges);
        this.elimine=elimine;
    }

    public ListeElectorale(Long id, Long version, String nom, int voix, int sieges, boolean elimine) {
        // батьківський
        super(id, version);
        // локальні поля
        initNom(nom);
        initVoix(voix);
        initSieges(sieges);
        this.elimine=elimine;
    }

    // приватні методи
    private void initNom(String nom) {
        this.nom = nom.trim();
        if ("".equals(nom)) {
            throw new ElectionsException(10, "Le nom ne peut être vide");
        }
    }

    private void initVoix(int voix) {
        this.voix = voix;
        if (voix < 0) {
            throw new ElectionsException(11, "Le nombre de voix ne peut être <0");
        }
    }

    private void initSieges(int sieges) {
        this.sieges = sieges;
        if (sieges < 0) {
            throw new ElectionsException(12, "Le nombre de sièges ne peut être <0");
        }
    }

    // гетери та сеттери

    public String getNom() {
        return nom;
    }

    public void setNom(String nom) {
        initNom(nom);
    }

    public int getVoix() {
        return voix;
    }

    public void setVoix(int voix) {
        initVoix(voix);
    }

    public int getSieges() {
        return sieges;
    }

    public void setSieges(int sieges) {
        initSieges(sieges);
    }

    public boolean isElimine() {
        return elimine;
    }

    public void setElimine(boolean elimine) {
        this.elimine = elimine;
    }

}
  • рядок 3: клас успадковує клас [AbstractEntity];
  • рядки 6–9: клас зберігає стовпці [NOM, VOIX, SIEGES, ELIMINE] таблиці [LISTES];

7.6. Конфігурація Spring для шару [DAO]

 

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


package elections.dao.config;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@ComponentScan(basePackages = { "elections.dao.service" })
@EnableCaching
public class AppConfig {

    // константи
    public final static String URL = "jdbc:mysql://localhost:3306/dbelections";
    public final static String USER = "root";
    public final static String PASSWD = "";
    public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    public final static String SELECT_LISTES = "SELECT ID, VERSION, NOM, VOIX, SIEGES, ELIMINE FROM LISTES";
    public final static String SELECT_CONF = "SELECT ID, VERSION, SAP, SEUILELECTORAL, SAP FROM CONF";
    public final static String UPDATE_LISTES = "UPDATE LISTES SET VOIX=?, SIEGES=?, ELIMINE=? WHERE ID=?";

    @Bean
    public DataSource dataSource() {
        // джерело даних TomcatJdbc
        DataSource dataSource = new DataSource();
        // конфігурація доступу JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // спочатку відкрите з'єднання
        dataSource.setInitialSize(1);
        // результат
        return dataSource;
    }

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("electionsConfig");
    }
}
  • рядки 25–38: доступ до BD здійснюватиметься через джерело даних [tomcat-jdbc]. Цей тип джерела було використано та пояснено в розділі 6.5;
  • рядки 17–23: набір статичних констант, доступних для всіх класів проекту;
  • рядок 13: анотація [@Configuration] робить клас [AppConfig] класом конфігурації Spring;
  • рядок 11: анотація [@ComponentScan] вказує пакети, у яких можна знайти об’єкти Spring. Тут ми визначимо об’єкт Spring у пакеті [dao]. Анотація [@ComponentScan] перетворює клас на клас конфігурації, що позбавляє нас необхідності додавати анотацію [@Configuration];
  • у рядку 12 вмикається управління кешем. Принцип такий:
    • до методу M додаємо анотацію [@Cacheable('nom_du_cache')]. «ім'я_кешу» — це ім'я, яке використовується у рядку 41;
    • коли метод M викликається вперше, його результати повертаються і також зберігаються у кеші, названому за допомогою анотації;
    • коли метод M викликається наступні рази з тими самими параметрами, що й уперше, він не виконується, а Spring просто повертає значення з кешу;

7.7. Налаштування журналів

Бібліотеки журналів визначаються наступною залежністю у [pom.xml]:


        <!-- Ведення журналу Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
</dependency>

Ця залежність включає такі бібліотеки:

  

Бібліотека [logback] відповідатиме за логи. Вона налаштовується двома файлами:

  • [logback.xml] — для основної гілки коду;
  • [logback-test.xml] — для тестової гілки коду. У разі відсутності цього файлу використовується попередній файл;

Ці два файли мають знаходитися в папці [Classpath] проекту. З цієї причини їх розміщують у папці:

  • [src / main / ressources] — для основної гілки коду;
  • [src / test / ressources] — для тестової гілки коду;

Вміст файлів тут однаковий:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- кодерам за замовчуванням присвоюється тип
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- контроль рівня журналів -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Все відбувається у рядку 12, де встановлюється бажаний рівень деталізації:

  • [debug]: найдетальніший рівень;
  • [off]: відсутність журналів;
  • [info]: звичайний рівень журналів;
  • [warn]: те саме, що рівень [info], плюс попереджувальні повідомлення (warning). Ці повідомлення вказують на можливу помилку;

Перейдіть у режим [debug], щойно під час виконання з’являться помилки.

7.8. Реалізація рівня [DAO]

  

Інтерфейс [IDao] шару [DAO] має такий вигляд:


package istia.st.elections.webapi.client.dao;

import istia.st.elections.webapi.client.entities.ElectionsConfig;
import istia.st.elections.webapi.client.entities.ListeElectorale;

public interface IDao {
    // конфігурація виборів
    public ElectionsConfig getElectionsConfig();

    // списки кандидатів
    public ListeElectorale[] getListesElectorales();

    // збереження результатів виборів
    public void setListesElectorales(ListeElectorale[] listesElectorales);

}

Скелет класу [ElectionsDaoJdbc], що реалізує рівень [dao] із базою даних, матиме такий вигляд:


package elections.dao.service;

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

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ElectionsException;
import elections.dao.entities.ListeElectorale;

@Component
@SuppressWarnings("unused")
public class ElectionDaoJdbc implements IElectionsDao {

    @Autowired
    private DataSource dataSource;

    @Cacheable("electionsConfig")
    // отримання конфігурації виборів
    public ElectionsConfig getElectionsConfig() {
        throw new RuntimeException("[getElectionsConfig] not yet implemented");
    }

    // отримання списків
    public ListeElectorale[] getListesElectorales() {
        throw new RuntimeException("[getListesElectorales] not yet implemented");
    }

    // внесення змін до списків [voix, sieges, elimine]
    public void setListesElectorales(ListeElectorale[] listesElectorales) {
        throw new RuntimeException("[setListesElectorales] not yet implemented");
    }

    // -------------------- приватні методи

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

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

            }
        }
        // закрити з'єднання
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // повертається [ElectionsException]
                return new ElectionsException(code, e3);
            }
        }
        // винятку немає
        return null;
    }

    // обробка помилки
    private ElectionsException doCatchException(int code1, int code2, Connection connexion, Throwable th) {
        // генерується [ElectionsException]
        ElectionsException ex1 = new ElectionsException(code1, th);
        // скасування транзакції
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
        }
        // повертається виняток
        return ex1;
    }
}
  • рядок 24: анотація [@Cacheable] — це анотація Spring, яка вимагає збереження в пам’яті («кешування») результатів методу [getElectionsConfig]. Це можливо зробити тут, оскільки вміст таблиці [CONF] ніколи не змінюється. Неможливо застосувати цю анотацію до методу [getListesElectorales], оскільки вміст таблиці [LISTES] змінюється з часом;
  • методи [doCatchException] та [doFinally] повертають тип [ElectionsException]. Метод [doFinally] повертає покажчик типу null, якщо звільнення ресурсів відбулося без помилок;

Завдання: напишіть клас [ElectionDaoJdbc], спираючись на клас [IntroJdbc02], розглянутий у розділі 6.5.2.


7.9. Тестовий клас [Main]

  

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


package elections.dao.console;

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

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import elections.dao.config.AppConfig;
import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ElectionsException;
import elections.dao.entities.ListeElectorale;
import elections.dao.service.IElectionsDao;

public class Main {

    // джерело даних
    private static IElectionsDao dao;

    public static void main(String[] args) {
        // отримано посилання на шар [DAO] після створення екземпляра контексту Spring
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // отримання джерела даних
            dao = ctx.getBean(IElectionsDao.class);
        } catch (Exception e) {
            System.out.println("Les erreurs suivantes se sont produites lors de l'initialisation du contexte Spring -------");
            for (String erreur : getErreursForThrowable(e)) {
                System.out.println(erreur);
            }
            // кінець
            return;
        }
        // читання BD
        ElectionsConfig electionsConfig = null;
        ListeElectorale[] listes;
        try {
            // вміст обох таблиць
            electionsConfig = dao.getElectionsConfig();
            listes = dao.getListesElectorales();
        } catch (ElectionsException e) {
            System.out.println("Les erreurs suivantes se sont produites lors de la lecture des tables ----------");
            for (String erreur : e.getErreurs()) {
                System.out.println(erreur);
            }
            // кінець
            return;
        }
        // все пройшло успішно — виведення на екран
        System.out.println(String.format("Nombre de sièges à pourvoir : %d", electionsConfig.getNbSiegesAPourvoir()));
        System.out.println(String.format("Seuil électoral : %5.2f", electionsConfig.getSeuilElectoral()));
        System.out.println("Listes candidates----------------");
        for (ListeElectorale liste : listes) {
            System.out.println(liste);
        }
    }

    // приватні методи ------------------
    private static List<String> getErreursForThrowable(Throwable th) {
        // отримуємо список повідомлень про помилки винятку
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // отримуємо повідомлення, лише якщо воно !=null і не порожнє
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // наступна причина
            cause = cause.getCause();
        }
        return erreurs;
    }

}
  • рядки 21–31: отримання посилання на шар [DAO];
  • рядок 21: використовується синтаксис під назвою try_with_resources. Його синтаксис такий:

try (T ressource=expression) {
            // використання ресурсу
...
}
  • (продовження)
    • тип T у рядку 1 повинен реалізовувати інтерфейс [java.lang.AutoCloseable];
    • після виходу з блоку рядків 1–4 ресурс типу [java.lang.AutoCloseable] автоматично звільняється незалежно від того, чи стався виняток. Ті, хто знайомий з мовою C#, впізнають тут синтаксичного та функціонального аналога оператора using (T ресурс = вираз);
  • рядки 40–49: використання шару [DAO] для отримання вмісту таблиць [CONF] та [LISTES];
  • рядки 41–47: перевіряється кеш [electionsconfig]. Він був визначений у двох місцях:
    • у класі [ElectionsDaoJdbc]:

  @Cacheable("electionsConfig")
  public ElectionsConfig getElectionsConfig() {
  • (продовження)
    • у класі конфігурації [AppConfig]:

@EnableCaching
public class AppConfig {
...
  @Bean
  public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("electionsConfig");
  }
}
  • рядок 59: закриття контексту Spring;
  • рядки 62–67: виведення отриманої інформації;

Результати, отримані з реалізованим шаром [DAO], такі:

...
début requête 1 : 11:09:29:752
fin requête 1 et début requête 2: 11:09:30:132
fin requête 2 : 11:09:30:133
...
Nombre de sièges à pourvoir : 6
Seuil électoral :  0,05
Listes candidates----------------
{"id":1,"version":9,"nom":"A","voix":32000,"sieges":2,"elimine":false}
{"id":2,"version":13,"nom":"B","voix":25000,"sieges":2,"elimine":false}
{"id":3,"version":14,"nom":"C","voix":16000,"sieges":1,"elimine":false}
{"id":4,"version":13,"nom":"D","voix":12000,"sieges":1,"elimine":false}
{"id":5,"version":14,"nom":"E","voix":8000,"sieges":0,"elimine":false}
{"id":6,"version":13,"nom":"F","voix":4500,"sieges":0,"elimine":true}
{"id":7,"version":13,"nom":"G","voix":2500,"sieges":0,"elimine":true}
  • рядки 2–4: демонструють вплив кешу:
    • запит 1 триває 80 мілісекунд;
    • запит 2 триває 1 мілісекунду;

Коли база даних відключена, отримуємо такі результати:

1
2
3
4
5
Les erreurs suivantes se sont produites lors de la lecture des tables ----------
Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
Connection refused: connect

7.10. Тести JUnit класу [ElectionsDaoJdbc]

  

Клас [Test01] є наступним тестовим класом [JUnit]:


package elections.dao.junit;

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 elections.dao.config.AppConfig;
import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ListeElectorale;
import elections.dao.service.IElectionsDao;


@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // шар [DAO]
    @Autowired
    private IElectionsDao electionsDao;

    @Before
    public void init() {
        // очищається таблиця [LISTES]
        // списки кандидатів
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // обнуляємо кількість голосів і місць та встановлюємо значення «false»
        int voix = 0;
        int sièges = 0;
        boolean elimine = false;
        for (ListeElectorale liste : listes) {
            liste.setVoix(voix);
            liste.setSieges(sièges);
            liste.setElimine(elimine);
        }
        // збереження цих даних за допомогою шару [dao]
        electionsDao.setListesElectorales(listes);
    }

    @Test
    public void testElections01() {
        ...
    }
}
  • рядок 17: анотація [RunWith], яка є анотацією [JUnit] (рядок 6), забезпечує інтеграцію зі Spring через клас [SpringJUnit4ClassRunner];
  • рядок 16: анотація [SpringApplicationConfiguration] є анотацією [Spring] (рядок 8), яка дозволяє вказати класи конфігурації тесту JUnit. Тут вказується клас [AppConfig], який використовується для конфігурації проєкту. Таким чином, ми отримуємо доступ до всіх об’єктів Spring, визначених цим класом конфігурації. Саме так у рядках 21–22 можна ввести посилання на рівень [DAO], який буде тестуватися;
  • рядок 25: анотація [Before] позначає метод, який має виконуватися перед кожним тестом;
  • рядки 26–41: метод [init] обнуляє кількість голосів і місць у списках таблиці [LISTES] та встановлює значення логічного оператора [elimine] на [false];

Єдиний метод тестування такий:


@Test
    public void testElections01() {
        System.out.println("testElections01-------------------------------------");
        // отримання конфігурації виборів
        ElectionsConfig electionsConfig = electionsDao.getElectionsConfig();
        int nbSiegesAPourvoir = electionsConfig.getNbSiegesAPourvoir();
        double seuilElectoral = electionsConfig.getSeuilElectoral();
        Assert.assertEquals(6, nbSiegesAPourvoir);
        Assert.assertEquals(0.05, seuilElectoral, 1E-6);

        // списки кандидатів
        ListeElectorale[] listes = electionsDao.getListesElectorales();
        // відображення зчитаних значень
        System.out.println("Nombre de sièges à pourvoir : " + nbSiegesAPourvoir);
        System.out.println("Seuil électoral : " + seuilElectoral);
        System.out.println("Listes en compétition ---------------------");
        for (int i = 0; i < listes.length; i++) {
            System.out.println(listes[i]);
        }

        // присвоєння голосів і місць спискам
        int voix = 0;
        int sièges = 0;
        boolean elimine = false;
        for (ListeElectorale liste : listes) {
            liste.setVoix(voix);
            liste.setSieges(sièges);
            liste.setElimine(elimine);
            voix += 10;
            sièges += 1;
            elimine = !elimine;
        }

        // збереження цих даних за допомогою шару [dao]
        electionsDao.setListesElectorales(listes);

        // повторне зчитування даних
        ListeElectorale[] listesElectorales2 = electionsDao.getListesElectorales();
        // перевіряються зчитані дані
        Assert.assertEquals(7, listesElectorales2.length);
        voix = 0;
        sièges = 0;
        elimine = false;
        for (ListeElectorale liste : listesElectorales2) {
            Assert.assertEquals(voix, liste.getVoix());
            Assert.assertEquals(sièges, liste.getSieges());
            Assert.assertEquals(elimine, liste.isElimine());
            voix += 10;
            sièges += 1;
            elimine = !elimine;
        }
        System.out.println("Listes en compétition ---------------------");
        for (int i = 0; i < listes.length; i++) {
            System.out.println(listes[i]);
        }
    }
  • рядки 5–9: перевіряємо, чи можна отримати вміст таблиці [CONF];
  • рядки 11–19: відображаємо вміст таблиці [LISTES]. Тут немає тестів, лише візуальна перевірка;
  • рядки 21–35: змінюємо в базі даних таблицю [LISTES], присвоюючи спискам значення для їхніх полів [voix, sieges, elimine];
  • рядки 37–51: повторно зчитується таблиця [LISTES] і перевіряється, чи отримане значення дійсно дорівнює введеному;
  • рядки 52–55: візуальна перевірка;

Результати виведення на консоль, отримані з реалізованим шаром [DAO], такі:

mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper getDefaultTestExecutionListenerClassNames
INFOS: Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]
mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper instantiateListeners
INFOS: Could not instantiate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener]. Specify custom listener classes or make the default listener classes (and their required dependencies) available. Offending class: [org/springframework/transaction/interceptor/TransactionAttributeSource]
mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper instantiateListeners
INFOS: Could not instantiate TestExecutionListener [org.springframework.test.context.web.ServletTestExecutionListener]. Specify custom listener classes or make the default listener classes (and their required dependencies) available. Offending class: [javax/servlet/ServletContext]
mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper instantiateListeners
INFOS: Could not instantiate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]. Specify custom listener classes or make the default listener classes (and their required dependencies) available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute]
mars 11, 2015 4:50:00 PM org.springframework.test.context.support.DefaultTestContextBootstrapper getTestExecutionListeners
INFOS: Using TestExecutionListeners: [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@483bf400, org.springframework.test.context.support.DirtiesContextTestExecutionListener@21a06946]

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

[2015-03-11 16:50:01.272] - 11696 INFOS [main] --- org.eclipse.jdt.internal.junit.runner.RemoteTestRunner: Starting RemoteTestRunner on Gportpers3 with PID 11696 (started by ST in D:\data\istia-1415\eclipse\intro-jdbc\elections-jdbc-01)
[2015-03-11 16:50:01.317] - 11696 INFOS [main] --- org.springframework.context.annotation.AnnotationConfigApplicationContext: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@74ad1f1f: startup date [Wed Mar 11 16:50:01 CET 2015]; root of context hierarchy
mars 11, 2015 4:50:01 PM org.eclipse.jdt.internal.junit.runner.RemoteTestRunner logStarted
INFOS: Started RemoteTestRunner in 0.775 seconds (JVM running for 1.433)
testElections01-------------------------------------
Nombre de sièges à pourvoir : 6
Seuil électoral : 0.05
Listes en compétition ---------------------
{"id":1,"version":21,"nom":"A","voix":0,"sieges":0,"elimine":false}
{"id":2,"version":22,"nom":"B","voix":0,"sieges":0,"elimine":false}
{"id":3,"version":21,"nom":"C","voix":0,"sieges":0,"elimine":false}
{"id":4,"version":13,"nom":"D","voix":0,"sieges":0,"elimine":false}
{"id":5,"version":17,"nom":"E","voix":0,"sieges":0,"elimine":false}
{"id":6,"version":18,"nom":"F","voix":0,"sieges":0,"elimine":false}
{"id":7,"version":19,"nom":"G","voix":0,"sieges":0,"elimine":false}
Listes en compétition ---------------------
{"id":1,"version":21,"nom":"A","voix":0,"sieges":0,"elimine":false}
{"id":2,"version":22,"nom":"B","voix":10,"sieges":1,"elimine":true}
{"id":3,"version":21,"nom":"C","voix":20,"sieges":2,"elimine":false}
{"id":4,"version":13,"nom":"D","voix":30,"sieges":3,"elimine":true}
{"id":5,"version":17,"nom":"E","voix":40,"sieges":4,"elimine":false}
{"id":6,"version":18,"nom":"F","voix":50,"sieges":5,"elimine":true}
{"id":7,"version":19,"nom":"G","voix":60,"sieges":6,"elimine":false}
  • рядки 1–23: журнали Spring Test;
  • рядки 25–26: вміст таблиці [CONF];
  • рядки 27–34: початковий вміст таблиці [LISTES];
  • рядки 35–42: вміст таблиці [LISTES] після присвоєння значень полям [voix, sieges, elimine];

Крім того, тест JUnit пройшов успішно:

 

7.11. Створення архіву [with-dependencies] з шару [dao]

Остаточний проект має таку архітектуру:

Нагадаємо конфігурацію Maven для проекту:


<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>istia.st.elections</groupId>
    <artifactId>elections-dao-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

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

    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- бібліотека jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Тестування Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Ведення журналів у Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <!-- плагіни -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>config.AppConfig</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- для встановлення артефакту проєкту в локальне сховище Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  

Ми створимо єдиний jar-файл, який міститиме класи з усіх вищезазначених jar-файлів, а також класи з проєкту шару [DAO]. Це робиться шляхом внесення змін до файлу [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        ...
    </dependencies>

    <!-- плагіни -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>console.Main</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • рядки 25–37: налаштовують плагін Maven для створення JAR-файлу;
  • рядок 15: вказує версію Java, яку слід використовувати для компіляції;

Після внесення цієї зміни можна згенерувати jar-файл наступним чином [1-10]:

  • у [5] вкажіть папку проекту за допомогою кнопки [6];
  • у [7] — вкажіть ім’я конфігурації виконання;
  • у [8] вкажіть список цілей Maven, які потрібно виконати:
    • [clean]: очищає папку [target] проекту, в яку буде розміщено згенерований jar-файл;
    • [compile]: компілює проект;
    • [assembly:single]: створює єдиний JAR-файл, що містить усі класи проекту та його залежностей;
  • у [9-10] переконайтеся, що у вас є JDK (Java Development Kit), а не JRE (Java Runtime Environment). Різниця полягає в тому, що JDK містить компілятор, а JRE — ні. Якщо у вас немає JDK, вам потрібно додати його до [10] разом із [11]. Для цього дотримуйтесь процедури, описаної в пункті 3.1;
  • у [17] виконайте налаштування виконання;
  • у [13] — згенерований архів;

Цей архів можна відкрити за допомогою програми для розпакування:

 

7.12. Перевірка архіву шару [DAO]

Створимо стандартний проект Eclipse (не Maven) [1]:

Скопіюємо та вставимо пакет [dao.console] із проєкту [elections-dao-jdbc-01] у проєкт [elections-dao-jdbc-02] [2]. З’являється багато помилок, оскільки клас [Main] посилається на класи, яких немає в його [Classpath]. Ми змінимо цей файл.

Спочатку створимо папку [lib] у новому проєкті:

У [9] ми розміщуємо архів, створений на попередньому етапі, у папці [lib], а потім змінюємо Build Path проекту:

  • на [18], імпортували архів шару [DAO], створений раніше;
  • у [19] проект більше не містить помилок;

Тепер можна запустити клас [Main]. Отримуємо ті самі результати, що й раніше.