Skip to content

11. [Cours]: Управління реляційними базами даних за допомогою Spring Data

Ключові слова: багатошарова архітектура, Spring, ін’єкція залежностей, API JPA (Java Persistence API), Spring Data.

Ми реалізуємо рівень [DAO] з TD за допомогою [Spring Data], гілки екосистеми Spring. [Spring Data] базується на шарі JPA (Java Persistence API), що дозволяє шару [DAO] оперувати об’єктами, а не командами SQL. У підсумку рівень [DAO] не знає, що взаємодіє з базою даних. Він знає лише інтерфейс рівня [Spring Data].

Спочатку ми розглянемо [Spring Data] на двох прикладах.

11.1. Support

  • у [1], папка [support / chap-11] містить три проекти Eclipse;
  • у [2] — скрипт SQL, що дозволяє створити приклад бази даних для цього розділу;

11.2. Приклад 1

На сайті Spring є чимало підручників для початківців із Spring [http://spring.io/guides]. Ми скористаємося одним із них, щоб ознайомитися зі Spring Data. Для цього ми використовуємо Spring Tool Suite (STS).

  • у [1] ми імпортуємо один із підручників із [spring.io/guides];
  • у [2] ми обираємо підручник [Accessing Data Jpa], який демонструє, як отримати доступ до бази даних за допомогою Spring Data;
  • у [3] вибирається проект, налаштований за допомогою Maven;
  • у [4] підручник можна отримати у двох формах: [initial] — це порожня версія, яку заповнюють, слідуючи підручнику, або [complete] — це остаточна версія підручника. Ми обираємо останню;
  • у [5] можна вибрати перегляд посібника у браузері;
  • у [6] — кінцевий проект.

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

Залежності Maven для проекту налаштовуються у файлі [pom.xml]:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- використовувати UTF-8 для всього -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • рядки 5–9: визначають батьківський проект Maven. Саме він визначає основну частину залежностей проекту. Їх може бути достатньо, і в цьому випадку додаткові залежності не додаються, або ж їх може не вистачати, і в цьому випадку додаються відсутні залежності;
  • рядки 12–15: визначають залежність від [spring-boot-starter-data-jpa]. Цей артефакт містить класи Spring Data;
  • рядки 16–19: визначають залежність від SGBD та H2, що дозволяють створювати та керувати базами даних у пам’яті.

Розглянемо класи, що надаються цими залежностями:

Їх дуже багато:

  • деякі належать до екосистеми Spring (ті, що починаються з spring);
  • інші належать до екосистеми Hibernate (hibernate, jboss), реалізацію якої ми тут використовуємо — JPA;
  • ще інші — це бібліотеки тестів (junit, hamcrest);
  • ще інші — бібліотеки логів (log4j, logback, slf4j);

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

У рядку 26 файлу [pom.xml] міститься такий рядок:


<start-class>hello.Application</start-class>

Цей рядок пов’язаний із такими рядками:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

У рядках 6–9 плагін [spring-boot-maven-plugin] дозволяє згенерувати виконуваний JAR-файл додатка. Рядок 26 файлу [pom.xml] позначає виконуваний клас цього JAR-файлу.

11.2.2. Рівень [JPA]

Доступ до бази даних здійснюється через шар [JPA], Java Persistence API:

  

Додаток є простим і обслуговує клієнтів [Customer]. Клас [Customer] входить до складу шару [JPA] і має такий вигляд:


package hello;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }

}

Клієнт має ідентифікатор [id], ім’я [firstName] та прізвище [lastName]. Кожен екземпляр [Customer] представляє рядок таблиці бази даних.

  • рядок 8: анотація JPA, яка визначає, що збереження екземплярів [Customer] (Create, Read, Update, Delete) буде здійснюватися за допомогою реалізації JPA. Згідно з залежностями Maven, бачимо, що використовується реалізація JPA / Hibernate;
  • рядки 11–12: анотації JPA, які пов’язують поле [id] із первинним ключем таблиці [Customer]. Рядок 12 вказує, що реалізація JPA використовуватиме метод генерації первинного ключа, властивий використовуваному SGBD, у даному випадку H2;

Інших анотацій JPA немає. У такому разі будуть використані значення за замовчуванням:

  • таблиця [Customer] матиме назву класу, тобто [Customer];
  • стовпці цієї таблиці матимуть назви полів класу: [id, firstName, lastName], враховуючи, що у назві стовпця таблиці не враховується регістр;

Слід зауважити, що використовувана реалізація JPA ніде не називається.

11.2.3. Рівень [Spring Data]

Клас [CustomerRepository] реалізує рівень доступу до таблиці [Customer]. Його код такий:

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Отже, це інтерфейс, а не клас (рядок 7). Він розширює інтерфейс [CrudRepository], інтерфейс Spring Data (рядок 5). Цей інтерфейс має два параметри: перший — тип керованих елементів, у даному випадку тип [Customer], другий — тип первинного ключа керованих елементів, у даному випадку тип [Long]. Інтерфейс [CrudRepository] має такий вигляд:


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

Цей інтерфейс визначає операції CRUD (Create – Read – Update – Delete), які можна виконувати над типом JPA T:

  • рядок 8: метод save дозволяє зберегти суть T у базі даних. Він зберігає суть із первинним ключем, який їй присвоїв SGBD. Він також дозволяє оновити суть T, ідентифіковану за її первинним ключем id. Вибір тієї чи іншої дії залежить від значення первинного ключа id: якщо його значення дорівнює null, виконується операція збереження, в іншому випадку — операція оновлення;
  • рядок 10: те саме, але для списку сутностей;
  • рядок 12: метод findOne дозволяє знайти суть T, ідентифіковану за її первинним ключем id;
  • рядок 22: метод delete дозволяє видалити суть T, ідентифіковану за її первинним ключем id;
  • рядки 24–28: варіанти методу [delete];
  • рядок 16: метод [findAll] дозволяє знайти всі збережені об’єкти T;
  • рядок 18: те саме, але обмежено сутностями, для яких передано список ідентифікаторів;

Повернемося до інтерфейсу [CustomerRepository]:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • рядок 9 дозволяє знайти [Customer] за його назвою [lastName];

І це все щодо шару [DAO]. Класу реалізації попереднього інтерфейсу немає. Він генерується під час виконання за допомогою [Spring Data]. Методи інтерфейсу [CrudRepository] реалізуються автоматично. Щодо методів, доданих до інтерфейсу [CustomerRepository], то тут все залежить від конкретного випадку. Повернемося до визначення [Customer]:


private long id;
private String firstName;
private String lastName;

Метод у рядку 9 реалізується автоматично класом [Spring Data], оскільки він посилається на поле [lastName] (рядок 3) класу [Customer]. Коли Spring Data зустрічає метод [findBySomething] в інтерфейсі, що підлягає реалізації, він реалізує його за допомогою такого запиту JPQL (Java Persistence Query Language):

select t from T t where t.something=:value

Отже, тип T повинен мати поле з іменем [something]. Таким чином, метод

List<Customer> findByLastName(String lastName);

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

return [em].createQuery("select c from Customer c where  c.lastName=:value").setParameter("value",lastName).getResultList()

де [em] позначає контекст збереження JPA. Це можливо лише за умови, що клас [Customer] має поле з іменем [lastName], що й є у даному випадку.

Отже, у простих випадках Spring Data дозволяє нам реалізувати шар [DAO] за допомогою простого інтерфейсу.

11.2.4. Рівень [console]

  

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


package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired
    CustomerRepository repository;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Override
    public void run(String... strings) throws Exception {
        // зберегти кількох клієнтів
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // завантажити всіх клієнтів
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // отримати даних про окремого клієнта за ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // отримати клієнтів за прізвищем
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }

}
  • рядок 9: клас реалізує інтерфейс [CommandLineRunner], який є інтерфейсом [Spring Boot] (рядок 4). Цей інтерфейс має лише один метод, зазначений у рядку 19;
  • рядок 8: @SpringBootApplication — це анотація, що об’єднує кілька анотацій [Spring Boot]:
    • @Configuration: вказує, що клас є класом конфігурації;
    • @EnableAutoConfiguration: вимагає від [Spring Boot] самостійно створити певну кількість бінів на основі різних властивостей, зокрема вмісту Classpath проекту. Оскільки бібліотеки Hibernate знаходяться у Classpath, бін [entityManagerFactory] буде реалізовано з використанням Hibernate. Оскільки бібліотека SGBD H2 знаходиться в Classpath, bean [dataSource] буде реалізовано з використанням H2. У біні [dataSource] також потрібно вказати ім’я користувача та його пароль. У цьому випадку Spring Boot використовуватиме адміністратора за замовчуванням з H2, який не має пароля. Оскільки бібліотека [spring-tx] знаходиться у Classpath, буде використовуватися менеджер транзакцій Spring;
    • @EnableWebMvc: якщо у Classpath знаходиться бібліотека [spring-mvc]. У цьому випадку для веб-додатка виконується автоконфігурація;
    • @ComponentScan: вказує Spring, де шукати інші біни, конфігурації та сервіси. Тут їх за замовчуванням шукають у пакеті, що містить позначений клас, тобто в пакеті [hello]. Таким чином, будуть знайдені класи [Customer] та [CustomerRepository]. Оскільки перший має анотацію [@Entity], він буде каталогізований як об’єкт, що має управлятися Hibernate. Оскільки другий розширює інтерфейс [CrudRepository], він буде зареєстрований як бін Spring;
  • рядки 11–12: bean [CustomerRepository] вводиться в код головного класу;
  • рядок 15: виконується статичний метод [run] класу [SpringApplication] проекту Spring Boot. Його параметром є клас, що має анотацію [Configuration] або [EnableAutoConfiguration]. Після цього відбудеться все, що було описано раніше. Результатом є контекст додатка Spring, тобто набір бінів, що керуються Spring;

Наступні операції лише використовують методи біна, що реалізує інтерфейс [CustomerRepository]. Результати у консолі такі:

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

2015-03-10 15:35:43.661  INFO 5784 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 5784 (started by ST in C:\Users\Serge Tahé\Documents\workspace-sts-3.6.3.RELEASE\gs-accessing-data-jpa-complete)
2015-03-10 15:35:43.708  INFO 5784 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:45.230  INFO 5784 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:45.254  INFO 5784 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-10 15:35:45.331  INFO 5784 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-10 15:35:45.332  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-10 15:35:45.334  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-10 15:35:45.651  INFO 5784 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-10 15:35:45.754  INFO 5784 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2015-03-10 15:35:45.877  INFO 5784 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-10 15:35:46.154  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:46.169  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2015-03-10 15:35:46.779  INFO 5784 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2015-03-10 15:35:47.040  INFO 5784 --- [           main] hello.Application                        : Started Application in 3.623 seconds (JVM running for 4.324)
2015-03-10 15:35:47.042  INFO 5784 --- [       Thread-1] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:47.044  INFO 5784 --- [       Thread-1] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2015-03-10 15:35:47.046  INFO 5784 --- [       Thread-1] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:47.047  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:47.051  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • рядки 1–8: логотип проєкту Spring Boot;
  • рядок 9: виконується клас [hello.Application];
  • рядок 10: [AnnotationConfigApplicationContext] — це клас, що реалізує інтерфейс [ApplicationContext] від Spring. Це контейнер бінів;
  • рядок 11: бін [entityManagerFactory] реалізовано за допомогою класу [LocalContainerEntityManagerFactory], класу Spring;
  • рядок 12: з’являється [Hibernate]. Саме ця реалізація JPA була обрана;
  • рядок 19: діалектом Hibernate є варіант SQL, який слід використовувати разом із SGBD. Тут діалект [H2Dialect] вказує, що Hibernate працюватиме з SGBD та H2;
  • рядки 21–22: база даних створена. Створено таблицю [CUSTOMER]. Це означає, що Hibernate було налаштовано на генерацію таблиць на основі визначень JPA, у даному випадку — визначення JPA класу [Customer];
  • рядки 26–30: результат виклику методу [findAll] інтерфейсу;
  • рядок 34: результат методу [findOne] інтерфейсу;
  • рядки 38–39: результати методу [findByLastName];
  • рядки 41 і наступні: журнали закриття контексту Spring.

11.2.5. Ручна конфігурація проєкту Spring Data

Ми дублюємо попередній проєкт у проєкті [gs-accessing-data-jpa-02]:

  

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

Спочатку ми вкажемо необхідні залежності у файлі [pom.xml]:


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

    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa-02</artifactId>
    <version>0.1.0</version>

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

    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- База даних H2 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- використовуйте UTF-8 для всього -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>

</project>
  • рядки 10–14: батьківський проект Maven, бібліотеки якого ми будемо використовувати;
  • рядки 18–21: Spring Data, що використовується для доступу до бази даних;
  • рядки 23–26: реалізація Hibernate специфікації JPA;
  • рядки 28–31: SGBD H2;
  • рядки 33–36: бази даних часто використовуються з пулами відкритих з’єднань, що дозволяє уникнути багаторазового відкриття та закриття з’єднань. У даному випадку використовується реалізація [tomcat-jdbc];

У новому проєкті суть [Customer] та інтерфейс [CustomerRepository] залишаються без змін. Ми змінимо клас [Application], який буде розділено на два класи:

  • [Config], який буде класом конфігурації;
  • [Main], який стане класом виконання;
  

Виконувальний клас [Application] тепер має такий вигляд:


package console;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import repositories.CustomerRepository;
import config.AppConfig;
import entities.Customer;

public class Application {
    public static void main(String[] args) {
        // ініціалізація контексту Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // збереження даних кількох клієнтів
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        ...

        // закриття контексту
        context.close();
    }
}
  • рядок 9: клас [Application] більше не має анотацій конфігурації;
  • рядки 3–7: зверніть увагу, що імпорт пакетів [Spring Boot] більше відсутній;
  • рядок 12: створюються екземпляри бінів Spring. Отримується контекст Spring, який містить посилання на створені біни;
  • рядок 13: запитується посилання на бін типу [CustomerRepository];

Клас [Config] , який налаштовує проєкт, має такий вигляд:


package config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

//@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "repositories" })
@Configuration
// @ComponentScan(basePackages={"package1","package2"})
public class AppConfig {

    // база даних H2
    @Bean
    public DataSource dataSource() {
        // джерело даних TomcatJdbc
        DataSource dataSource = new DataSource();
        // конфігурація доступу JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // спочатку відкрите з'єднання
        dataSource.setInitialSize(1);
        // результат
        return dataSource;
    }

    // провайдер JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Менеджер транзакцій
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • рядок 17: анотація [@EnableTransactionManagement] вказує, що методи інтерфейсів [CrudRepository] повинні виконуватися в межах транзакції. Вона винесена в коментарі, оскільки це є стандартним налаштуванням;
  • рядок 18: анотація [@EnableJpaRepositories] дозволяє вказати папки, в яких знаходяться інтерфейси Spring Data [CrudRepository]. Ці інтерфейси стануть компонентами Spring і будуть доступні в його контексті;
  • рядок 19: анотація [@Configuration] перетворює клас [Config] на клас конфігурації Spring;
  • рядок 20: анотація [@ComponentScan] дозволяє вказати папки, в яких слід шукати компоненти Spring. Компоненти Spring — це класи, позначені анотаціями Spring, такими як @Service, @Component, @Controller тощо. Тут немає інших компонентів, крім тих, що визначені в класі [AppConfig], тому анотацію було виведено в коментарі;
  • рядки 24–37: визначають джерело даних — базу даних H2. Саме анотація @Bean у рядку 25 робить об’єкт, створений цим методом, компонентом, що керується Spring. Назва методу тут може бути будь-якою. Однак він повинен називатися [dataSource], якщо EntityManagerFactory у рядку 51 відсутній і визначається шляхом автоконфігурації;
  • рядок 30: база даних матиме назву [demo] і буде згенерована в папці проєкту;
  • рядки 40–47: визначають використовувану реалізацію JPA, у даному випадку реалізацію Hibernate. Назва методу тут може бути будь-якою;
  • рядок 43: відсутність логів SQL;
  • рядок 44: база даних буде створена, якщо її ще немає;
  • рядки 50–58: визначають EntityManagerFactory, який буде керувати збереженням даних JPA. Метод обов’язково має називатися [entityManagerFactory];
  • рядок 51: метод отримує два параметри, що мають типи двох раніше визначених бінів. Вони будуть створені, а потім введені Spring як параметри методу;
  • рядок 53: визначає реалізацію JPA, що використовується;
  • рядок 54: вказує папки, де знаходяться сутності JPA;
  • рядок 55: визначає джерело даних, що підлягають обробці;
  • рядки 61–66: менеджер транзакцій. Метод обов’язково має називатися [transactionManager]. Він отримує як параметр бін із рядків 51–58;
  • рядок 64: менеджер транзакцій пов’язаний з EntityManagerFactory;

Вищезазначені методи можна визначати в будь-якому порядку.

Виконання проєкту дає ті самі результати. У папці проєкту з’являється новий файл — файл бази даних H2:

  

11.2.6. Створення виконуваного архіву

Щоб створити виконуваний архів проекту, можна виконати такі дії:

  • у [1]: створюється конфігурація виконання;
  • у [2]: типу [Java Application]
  • у [3]: вказується проект, який потрібно виконати (використовуйте кнопку Browse);
  • у [4]: вказує клас, який потрібно виконати;
  • у [5]: назва конфігурації виконання — може бути будь-якою;
  • у [6]: проект експортується;
  • у [7]: у вигляді виконуваного архіву JAR;
  • в [8]: вказує шлях та ім'я виконуваного файлу, що створюється;
  • у [9]: ім’я конфігурації виконання, створеної у [5];
  • у [10] — створений архів;

Після цього відкриваємо консоль у папці, що містить виконуваний архів:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-02.jar

Архів запускається наступним чином:


.....\dist>java -jar gs-accessing-data-jpa-02.jar

У консолі з’являються такі результати:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder для отримання додаткової інформації.
mars 10, 2015 5:27:20 PM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
mars 10, 2015 5:27:20 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.8.Final}
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
mars 10, 2015 5:27:22 PM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
mars 10, 2015 5:27:22 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
mars 10, 2015 5:27:22 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

11.3. Приклад 2

11.3.1. Вступ

Ми повернемося до прикладу таблиці товарів, яку ми використовували для ознайомлення з API JDBC, і створимо таку архітектуру:

База даних [dbintrospringjpa] містить дві таблиці: [PRODUITS] та [CATEGORIES]. Таблиця [CATEGORIES] має такий вигляд:

 
  • [ID]: первинний ключ у режимі AUTO_INCREMENT;
  • [VERSION]: номер версії запису;
  • [NOM]: назва категорії — унікальна;

Таблиця [PRODUITS] має такий вигляд:

 
  • [ID]: первинний ключ у режимі AUTO_INCREMENT;
  • [VERSION]: номер версії запису;
  • [NOM]: назва товару — унікальна;
  • [ID_CATEGORIE]: номер категорії — зовнішній ключ у полі [CATEGORIES.ID];
  • [PRIX]: його ціна;
  • [DESCRIPTION]: опис товару;

Завдання: створіть базу даних [dbintrospringdata] за допомогою скрипту SQL [dbintrospringdata.sql] з документації:


11.3.2. Створення проекту Maven

Щоб створити каркас проєкту Spring Data, можна вчинити наступним чином:

  • у [1] створюється новий проєкт;
  • у [2]: типу [Spring Starter Project];
  • згенерований проєкт буде проєктом Maven. У [3] вказуємо назву групи проєкту;
  • у [4]: вказуємо назву артефакту (у даному випадку — jar-файл), який буде створено під час компіляції проєкту;
  • у полі [5]: вказується ім’я проекту в Eclipse — воно може бути будь-яким (не обов’язково збігатися з [4]);
  • у полі [7]: вказується, що буде створено проект із шаром [JPA], який містить SGBD та MySQL. Необхідні для такого проєкту залежності будуть включені до файлу [pom.xml];
  • у [8] вкажіть назву папки проекту;
  • у [9] завершіть роботу майстра;
  • у [10]: створений проект;

Файл [pom.xml] містить необхідні залежності для проекту JPA:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

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

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.IntroSpringData01Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • рядки 14–19: батьківський проект Maven — визначає велику кількість бібліотек із зазначенням їхніх версій — ці бібліотеки використовуються як залежності Maven без вказівки їхніх версій;
  • рядки 28–31: необхідна залежність для JPA — включатиме [Spring Data];
  • рядки 32–36: залежність JDBC від драйвера MySQL;
  • рядки 37–41: необхідні залежності для тестів JUnit, інтегрованих із Spring;

Виконуваний клас [Application] нічого не робить, але попередньо налаштований:


package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class IntroSpringData01Application {

    public static void main(String[] args) {
        SpringApplication.run(IntroSpringData01Application.class, args);
    }
}
  • анотація [@SpringBootApplication] робить цей клас класом автоконфігурації проєкту;

Тестовий клас [ApplicationTests] нічого не робить, але попередньо налаштований:


package demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = IntroSpringData01Application.class)
public class IntroSpringData01ApplicationTests {

    @Test
    public void contextLoads() {
    }

}
  • рядок 9: анотація [@SpringApplicationConfiguration] дозволяє використовувати файл конфігурації [Application]. Таким чином, тестовий клас отримає доступ до всіх бінів, які будуть визначені цим файлом;
  • рядок 8: анотація [@RunWith] забезпечує інтеграцію Spring із JUnit: клас можна буде виконати як тест JUnit. [@RunWith] є анотацією JUnit (рядок 4), тоді як клас [SpringJUnit4ClassRunner] є класом Spring (рядок 6);

Тепер, коли ми маємо каркас додатка JPA, можемо доповнити його, щоб написати проект рівня персистентності, пов’язаного з базою даних продуктів.

11.3.3. Проєкт Eclipse

Ми модифікуємо попередній проект наступним чином:

  
  • [AppConfig.java]: клас конфігурації проекту Spring;
  • [Main.java] — виконуваний клас проекту;
  • [IDao.java] — інтерфейс шару [DAO];
  • [Dao.java]: клас реалізації шару [DAO];
  • [AbstractEntity.java]: батьківський клас для класів [Produit] та [Categorie];
  • [Produit.java]: клас, пов’язаний із рядком таблиці [PRODUITS] у базі даних;
  • [Categorie.java]: клас, пов’язаний із рядком таблиці [CATEGORIES] у базі даних;
  • [ProduitsRepository]: інтерфейс Spring Data для доступу до таблиці [PRODUITS];
  • [CategoriesRepository]: інтерфейс Spring Data для доступу до таблиці [CATEGORIES];
  • [pom.xml]: файл конфігурації проекту Maven;

Цей проєкт реалізує таку архітектуру:

Рівень [DAO] бачить лише рівень, реалізований [Spring Data].

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

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


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

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

    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- MySQL База даних -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- бібліотека jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Тестування Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- бібліотека логів -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

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

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

</project>

Ця конфігурація є тією, що використовується та пояснюється в розділі 11.2.5. До неї додаємо такі бібліотеки:

  • рядки 42–49: бібліотека jSON, яка використовується методом [toString] класу [Produit];
  • рядки 51–55: бібліотека [Google Guava], яка надає допоміжні методи для управління колекціями елементів. Вона буде використовуватися класом [Dao], який реалізує рівень [DAO];
  • рядки 56–67: бібліотеки, необхідні для тестування JUnit;
  • рядки 69–72: бібліотека логів;
  • рядки 81–86: плагіни Maven, необхідні для проекту;

11.3.5. Елементи шару [JPA]

Рівень

[DAO]

Рівень

[console]

Рівень

[JPA]

Пілот

[JDBC]

Шар

[Spring Data]

Spring 4

SGBD

  

11.3.5.1. Клас [AbstractEntity]

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


package spring.data.entities;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

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

@MappedSuperclass
public abstract class AbstractEntity {
    // властивості
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;

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

    }

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

    // перевизначення [equals] та [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id.longValue() == other.id.longValue();
    }

    // підпис jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // методи getter та setter
....
}

Цей клас призначений для забезпечення батьківського класу для сутностей JPA шляхом інкапсуляції в одному місці властивостей [id, version] (рядки 19, 22), спільні для обох сутностей [Produit] та [Categorie], пов’язаних із базою даних. Ці властивості пов’язані зі стовпцями [ID, VERSION] таблиць (рядки 18, 21).

  • рядок 13: анотація [@MappedSuperclass] вказує, що клас є батьківським класом об’єктів JPA;
  • рядок 16: анотація [@Id] вказує, що поле [id] (воно може мати іншу назву) пов’язане з первинним ключем таблиці;
  • рядок 17: анотація [@GeneratedValue(strategy=GenerationType.IDENTITY)] визначає режим генерації первинних ключів. Режим [GenerationType.IDENTITY] разом із MySQL використовуватиме режим [AUTO_INCREMENT]. З іншим SGBD цей режим використовував би інший метод. Перевага полягає в тому, що розробнику не потрібно про це турбуватися, і його код залишається дійсним незалежно від того, який SGBD використовується;
  • рядок 18: анотація [@Column] вказує стовпець, пов’язаний із полем. Якщо ця анотація відсутня, JPA припускає, що стовпець має таку саму назву, як і поле. Саме так і є в цьому випадку. Отже, цю анотацію можна було б не вказувати;
  • рядок 20: анотація [@Version] вказує, що поле [version] пов’язане зі стовпцем версій. Реалізація JPA буде збільшувати цей номер версії щоразу, коли суть буде змінена. Цей номер слугує для запобігання одночасному оновленню сутності двома різними користувачами: два користувачі U1 та U2 читають сутність E з номером версії, що дорівнює V1. U1 змінює E та зберігає цю зміну в базі даних: номер версії тоді змінюється на V1+1. U2, у свою чергу, змінює E та зберігає цю зміну в базі даних: він отримає виняток, оскільки має версію (V1), відмінну від тієї, що є в базі даних (V1+1);
  • рядки 35–52: перевизначення методів [hashCode] та [equals]. За замовчуванням значення [obj1.equals(obj2)] дорівнює true, якщо [obj1==obj2], тобто якщо ob1 та obj2 — два однакові покажчики. Якщо потрібно порівнювати об’єкти, на які вказують покажчики, а не самі покажчики, слід перевизначити методи [equals] та [hashCode]. Останній повинен повертати однакове значення для двох об’єктів, які метод [equals] визнає рівними;
  • рядки 42–51: два об’єкти типу [AbstractEntity] або похідних типів вважатимуться рівними, якщо їхні первинні ключі [id] рівні;
  • рядки 35–38: метод [hashCode] дійсно повертає однакове значення для двох однакових об’єктів [AbstractEntity], які, отже, мають однаковий первинний ключ [id];
  • рядки 55–63: метод [toString] повертає рядок jSon об’єкта [this]. Якщо цей об’єкт позначає дочірній клас, то цей метод поверне рядок jSON цього дочірнього класу. Це позбавляє нас необхідності створювати метод [toString] у дочірніх класах;

11.3.5.2. Суть JPA [Produit]

Клас [Produit] є сутністю JPA, пов’язаною з рядком таблиці [PRODUITS]:

 

package spring.data.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFilter;

@Entity
@Table(name = "PRODUITS")
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {

    // властивості
    @Column(name = "NOM")
    private String nom;

    @Column(name = "CATEGORIE_ID", insertable = false, updatable = false)    
    private Long idCategorie;

    @Column(name = "PRIX")
    private double prix;

    @Column(name = "DESCRIPTION")
    private String description;

    // категорія
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;

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

    }

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

    // методи getter та setter
...
}
  • рядок 12: анотація [@Entity] визначає клас [Produit] як об’єкт, що керується шаром [JPA];
  • рядок 13: анотація [@Table(name = "PRODUITS")] вказує, що клас [Produit] є об’єктом-зображенням одного рядка таблиці [PRODUITS] у базі даних;
  • рядок 14: ім’я фільтра jSON, який слід застосувати до об’єкта. Ми побачимо, що властивість [categorie] із рядка 13 не завжди доступна. Тоді його потрібно виключити з представлення об’єкта jSON. Для цього нам потрібен фільтр. Отже, саме у фільтрі з назвою [jsonFilterCategorie] ми вкажемо, чи хочемо ми, щоб властивість [categorie] була присутня;
  • рядок 18: анотація [@Column] пов’язує поле [nom] зі стовпцем [NOM] таблиці [PRODUITS]. Якщо поле має таку саму назву, як і пов’язаний стовпець, анотацію [@Column] можна опустити. У даному випадку це саме так;
  • рядки 31–33: категорія товару;
  • рядок 31: анотація [@ManyToOne] вказує, що стовпець анотації рядка 32 [@JoinColumn(name = "CATEGORIE_ID")] є зовнішнім ключем таблиці [PRODUITS]сутності [Produit] у таблиці [CATEGORIES], пов’язаній із сутністю у рядку 33. Ця анотація має анотувати сутність JPA. Отже, клас у рядку 33 має бути об’єктом JPA;
  • рядок 31: анотація [fetch = FetchType.LAZY] вимагає, щоб під час вилучення продукту з таблиці [PRODUITS] його категорія (рядок 33) не вилучалася одразу (lazy loading). Вона отримується під час першого виклику методу [getCategorie]. Цей атрибут не є обов’язковим. Використовувана реалізація JPA має право ігнорувати його. Саме тому, що властивість [categorie] може бути присутньою або відсутньою, ми ввели фільтр jSON у рядку 14. Існуючі реалізації JPA (Hibernate, Eclipselink, OpenJPA) обробляють цю анотацію по-різному. Hibernate розширює початковий метод [getCategorie] (який обмежується поверненням поля categorie) за допомогою виклику SGBD для отримання категорії. Щоб це було можливо, з’єднання з SGBD, яке спочатку використовувалося для отримання товару, має залишатися відкритим, інакше виникне виняток.

11.3.5.3. Об’єкт JPA [Categorie]

Клас [Categorie] — це суть JPA, пов’язана з рядком таблиці [CATEGORIES]:

 

Її код такий:


package spring.data.entities;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFilter;

@Entity
@Table(name = "CATEGORIES")
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {

    // властивості
    @Column(name = "NOM")
    private String nom;

    // супутні товари
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();

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

    }

    public Categorie(String nom) {
        this.nom = nom;
    }

    // методи
    public void addProduit(Produit produit) {
        // додаємо товар
        produits.add(produit);
        // визначаємо його категорію
        produit.setCategorie(this);
    }

    // гетери та сеттери
...
}
  • рядки 21–22: назва категорії;
  • рядки 25–26: товари цієї категорії;
  • рядок 25: анотація [@OneToMany] є оберненим відношенням до відношення [@ManyToOne], яке ми зустріли в об’єкті [Produit]. Атрибут [mappedBy = "categorie"] вказує на поле сутності [Produit], яке має анотацію у вигляді зворотного зв’язку [@ManyToOne]. Атрибут [cascade = { CascadeType.ALL }] вимагає, щоб операції (persist, merge, remove), виконані над @Entity [Categorie], поширювалися каскадно на [produits] у рядку 26. Можна вказати часткові каскадні дії за допомогою констант [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
  • рядок 25: атрибут [fetch = FetchType.LAZY] вимагає, щоб під час вилучення категорії з таблиці [CATEGORIES] її товари не вилучалися одразу. Вони будуть вилучені під час першого виклику методу [getProduits]. Існуючі реалізації JPA (Hibernate, Eclipselink, OpenJPA) обробляють цю анотацію по-різному. Hibernate розширює початковий метод [getProduits] (який обмежується поверненням поля produits) викликом методу SGBD для отримання товарів категорії. Щоб це було можливо, з’єднання з SGBD, яке спочатку використовувалося для отримання категорії, має залишатися відкритим. Цей атрибут є обов’язковим. Реалізація JPA не може його ігнорувати. Оскільки властивість [produits] може бути ініціалізована або ні, ми ввели фільтр jSON у рядку 17, який дозволить нам вказати, чи потрібна нам ця властивість;
  • рядок 26: тип [Set] є інтерфейсом. Тип [HashSet] — це клас, що реалізує цей інтерфейс. Він реалізує колекцію елементів під назвою ensemble. Колекція не може містити два однакові об’єкти. У даному випадку об’єкти мають тип [Produit]. Отже, у колекції не може бути двох однакових об’єктів. Оскільки метод [equals] батьківського класу [AbstractEntity] було перевизначено так, щоб вважати два товари однаковими, якщо вони мають однаковий первинний ключ, то поле [produits] не може містити два товари з однаковим первинним ключем;
  • рядки 38–43: метод [addProduit] дозволяє додати товар до категорії;

11.3.6. Рівень [Spring Data]

Рівень

[DAO]

Рівень

[console]

Шар

[JPA]

Пілот

[JDBC]

Шар

[Spring Data]

Весна 4

SGBD

  

Інтерфейс [CategoriesRepository] керує доступом до таблиці [CATEGORIES]:


package spring.data.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.data.entities.Categorie;

public interface CategoriesRepository extends CrudRepository<Categorie, Long> {

    // категорія з її товарами
    @Query("select c from Categorie c left join fetch c.produits p where c.id=?1")
    public Categorie getCategorieByIdWithProduits(Long id);

    @Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
    public Categorie getCategorieByNameWithProduits(String nom);

    // категорія без товарів, позначена за назвою
    public Categorie findByNom(String nom);
}
  • рядок 8: інтерфейс [CrudRepository] було використано та пояснено в розділі 11.2.3. Нагадуємо, що:
    • перший тип інтерфейсу — це об’єкт JPA, який керується для операцій доступу CRUD (findOne, findAll, збереження, видалення, deleteAll),
    • другий тип — це первинний ключ сутності JPA, у даному випадку ціле число [Long];
  • рядок 12: метод у рядку 12 реалізовано за допомогою запиту JPQL (Java Persistence Query Language) з рядка 11. Цей запит стосується сутностей JPA. У такому запиті:
    • таблиці замінюються відповідними сутностями JPA;
    • стовпці замінюються на поля сутностей JPA, що використовуються у запиті;
  • рядок 11: запит JPQL повертає категорію разом із її товарами. Нагадаємо, що в сутності [Categorie] поле [produits] мало атрибут [fetch = FetchType.LAZY] (відкладене завантаження). У запиті JPQL ми примусово завантажуємо товари за допомогою ключового слова [fetch]. Параметр ?1 запиту під час виконання буде замінено значенням першого параметра методу в рядку 12, тобто параметром [Long id];
  • рядки 14–15: аналогічний метод для категорії, що ідентифікується за назвою;
  • рядок 18: метод [findByNom] буде автоматично реалізовано методом [Spring Data], оскільки тип [Category] має поле [nom];

Інтерфейс [ProduitsRepository] керує доступом до таблиці [PRODUITS]:


package spring.data.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.data.entities.Produit;

public interface ProduitsRepository extends CrudRepository<Produit, Long> {

    // товар із категорією
    @Query("select p from Produit p left join fetch p.categorie c where p.id=?1")
    public Produit getProduitByIdWithCategorie(Long id);

    @Query("select p from Produit p left join fetch p.categorie c where p.nom=?1")
    public Produit getProduitByNameWithCategorie(String nom);

    // товар без категорії, позначений за назвою
    public Produit findByNom(String nom);
}

Пояснення такі самі, як і для інтерфейсу [CategoriesRepository].

Ці інтерфейси будуть реалізовані класами, згенерованими [Spring Data] під час виконання проекту. Такі класи називаються [proxy]. За замовчуванням методи класу реалізації виконуються в транзакції. Те, що ці інтерфейси успадковують клас [CrudRepository], робить їх компонентами Spring.

11.3.7. Рівень [DAO]

Рівень

[DAO]

Рівень

[console]

Шар

[JPA]

Пілот

[JDBC]

Шар

[Spring Data]

Весна 4

SGBD

  

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


package spring.data.dao;

import java.util.List;

import spring.data.entities.Categorie;
import spring.data.entities.Produit;

public interface IDao {

    // вставлення списку товарів
    public List<Produit> addProduits(List<Produit> produits);

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

    // оновлення списку товарів
    public List<Produit> updateProduits(List<Produit> produits);

    // отримання всіх товарів
    public List<Produit> getAllProduits();

    // додавання списку категорій
    public List<Categorie> addCategories(List<Categorie> categories);

    // видалення всіх категорій
    public void deleteAllCategories();

    // оновлення списку категорій
    public List<Categorie> updateCategories(List<Categorie> categories);

    // отримання всіх категорій
    public List<Categorie> getAllCategories();

    // конкретний товар із категорією або без неї
    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // отримання певної категорії з товарами або без них
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);
}

Тут було прийнято правило, згідно з яким будь-який метод, що змінює об’єкти, які він має у якості вхідних параметрів, повинен повертати їх у своєму результаті. Причина цього правила була пояснена у параграфі 4.2: воно дозволяє шару та його клієнту перебувати у двох окремих JVM і, отже, працювати у режимі «клієнт/сервер».

Реалізація [Dao] цього інтерфейсу виглядає наступним чином:


package spring.data.dao;

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

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

import com.google.common.collect.Lists;

import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;

@Component
public class Dao implements IDao {

    @Autowired
    private ProduitsRepository produitsRepository;

    @Autowired
    private CategoriesRepository categoriesRepository;

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(101, getMessagesForException(e));
        }
    }

    @Override
    public void deleteAllProduits() {
        try {
            produitsRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(102, getMessagesForException(e));
        }
    }

    @Override
    public List<Produit> updateProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(103, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(104, getMessagesForException(e));
        }
    }

    @Override
    public void deleteAllCategories() {
        try {
            categoriesRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(105, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> updateCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(106, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> getAllCategories() {
        try {
            return Lists.newArrayList(categoriesRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(107, getMessagesForException(e));
        }
    }

    @Override
    public List<Produit> getAllProduits() {
        try {
            return Lists.newArrayList(produitsRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(108, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByIdWithCategorie(Long idProduit) {
        try {
            return produitsRepository.getProduitByIdWithCategorie(idProduit);
        } catch (Exception e) {
            throw new DaoException(109, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByIdWithProduits(Long idCategorie) {
        try {
            return categoriesRepository.getCategorieByIdWithProduits(idCategorie);
        } catch (Exception e) {
            throw new DaoException(110, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        try {
            return categoriesRepository.getCategorieByNameWithProduits(nom);
        } catch (Exception e) {
            throw new DaoException(111, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByNameWithCategorie(String nom) {
        try {
            return produitsRepository.getProduitByNameWithCategorie(nom);
        } catch (Exception e) {
            throw new DaoException(112, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByIdWithoutCategorie(Long idProduit) {
        try {
            return produitsRepository.findOne(idProduit);
        } catch (Exception e) {
            throw new DaoException(113, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
        try {
            return categoriesRepository.findOne(idCategorie);
        } catch (Exception e) {
            throw new DaoException(114, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByNameWithoutCategorie(String nom) {
        try {
            return produitsRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(115, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByNameWithoutProduits(String nom) {
        try {
            return categoriesRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(116, getMessagesForException(e));
        }
    }

}
  • рядок 16: анотація [@Component] робить клас [Dao] компонентом Spring;
  • рядки 19–23: введення посилань на обидва інтерфейси [CrudRepository] з [Spring Data]. Це введення відбуватиметься під час інстанціювання об’єктів Spring, як правило, на початку виконання проекту Spring;
  • у рядках 28 і 46 слід звернути увагу, що метод [save] інтерфейсу [produitsRepository] використовується як для вставки, так і для оновлення продуктів. [Spring Data] використовує первинний ключ товару, щоб визначити, чи потрібно виконати вставку, чи оновлення. Якщо первинний ключ дорівнює [null], це буде вставка, інакше — оновлення;
  • рядок 82: використовується метод [Lists.newArrayList] з бібліотеки Guava для отримання списку товарів. Метод [produitsRepository.findAll()] повертає тип [Iterable<Produit>];
  • рядок 28: метод [produitsRepository.save(produits)] повертає тип [Iterable<Produit>]. Те саме стосується й інших операцій [save] цього класу;

У наведеному вище класі [Dao] винятки, що можуть виникнути, інкапсульовані в наступний тип [DaoException]:


package spring.data.dao;

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

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

public class DaoException extends RuntimeException implements Serializable {

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

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

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

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

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

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

    public DaoException(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;
    }

    // гетери та сеттери
...
}
  • рядок 10: клас походить від класу [RuntimeException] і, отже, є неконтрольованим винятком;
  • рядок 16: код помилки;
  • рядок 17: список повідомлень про помилки, пов’язаних зі стеком винятків, що спричинили [DaoException];
  • рядки 59–76: приватний метод [getMessagesForException] дозволяє отримати список повідомлень про помилки, пов’язаних із винятками у стеку винятків. Дійсно, можна накопичувати винятки за допомогою таких конструкторів класу Exception:
    • Exception(String message, Throwable cause): створює виняток із повідомленням та винятком, який потрібно інкапсулювати;
    • Exception(Throwable cause): створює виняток із винятком, який потрібно інкапсулювати;

Тип [Throwable] є батьківським класом класу [Exception]. Якщо вищезазначені конструктори виконуються повторно, то кінцеве виключення містить кілька виключень. У такому випадку кажуть, що утворився стек виключень.

  • останню причину винятку e1 отримують за допомогою виразу [e1.getCause()];
  • передостанню причину винятку e1 отримують за допомогою виразу [e1.getCause().getCause()];
  • так продовжуємо, доки не отримаємо [getCause()==null];

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

  

Клас [DaoConfig] налаштовує рівень [DAO]:


package spring.data.config;

import javax.persistence.EntityManagerFactory;

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.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
public class DaoConfig {

    // константи
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroSpringData";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    final static String[] ENTITIES_PACKAGES = { "spring.data.entities" };

    // джерело даних [tomcat-jdbc]
    @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;
    }

    // провайдер JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Менеджер транзакцій
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}

Аналогічна конфігурація була розглянута та пояснена в розділі 11.2.5. Ми додали до неї такі анотації Spring:

  • рядок 17: анотація [@EnableJpaRepositories] використовується для вказівки пакетів, у яких знаходяться інтерфейси [CrudRepository] та [Spring Data];
  • рядок 18: клас є класом конфігурації Spring. Ця інформація є важливою. Якщо її видалити, проєкт працюватиме. Але далі в документі, коли ми будемо створювати проєкти, що базуються на цьому, деякі з них перестануть працювати, якщо анотацію з рядка 18 видалити;
  • рядок 19: анотація [@ComponentScan] вказує пакети, в яких знаходяться об’єкти Spring. Це класи, анотовані [@Component, @Service, @Controller, ...]. Тут буде знайдено та інстанційовано компонент Spring [Dao];
  • рядки 73–76: ми визначили bean, який представляє масив пакетів, що підлягають скануванню для пошуку сутностей JPA. Це дозволить проєкту, який імпортує клас [DaoConfig], перевизначити цей бін і, таким чином, змінити пакети, що скануються (рядок 59). Далі в документі ми зіткнемося з цією проблемою;

Клас [AppConfig] налаштовує весь проєкт:


package spring.data.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

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

@Configuration
@Import({DaoConfig.class})
public class AppConfig {
    // фільтри jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // маппер jSON
        ObjectMapper mapper = new ObjectMapper();
        // фільтри
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // результат
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // маппер jSON
        ObjectMapper mapper = new ObjectMapper();
        // фільтри
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // результат
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // маппер jSON
        ObjectMapper mapper = new ObjectMapper();
        // фільтри
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // результат
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // маппер jSON
        ObjectMapper mapper = new ObjectMapper();
        // фільтри
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // результат
        return mapper;
    }
}
  • рядок 11: цей клас є класом конфігурації Spring;
  • рядок 12: який імпортує біни, визначені класом [DaoConfig], який ми щойно розглянули;
  • шар [console] використовує маппери jSON, які визначені тут;
  • рядки 14–64: визначають п’ять маперів jSON;
  • рядки 15–18: фільтр jSON [jsonMapper] не має фільтрів;
  • рядки 20–30: фільтр jSON [jsonMapperCategorieWithProduits] дозволяє серіалізувати/десеріалізувати об’єкт [Categorie] разом із його похідними;
  • рядки 32–42: фільтр jSON [jsonMapperProduitWithCategorie] дозволяє серіалізувати/десеріалізувати об’єкт [Produit] разом із його категорією;
  • рядки 43–53: фільтр jSON [jsonMapperCategorieWithoutProduits] дозволяє серіалізувати/десеріалізувати об’єкт [Categorie] без його товарів;
  • рядки 55–64: фільтр jSON [jsonMapperProduitWithoutCategorie] дозволяє серіалізувати/десеріалізувати об’єкт [Produit] без його категорії;

Слід зауважити, що при створенні фільтра jSON для сутності T необхідно налаштувати не тільки фільтр сутності T, а й фільтри сутностей Ti, які вона може містити.

11.3.9. Рівень [console]

Рівень

[DAO]

Рівень

[console]

Шар

[JPA]

Пілот

[JDBC]

Шар

[Spring Data]

Spring 4

SGBD

  

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


package spring.data.console;

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

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

public class Main {

    public static void main(String[] args) throws JsonProcessingException {
        AnnotationConfigApplicationContext context = null;
        try {
            // інстанціювання контексту Spring
            context = new AnnotationConfigApplicationContext(AppConfig.class);
            ObjectMapper jsonMapperCategorieWithProduits = context.getBean("jsonMapperCategorieWithProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithCategorie = context.getBean("jsonMapperProduitWithCategorie",
                    ObjectMapper.class);
            ObjectMapper jsonMapperCategorieWithoutProduits = context.getBean("jsonMapperCategorieWithoutProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithoutCategorie = context.getBean("jsonMapperProduitWithoutCategorie",
                    ObjectMapper.class);
            IDao dao = context.getBean(IDao.class);
            // --------------------------------------------------------------------------------------
            // очищення бази даних
            log("Vidage de la base de données", 1);
            // очищення таблиці [CATEGORIES] — за каскадним принципом буде очищено таблицю [PRODUITS]
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // заповнюються таблиці
            List<Categorie> categories = new ArrayList<Categorie>();
            for (int i = 0; i < 2; i++) {
                Categorie categorie = new Categorie(String.format("categorie%d", i));
                for (int j = 0; j < 5; j++) {
                    categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                            String.format("desc%d%d", i, j)));
                }
                categories.add(categorie);
            }
            // додавання категорії — за каскадним принципом товари також будуть вставлені
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // список категорій
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // перелік товарів
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // категорія 1 з її товарами
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // товар [produit14] та його категорія
            Produit p = dao.getProduitByNameWithCategorie("produit14");
            log("Produit [produit14] avec sa catégorie", 2);
            affiche(p, jsonMapperProduitWithCategorie);
            // --------------------------------------------------------------------------------------
            log("Mise à jour du prix des produits de [categorie1]", 1);
            log("Produits de la catégorie [categorie1] avant la mise à jour", 2);
            Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
            Set<Produit> produits = categorie1.getProduits();
            affiche(categorie1, jsonMapperCategorieWithProduits);
            for (Produit produit : produits) {
                produit.setPrix(1.1 * produit.getPrix());
            }
            dao.updateProduits(Lists.newArrayList(produits));
            log("Produits de la catégorie [categorie1] après la mise à jour", 2);
            affiche(dao.getCategorieByNameWithProduits("categorie1"), jsonMapperCategorieWithProduits);
            // --------------------------------------------------------------------------------------
            log("Vidage de la base de données", 1);
            // очищення таблиці [CATEGORIES] — каскадно буде очищено таблицю [PRODUITS]
            dao.deleteAllCategories();
            // відображення бази даних
            log("Liste des categories avant l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits avant l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
            // здійснюється вставка
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // додавання категорії — за принципом каскаду товари також будуть додані
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // перевірка
            log("Liste des categories après l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits après l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
        } catch (DaoException e) {
            System.out.println(e);
        } finally {
            if (context != null) {
                // завершено
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }

    // відображення елемента типу T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // відображення списку елементів типу T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // виводить повідомлення
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }
}
  • рядок 25: створення екземплярів Spring-бінів на основі класу конфігурації [AppConfig];
  • рядки 26–33: отримання посилань на маппери jSON. Використовується такий сигнатур методу [ApplicationContext].getBean:
    • [ApplicationContext].getBean(String id, Class клас): який використовується, коли існує кілька бінів типу [classe]. У цьому випадку вказується ідентифікатор запитуваного біна. Якщо він був визначений за допомогою анотації [@Bean], його ідентифікатор — це назва анотованого методу. Якщо він був визначений за допомогою анотації [@Bean(« identifiant »], його ідентифікатор — це той, що вказано в анотації;
  • рядок 34: отримання посилання на шар [DAO];
  • рядки 37–39: очищення бази даних. Очищається таблиця категорій (рядок 39). Оскільки було написано:

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();

тож коли категорія видаляється, видаляються й усі пов’язані з нею товари;

  • рядки 43–53: заповнення таблиці двома категоріями по 5 товарів у кожній. У рядку 50 вставлення двох категорій одночасно спричинить вставлення їхніх товарів, знову ж таки тому, що ми написали [cascade = { CascadeType.ALL }];
  • рядок 58: відображаються категорії. Використовується мапер jSON [jsonMapperCategorieWithoutProduits] для відображення категорій без їхніх товарів. Адже метод [dao.getAllCategories()] відображає категорії без їхніх товарів (lazy loading);
  • рядок 61: відображаються товари без їхніх категорій. Адже метод [dao.getAllProduits()] відображає товари без їхніх категорій (lazy loading);
  • рядки 63–65: відображають категорію з назвою [categorie1] разом із товарами (eager loading);
  • рядки 67–69: відображають товар разом із його категорією;
  • рядки 71–81: усі ціни на товари категорії [categorie1] підвищуються на 10 %;
  • рядки 91–101: додається категорія з двома товарами з однаковою назвою. Однак у таблиці [PRODUITS] існує обмеження унікальності для стовпця [NOM]. Тому вставка другого товару буде відхилена, і буде згенеровано виняток. Однак метод [dao.addProduits] виконується в рамках транзакції. Отже, те, що друге вставлення завершується невдачею, має також скасувати вставлення першого товару, а також їхньої категорії [cat1]. Саме це ми й хочемо перевірити;
  • рядки 119–121: узагальнений метод, здатний відобразити рядок jSON для будь-якого елемента типу T. Серіалізація jSON контролюється маппером, переданим як параметр;
  • рядки 124–128: аналогічний метод, цього разу для списку елементів типу T;

Виконання класу [Main] дає такі результати (без урахування логів Spring):


Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Affichage de la base --------------------------------
-- Liste des catégories
{"id":4,"version":0,"nom":"categorie0"}
{"id":5,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":13,"version":0,"nom":"produit00","idCategorie":4,"prix":100.0,"description":"desc00"}
{"id":14,"version":0,"nom":"produit01","idCategorie":4,"prix":101.0,"description":"desc01"}
{"id":15,"version":0,"nom":"produit02","idCategorie":4,"prix":102.0,"description":"desc02"}
{"id":16,"version":0,"nom":"produit03","idCategorie":4,"prix":103.0,"description":"desc03"}
{"id":17,"version":0,"nom":"produit04","idCategorie":4,"prix":104.0,"description":"desc04"}
{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"}
{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"}
{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"}
{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"}
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}
-- Catégorie 1 avec ses produits
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produit [produit14] avec sa catégorie
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14","categorie":{"id":5,"version":0,"nom":"categorie1"}}
Mise à jour du prix des produits de [categorie1] --------------------------------
-- Produits de la catégorie [categorie1] avant la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produits de la catégorie [categorie1] après la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":1,"nom":"produit10","idCategorie":5,"prix":121.0,"description":"desc10"},{"id":19,"version":1,"nom":"produit11","idCategorie":5,"prix":122.1,"description":"desc11"},{"id":20,"version":1,"nom":"produit12","idCategorie":5,"prix":123.2,"description":"desc12"},{"id":21,"version":1,"nom":"produit13","idCategorie":5,"prix":124.3,"description":"desc13"},{"id":22,"version":1,"nom":"produit14","idCategorie":5,"prix":125.4,"description":"desc14"}]}
Vidage de la base de données --------------------------------
-- Liste des categories avant l'ajout
-- Liste des produits avant l'ajout
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites : 
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
-- Liste des categories après l'ajout
-- Liste des produits après l'ajout
Travail terminé
  • рядки 4–17: категорії та товари, вставлені в таблицю;
  • рядки 18–19: категорія разом із товарами;
  • рядки 20–21: товар із відповідною категорією;
  • рядки 22–26: оновлення ціни деяких товарів. У рядку 24 видно, що ціни дійсно зросли на 10 %;
  • рядки 27–36: додавання категорії [cat1] із двома товарами з однаковою назвою. Бачимо, що таблиця є однаковою до (рядки 28–29) та після додавання (рядки 35–36), що свідчить про те, що всі вставки транзакції були успішно скасовані;
  • рядки 31–34: виняток, що виник під час введення другого товару та призвів до зриву всієї транзакції;

11.3.10. Юнітарний тест JUnit

  

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


package spring.data.tests;

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

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.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

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

    // шар [DAO]
    @Autowired
    private IDao dao;

    // фільтри jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;

    @Before
    public void cleanAndFill() {
        // очищаємо базу даних перед кожним тестом
        log("Vidage de la base de données", 1);
        // очищається таблиця [CATEGORIES] — каскадно очищається таблиця [PRODUITS]
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // таблиці заповнюються
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // додавання категорії — за каскадним принципом товари також будуть вставлені
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // список категорій
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // список товарів
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // деякі перевірки
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }

    @Test
    public void getCategorieByNameWithProduits() {
        log("getCategorieByNameWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals(5, categorie1.getProduits().size());
    }

    @Test
    public void getCategorieByNameWithoutProduits() {
        log("getCategorieByNameWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals("categorie1", categorie1.getNom());
    }

    @Test
    public void getProduitByIdWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
        Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
    }

    @Test
    public void getProduitByIdWithoutCategorie() {
        log("getProduitByIdWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
    }
...
    // -------------- приватні методи
    private Produit findProduitByName(String nom, List<Produit> produits) {
        for (Produit produit : produits) {
            if (produit.getNom().equals(nom)) {
                return produit;
            }
        }
        return null;
    }

    private Categorie findCategorieByName(String nom, List<Categorie> categories) {
        for (Categorie categorie : categories) {
            if (categorie.getNom().equals(nom)) {
                return categorie;
            }
        }
        return null;
    }

    // відображення елемента типу T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // виведення списку елементів типу T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // виводить повідомлення
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }

    private static void show(String title, List<String> messages) {
        // заголовок
        System.out.println(String.format("%s : ", title));
        // повідомлення
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}
  • рядок 27: модульний тест налаштовується класом [AppConfig], який вже було представлено в розділі 11.3.8;
  • рядки 32–33: введення посилання на рівень [DAO];
  • рядки 36–50: введення п’яти маперів jSON;
  • рядки 60–71: після очищення бази даних (рядок 57) база даних заповнюється двома категоріями, кожна з яких містить 5 продуктів. Цей метод виконується перед кожним тестом завдяки анотації [@Before] у рядку 52;
  • рядки 75–93: відображає вміст бази даних;
  • рядки 95–101: запит категорії разом із її товарами, категорія ідентифікується за назвою;
  • рядки 103–109: запит категорії без її товарів, категорія ідентифікується за назвою;
  • рядки 111–120: запит на товар разом із його категорією, товар ідентифікується за номером;
  • рядки 122–130: запит на товар без його категорії, товар ідентифікується за номером;
  • рядки 133–184: приватні методи, спільні для різних тестів;

Завдання: виконайте тест. Він має пройти успішно.


11.3.11. Управління журналами

Журнали консольного додатка або тесту JUnit налаштовуються за допомогою такого файлу [logback.xml]:

  

Файл повинен мати назву [logback.xml] і знаходитися в Classpath проекту. Для цього його розміщено тут, у папці [src/main/resources], яка входить до Classpath. Його вміст такий:


<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: тег [<root level="info">] відображає журнали рівня [info]. Замість [info] можна вказати:
    • [debug]: це найдетальніший рівень журналів. Рекомендується використовувати його під час налагодження проєкту, оскільки там містяться дуже цікаві записи про обмін даними між клієнтом і сервером. Це спосіб зрозуміти, що відбувається «під капотом»;
    • [off]: логів немає взагалі;
    • [warn]: проміжний рівень логів, на якому Spring відображає аномалії, які, однак, не є помилками. Їх слід перевірити, якщо не отримано очікуваного результату;

Завдання: змініть рівень у рядку 12 на [debug], а потім виконайте модульний тест. Зверніть увагу на різницю в логах.


11.3.12. Створення архіву Maven для проєкту

Щоб встановити архів проекту в локальне сховище Maven, виконайте такі дії [1-3]:

Архів буде створено з використанням ідентифікаторів, знайдених у файлі [pom.xml]:


    <groupId>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

Розташування локального репозиторію Maven можна знайти в налаштуваннях Eclipse:

 

Після цього можна перевірити правильність встановлення артефакту Maven:

 

Відтепер інший локальний проект Maven зможе використовувати цей архів.