Skip to content

11. [Cours]: Zarządzanie relacyjnymi bazami danych za pomocą Spring Data

Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności, API JPA (Java Persistence API), Spring Data.

Zaimplementujemy warstwę [DAO] z TD przy użyciu [Spring Data], gałęzi ekosystemu Spring. [Spring Data] opiera się na warstwie JPA (Java Persistence API), która umożliwia warstwie [DAO] operowanie obiektami zamiast poleceniami SQL. Ostatecznie warstwa [DAO] nie zdaje sobie sprawy, że komunikuje się z bazą danych. Zna jedynie interfejs warstwy [Spring Data].

Najpierw zapoznamy się z warstwą [Spring Data] na podstawie dwóch przykładów.

11.1. Support

  • w [1], folder [support / chap-11] zawiera trzy projekty Eclipse;
  • w pliku [2] znajduje się skrypt SQL służący do utworzenia przykładowej bazy danych z tego rozdziału;

11.2. Przykład 1

Na stronie Springa znajduje się wiele samouczków dotyczących rozpoczęcia pracy ze Springiem [http://spring.io/guides]. Wykorzystamy jeden z nich, aby zapoznać się z Spring Data. W tym celu użyjemy Spring Tool Suite (STS).

  • w [1] importujemy jeden z samouczków z [spring.io/guides];
  • w [2] wybieramy samouczek [Accessing Data Jpa], który pokazuje, jak uzyskać dostęp do bazy danych za pomocą Spring Data;
  • w przypadku [3] wybiera się projekt skonfigurowany przez Maven;
  • w pliku [4] samouczek może być udostępniony w dwóch formach: [initial], czyli pusta wersja, którą wypełnia się zgodnie z samouczkiem, lub [complete], czyli ostateczna wersja samouczka. Wybieramy tę drugą;
  • w pliku [5] można wybrać opcję wyświetlenia samouczka w przeglądarce;
  • w pliku [6] znajduje się gotowy projekt.

11.2.1. Konfiguracja projektu w Maven

Zależności Maven projektu są skonfigurowane w pliku [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>
        <!-- użyj UTF-8 do wszystkiego -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • wiersze 5–9: definiują nadrzędny projekt Maven. To właśnie on określa większość zależności projektu. Mogą one być wystarczające, w którym to przypadku nie dodaje się żadnych nowych, lub nie, w którym to przypadku dodaje się brakujące zależności;
  • wiersze 12–15: definiują zależność od [spring-boot-starter-data-jpa]. Ten artefakt zawiera klasy Spring Data;
  • wiersze 16–19: definiują zależność od artefaktów SGBD i H2, które umożliwiają tworzenie i zarządzanie bazami danych w pamięci.

Przyjrzyjmy się klasom dostarczanym przez te zależności:

Jest ich bardzo wiele:

  • niektóre należą do ekosystemu Spring (te zaczynające się od „spring”);
  • inne należą do ekosystemu Hibernate (hibernate, jboss), z którego implementacją korzystamy tutaj (JPA);
  • inne to biblioteki testowe (junit, hamcrest);
  • inne to biblioteki logowania (log4j, logback, slf4j);

Zachowamy je wszystkie. W przypadku aplikacji produkcyjnej należałoby zachować tylko te, które są niezbędne.

W wierszu 26 pliku [pom.xml] znajduje się następujący wiersz:


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

Ten wiersz jest powiązany z następującymi wierszami:


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

W wierszach 6–9 wtyczka [spring-boot-maven-plugin] umożliwia wygenerowanie pliku jar zawierającego kod wykonywalny aplikacji. Wiersz 26 pliku [pom.xml] określa zatem klasę wykonywalną tego pliku jar.

11.2.2. Warstwa [JPA]

Dostęp do bazy danych odbywa się za pośrednictwem warstwy [JPA], Java Persistence API:

  

Aplikacja jest prosta i obsługuje klientów [Customer]. Klasa [Customer] należy do warstwy [JPA] i ma następującą postać:


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);
    }

}

Klient ma identyfikator [id], imię [firstName] i nazwisko [lastName]. Każda instancja [Customer] reprezentuje wiersz w tabeli bazy danych.

  • wiersz 8: adnotacja JPA, która powoduje, że trwałość instancji [Customer] (tworzenie, odczyt, aktualizacja, usunięcie) będzie zarządzana przez implementację JPA. Z zależności Maven wynika, że wykorzystywana jest implementacja JPA / Hibernate;
  • wiersze 11–12: adnotacje JPA, które powiązują pole [id] z kluczem głównym tabeli [Customer]. Wiersz 12 wskazuje, że implementacja JPA będzie korzystać z metody generowania klucza głównego właściwej dla używanego SGBD, w tym przypadku H2;

Nie ma innych adnotacji dotyczących JPA. W takim przypadku zostaną zastosowane wartości domyślne:

  • tabela o nazwie [Customer] będzie nosiła nazwę klasy, tj. [Customer];
  • kolumny tej tabeli będą nosiły nazwy pól klasy: [id, firstName, lastName], przy czym w nazwie kolumny tabeli nie jest brana pod uwagę wielkość liter;

Należy zauważyć, że w żadnym momencie nie podano nazwy używanej implementacji JPA.

11.2.3. Warstwa [Spring Data]

Klasa [CustomerRepository] implementuje warstwę dostępu do tabeli [Customer]. Jej kod wygląda następująco:

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Jest to zatem interfejs, a nie klasa (wiersz 7). Rozszerza on interfejs [CrudRepository], interfejs Spring Data (wiersz 5). Interfejs ten jest definiowany przez dwa typy: pierwszy to typ zarządzanych elementów, w tym przypadku typ [Customer], a drugi to typ klucza głównego zarządzanych elementów, w tym przypadku typ [Long]. Interfejs [CrudRepository] wygląda następująco:


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();
}

Interfejs ten definiuje operacje CRUD (Create – Read – Update – Delete), które można wykonać na typie JPA T:

  • wiersz 8: metoda save pozwala na zapisanie encji T w bazie danych. Zapisuje ona encję przy użyciu klucza głównego nadanego jej przez SGBD. Pozwala również na aktualizację encji T identyfikowanej za pomocą jej klucza głównego id. Wybór jednej lub drugiej operacji zależy od wartości klucza głównego id: jeśli ma on wartość null, wykonywana jest operacja zapisania, w przeciwnym razie – operacja aktualizacji;
  • wiersz 10: to samo, ale dla listy encji;
  • wiersz 12: metoda findOne pozwala odzyskać encję T identyfikowaną przez jej klucz pierwotny id;
  • wiersz 22: metoda delete pozwala usunąć encję T identyfikowaną przez jej klucz główny id;
  • wiersze 24–28: warianty metody [delete];
  • wiersz 16: metoda [findAll] pozwala odzyskać wszystkie zachowane encje T;
  • wiersz 18: to samo, ale ograniczone do encji, dla których przekazano listę identyfikatorów;

Wróćmy do interfejsu [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);
}
  • wiersz 9 pozwala odnaleźć obiekt [Customer] na podstawie jego nazwy [lastName];

I to wszystko, jeśli chodzi o warstwę [DAO]. Nie ma klasy implementującej poprzedni interfejs. Jest ona generowana w czasie wykonywania przez [Spring Data]. Metody interfejsu [CrudRepository] są implementowane automatycznie. W przypadku metod dodanych do interfejsu [CustomerRepository] sytuacja jest różna. Wróćmy do definicji [Customer]:


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

Metoda z linii 9 jest automatycznie implementowana przez [Spring Data], ponieważ odwołuje się do pola [lastName] (linia 3) z [Customer]. Gdy Spring Data napotka metodę [findBySomething] w interfejsie do zaimplementowania, implementuje ją za pomocą następującego zapytania JPQL (Java Persistence Query Language):

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

Typ T musi zatem posiadać pole o nazwie [something]. W ten sposób metoda

List<Customer> findByLastName(String lastName);

zostanie zaimplementowana za pomocą kodu podobnego do poniższego:

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

gdzie [em] oznacza kontekst trwałości JPA. Jest to możliwe tylko wtedy, gdy klasa [Customer] posiada pole o nazwie [lastName], co ma miejsce.

Podsumowując, w prostych przypadkach Spring Data pozwala nam zaimplementować warstwę [DAO] za pomocą prostego interfejsu.

11.2.4. Warstwa [console]

  

Klasa [Application] wygląda następująco:


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 {
        // zapisanie kilku klientów
        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"));

        // pobierz wszystkich klientów
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // pobierz pojedynczego klienta za pomocą ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // pobieranie klientów według nazwiska
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }

}
  • wiersz 9: klasa implementuje interfejs [CommandLineRunner], który jest interfejsem [Spring Boot] (wiersz 4). Interfejs ten posiada tylko jedną metodę, znajdującą się w wierszu 19;
  • wiersz 8: @SpringBootApplication to adnotacja łącząca kilka adnotacji [Spring Boot]:
    • @Configuration: wskazuje, że klasa jest klasą konfiguracyjną;
    • @EnableAutoConfiguration: nakazuje [Spring Boot] samodzielne utworzenie określonej liczby beanów w oparciu o różne właściwości, w szczególności zawartość ścieżki Classpath projektu. Ponieważ biblioteki Hibernate znajdują się w ścieżce Classpath, bean [entityManagerFactory] zostanie zaimplementowany przy użyciu Hibernate. Ponieważ biblioteka SGBD H2 znajduje się w ścieżce Classpath, bean [dataSource] zostanie zaimplementowany przy użyciu H2. W beanie [dataSource] należy również zdefiniować użytkownika i jego hasło. W tym przypadku Spring Boot użyje domyślnego administratora z H2, który nie ma hasła. Ponieważ biblioteka [spring-tx] znajduje się w ścieżce Classpath, zostanie użyty menedżer transakcji Springa;
    • @EnableWebMvc: jeśli w ścieżce Classpath znajduje się biblioteka [spring-mvc]. W takim przypadku następuje automatyczna konfiguracja aplikacji internetowej;
    • @ComponentScan: informuje Spring, gdzie szukać pozostałych beanów, konfiguracji i usług. Domyślnie są one wyszukiwane w pakiecie zawierającym klasę oznaczoną tagiem, tj. w pakiecie [hello]. W ten sposób zostaną znalezione klasy [Customer] i [CustomerRepository]. Ponieważ pierwsza z nich posiada adnotację [@Entity], zostanie skatalogowana jako encja zarządzana przez Hibernate. Ponieważ druga rozszerza interfejs [CrudRepository], zostanie zarejestrowana jako bean Spring;
  • wiersze 11–12: bean [CustomerRepository] jest wstrzykiwany do kodu klasy głównej;
  • wiersz 15: wykonywana jest metoda statyczna [run] klasy [SpringApplication] z projektu Spring Boot. Jej parametrem jest klasa posiadająca adnotację [Configuration] lub [EnableAutoConfiguration]. Następnie nastąpi wszystko, co zostało wyjaśnione powyżej. Wynikiem jest kontekst aplikacji Spring, tj. zbiór beanów zarządzanych przez Spring;

Kolejne operacje polegają jedynie na wykorzystaniu metod fasoli implementującej interfejs [CustomerRepository]. Wyniki wyświetlane w konsoli są następujące:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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
  • wiersze 1–8: logo projektu Spring Boot;
  • wiersz 9: uruchamiana jest klasa [hello.Application];
  • wiersz 10: [AnnotationConfigApplicationContext] to klasa implementująca interfejs [ApplicationContext] biblioteki Spring. Jest to kontener beanów;
  • wiersz 11: bean [entityManagerFactory] jest zaimplementowany za pomocą klasy [LocalContainerEntityManagerFactory], klasy biblioteki Spring;
  • wiersz 12: pojawia się [Hibernate]. Wybrano właśnie tę implementację JPA;
  • wiersz 19: dialektem Hibernate jest wariant SQL, który ma być używany wraz z SGBD. W tym przypadku dialekt [H2Dialect] wskazuje, że Hibernate będzie współpracować z SGBD i H2;
  • wiersze 21–22: utworzono bazę danych. Utworzono tabelę [CUSTOMER]. Oznacza to, że Hibernate został skonfigurowany do generowania tabel na podstawie definicji JPA, w tym przypadku definicji JPA klasy [Customer];
  • wiersze 26–30: wynik metody [findAll] z interfejsu;
  • wiersz 34: wynik metody [findOne] interfejsu;
  • wiersze 38–39: wyniki metody [findByLastName];
  • wiersze 41 i kolejne: logi zamknięcia kontekstu Spring.

11.2.5. Ręczna konfiguracja projektu Spring Data

Duplikujemy poprzedni projekt w projekcie [gs-accessing-data-jpa-02]:

  

W tym nowym projekcie nie będziemy polegać na automatycznej konfiguracji dokonywanej przez Spring Boot. Zrobimy to ręcznie. Może to być przydatne, jeśli domyślne ustawienia nam nie odpowiadają.

Najpierw określimy niezbędne zależności w pliku [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>
        <!-- Baza danych 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>
        <!-- używaj UTF-8 do wszystkiego -->
        <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>
  • wiersze 10–14: nadrzędny projekt Maven, z którego będziemy korzystać z zdefiniowanych w nim bibliotek;
  • wiersze 18–21: Spring Data używany do dostępu do bazy danych;
  • wiersze 23–26: implementacja specyfikacji JPA w Hibernate;
  • wiersze 28–31: SGBD H2;
  • wiersze 33–36: bazy danych są często wykorzystywane z pulami otwartych połączeń, co pozwala uniknąć ciągłego otwierania i zamykania połączeń. W tym przypadku zastosowano implementację o nazwie [tomcat-jdbc];

W nowym projekcie encja [Customer] i interfejs [CustomerRepository] pozostają bez zmian. Zmienimy klasę [Application], która zostanie podzielona na dwie klasy:

  • [Config], która będzie klasą konfiguracyjną:
  • [Main], która będzie klasą wykonalną;
  

Klasa wykonywalna [Application] ma teraz następujący wygląd:


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) {
        // instancjonowanie kontekstu Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // zapisanie kilku klientów
        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"));

        ...

        // zamknięcie kontekstu
        context.close();
    }
}
  • wiersz 9: klasa [Application] nie zawiera już adnotacji konfiguracyjnych;
  • wiersze 3–7: należy zauważyć, że nie ma już importów pakietów [Spring Boot];
  • wiersz 12: tworzone są instancje beanów Spring. Uzyskuje się kontekst Spring zawierający odniesienia do tak utworzonych beanów;
  • wiersz 13: żądamy odwołania do fasoli typu [CustomerRepository];

Klasa [Config] , która konfiguruje projekt, wygląda następująco:


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 {

    // baza danych H2
    @Bean
    public DataSource dataSource() {
        // źródło danych TomcatJdbc
        DataSource dataSource = new DataSource();
        // konfiguracja dostępu JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // początkowo otwarte połączenie
        dataSource.setInitialSize(1);
        // wynik
        return dataSource;
    }

    // dostawca 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();
    }

    // Menedżer transakcji
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • wiersz 17: adnotacja [@EnableTransactionManagement] wskazuje, że metody interfejsów [CrudRepository] muszą być wykonywane w ramach transakcji. Została ona skomentowana, ponieważ jest to ustawienie domyślne;
  • wiersz 18: adnotacja [@EnableJpaRepositories] pozwala wskazać katalogi, w których znajdują się interfejsy Spring Data [CrudRepository]. Interfejsy te staną się komponentami Springa i będą dostępne w jego kontekście;
  • wiersz 19: adnotacja [@Configuration] sprawia, że klasa [Config] staje się klasą konfiguracyjną Springa;
  • wiersz 20: adnotacja [@ComponentScan] pozwala wymienić katalogi, w których należy szukać komponentów Springa. Komponenty Spring to klasy oznaczone adnotacjami Spring, takimi jak @Service, @Component, @Controller itp. W tym przypadku nie ma innych komponentów poza tymi zdefiniowanymi w klasie [AppConfig], dlatego adnotacja została skomentowana;
  • wiersze 24–37: definiują źródło danych, bazę danych H2. To właśnie adnotacja @Bean w wierszu 25 sprawia, że obiekt utworzony przez tę metodę staje się komponentem zarządzanym przez Spring. Nazwa metody może być tutaj dowolna. Musi ona jednak nosić nazwę [dataSource], jeśli metoda EntityManagerFactory z wiersza 51 nie istnieje i jest definiowana przez autokonfigurację;
  • wiersz 30: baza danych będzie nosić nazwę [demo] i zostanie wygenerowana w folderze projektu;
  • wiersze 40–47: definiują używaną implementację JPA, w tym przypadku implementację Hibernate. Nazwa metody może być dowolna;
  • wiersz 43: brak logów SQL;
  • wiersz 44: baza danych zostanie utworzona, jeśli jeszcze nie istnieje;
  • wiersze 50–58: definiują metodę EntityManagerFactory, która będzie zarządzać trwałością JPA. Metoda ta musi obowiązkowo nosić nazwę [entityManagerFactory];
  • wiersz 51: metoda otrzymuje dwa parametry o typach odpowiadających dwóm wcześniej zdefiniowanym beanom. Zostaną one następnie skonstruowane, a następnie wstrzyknięte przez Spring jako parametry metody;
  • wiersz 53: określa używaną implementację JPA;
  • wiersz 54: określa katalogi, w których znajdują się encje JPA;
  • wiersz 55: określa źródło danych, które ma być obsługiwane;
  • wiersze 61–66: menedżer transakcji. Metoda musi nosić nazwę [transactionManager]. Jako parametr otrzymuje ona bean z wierszy 51–58;
  • wiersz 64: menedżer transakcji jest powiązany z EntityManagerFactory;

Powyższe metody można zdefiniować w dowolnej kolejności.

Uruchomienie projektu daje te same wyniki. W folderze projektu pojawia się nowy plik – plik bazy danych o nazwie H2:

  

11.2.6. Tworzenie archiwum wykonywalnego

Aby utworzyć archiwum wykonywalne projektu, można postępować w następujący sposób:

  • w [1]: tworzymy konfigurację uruchomienia;
  • w [2]: typu [Java Application]
  • w [3]: określa projekt do uruchomienia (użyj przycisku Browse);
  • w [4]: określa klasę do wykonania;
  • w [5]: nazwa konfiguracji uruchomienia – może być dowolna;
  • w [6]: eksportuje się projekt;
  • w [7]: w postaci archiwum wykonywalnego JAR;
  • w [8]: podaje ścieżkę i nazwę pliku wykonywalnego, który ma zostać utworzony;
  • w [9]: nazwa konfiguracji uruchomieniowej utworzonej w [5];
  • w [10] – utworzony plik archiwum;

Po wykonaniu tych czynności otwieramy konsolę w folderze zawierającym archiwum wykonywalne:

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

Archiwum uruchamia się w następujący sposób:


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

W konsoli pojawiają się następujące wyniki:

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  więcej szczegółów.
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. Przykład 2

11.3.1. Wprowadzenie

Wrócimy do przykładu tabeli produktów, której użyliśmy do wprowadzenia API JDBC, i stworzymy następującą architekturę:

Baza danych [dbintrospringjpa] zawiera dwie tabele: [PRODUITS] i [CATEGORIES]. Tabela [CATEGORIES] ma następującą strukturę:

 
  • [ID]: klucz główny w trybie AUTO_INCREMENT;
  • [VERSION]: numer wersji rekordu;
  • [NOM]: nazwa kategorii – unikalna;

Tabela [PRODUITS] ma następujący wygląd:

 
  • [ID]: klucz główny w trybie AUTO_INCREMENT;
  • [VERSION]: numer wersji rekordu;
  • [NOM]: nazwa produktu – unikalna;
  • [ID_CATEGORIE]: numer kategorii – klucz obcy w polu [CATEGORIES.ID];
  • [PRIX]: cena;
  • [DESCRIPTION]: opis produktu;

Zadanie: utwórz bazę danych [dbintrospringdata] za pomocą skryptu SQL [dbintrospringdata.sql] z dokumentacji:


11.3.2. Tworzenie projektu Maven

Aby utworzyć szkielet projektu Spring Data, można postępować w następujący sposób:

  • w [1] tworzymy nowy projekt;
  • w [2]: typu [Spring Starter Project];
  • w [3] podajemy nazwę grupy projektu;
  • w pliku [4]: podajemy nazwę artefaktu (w tym przypadku pliku jar), który zostanie utworzony podczas kompilacji projektu;
  • w polu [5]: nazwa projektu w Eclipse – może być dowolna (nie musi być identyczna z wartością z pola [4]);
  • w polu [7]: należy wskazać, że zamierza się utworzyć projekt zawierający warstwę [JPA] wraz z SGBD i MySQL. Niezbędne zależności dla takiego projektu zostaną następnie uwzględnione w pliku [pom.xml];
  • w pliku [8] należy podać nazwę folderu projektu;
  • w pliku [9] zakończyć pracę kreatora;
  • w pliku [10]: utworzony projekt;

Plik [pom.xml] zawiera zależności niezbędne dla projektu 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/> <!-- wyszukanie elementu nadrzędnego z repozytorium -->
    </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>
  • wiersze 14–19: nadrzędny projekt Maven – definiuje wiele bibliotek wraz z ich wersjami – biblioteki te są wykorzystywane jako zależności Maven bez określania ich wersji;
  • wiersze 28–31: zależność niezbędna dla projektu JPA – obejmie ona projekt [Spring Data];
  • wiersze 32–36: zależność od sterownika JDBC w projekcie MySQL;
  • wiersze 37–41: zależności wymagane dla testów JUnit zintegrowanych ze Springiem;

Klasa wykonywalna [Application] nie wykonuje żadnych czynności, ale jest wstępnie skonfigurowana:


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);
    }
}
  • adnotacja [@SpringBootApplication] sprawia, że klasa ta staje się klasą samokonfigurującą się w projekcie;

Klasa testowa [ApplicationTests] nie wykonuje żadnych czynności, ale jest wstępnie skonfigurowana:


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() {
    }

}
  • wiersz 9: adnotacja [@SpringApplicationConfiguration] umożliwia wykorzystanie pliku konfiguracyjnego [Application]. Dzięki temu klasa testowa będzie korzystać ze wszystkich beanów zdefiniowanych w tym pliku;
  • wiersz 8: adnotacja [@RunWith] umożliwia integrację Springa z JUnit: klasa będzie mogła zostać uruchomiona jako test JUnit. [@RunWith] jest adnotacją JUnit (wiersz 4), podczas gdy klasa [SpringJUnit4ClassRunner] jest klasą Spring (wiersz 6);

Teraz, gdy mamy już szkielet aplikacji JPA, możemy go uzupełnić, aby stworzyć projekt warstwy trwałości powiązanej z bazą danych produktów.

11.3.3. Projekt w Eclipse

Rozbudowujemy poprzedni projekt w następujący sposób:

  
  • [AppConfig.java]: klasa konfiguracyjna projektu Spring;
  • [Main.java]: klasa wykonawcza projektu;
  • [IDao.java]: interfejs warstwy [DAO];
  • [Dao.java]: klasa implementacyjna warstwy [DAO];
  • [AbstractEntity.java]: klasa nadrzędna klas [Produit] i [Categorie];
  • [Produit.java]: klasa powiązana z wierszem tabeli [PRODUITS] w bazie danych;
  • [Categorie.java]: klasa powiązana z wierszem tabeli [CATEGORIES] w bazie danych;
  • [ProduitsRepository]: interfejs Spring Data umożliwiający dostęp do tabeli [PRODUITS];
  • [CategoriesRepository]: interfejs Spring Data umożliwiający dostęp do tabeli [CATEGORIES];
  • [pom.xml]: plik konfiguracyjny projektu Maven;

Projekt ten wykorzystuje następującą architekturę:

Warstwa [DAO] widzi wyłącznie warstwę zaimplementowaną przez [Spring Data].

11.3.4. Konfiguracja Maven

Plik [pom.xml] projektu Maven ma następującą treść:


<?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 Baza danych -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- biblioteka 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>
        <!-- Test 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>
        <!-- biblioteka logów -->
        <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>

Jest to konfiguracja wykorzystana i wyjaśniona w punkcie 11.2.5. Dodajemy do niej następujące biblioteki:

  • wiersze 42–49: biblioteka jSON wykorzystywana przez metodę [toString] klasy [Produit];
  • wiersze 51–55: biblioteka [Google Guava], która udostępnia metody pomocnicze do zarządzania zbiorami elementów. Będzie ona wykorzystywana przez klasę [Dao], która implementuje warstwę [DAO];
  • wiersze 56–67: biblioteki niezbędne do testów JUnit;
  • wiersze 69–72: biblioteka logów;
  • wiersze 81–86: wtyczki Maven niezbędne dla projektu;

11.3.5. Elementy warstwy [JPA]

Warstwa

[DAO]

Warstwa

[console]

Warstwa

[JPA]

Kierowca

[JDBC]

Warstwa

[Spring Data]

Spring 4

SGBD

  

11.3.5.1. Klasa [AbstractEntity]

Klasa [AbstractEntity] ma następującą postać:


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 {
    // właściwości
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;

    // konstruktory
    public AbstractEntity() {

    }

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

    // przedefiniowanie [equals] i [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();
    }

    // podpis jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // metody pobierające i ustawiające
....
}

Celem tej klasy jest zapewnienie klasy nadrzędnej dla encji JPA poprzez zgrupowanie w jednym miejscu właściwości [id, version] (wiersze 19, 22), wspólne dla obu encji [Produit] i [Categorie] powiązanych z bazą danych. Właściwości te są powiązane z kolumnami [ID, VERSION] w tabelach (wiersze 18, 21).

  • wiersz 13: adnotacja [@MappedSuperclass] wskazuje, że klasa ta jest klasą nadrzędną dla jednostek JPA;
  • wiersz 16: adnotacja [@Id] wskazuje, że pole [id] (może ono nosić inną nazwę) jest powiązane z kluczem głównym tabeli;
  • wiersz 17: adnotacja [@GeneratedValue(strategy=GenerationType.IDENTITY)] określa tryb generowania kluczy głównych. Tryb [GenerationType.IDENTITY] będzie wykorzystywał wraz z MySQL tryb [AUTO_INCREMENT]. W przypadku innego SGBD tryb ten wykorzystywałby inną metodę. Zaletą jest to, że programista nie musi się tym martwić, a jego kod pozostaje poprawny niezależnie od używanego SGBD;
  • wiersz 18: adnotacja [@Column] wskazuje kolumnę powiązaną z polem. Gdy ta adnotacja nie występuje, JPA zakłada, że kolumna nosi tę samą nazwę co pole. Tak jest w tym przypadku. Nie trzeba było więc umieszczać tej adnotacji;
  • wiersz 20: adnotacja [@Version] wskazuje, że pole [version] jest powiązane z kolumną wersjonowania. Implementacja JPA będzie zwiększać ten numer wersji za każdym razem, gdy encja zostanie zmodyfikowana. Numer ten służy do zapobiegania jednoczesnej aktualizacji encji przez dwóch różnych użytkowników: dwaj użytkownicy U1 i U2 odczytują encję E o numerze wersji równym V1. U1 modyfikuje E i zapisuje tę zmianę w bazie danych: numer wersji zmienia się wówczas na V1+1. U2 z kolei modyfikuje E i zapisuje tę zmianę w bazie danych: otrzyma wyjątek, ponieważ posiada wersję (V1) inną niż ta w bazie danych (V1+1);
  • wiersze 35–52: ponowne zdefiniowanie metod [hashCode] i [equals]. Domyślnie wartość [obj1.equals(obj2)] wynosi true, jeśli [obj1==obj2] ma wartość true, tj. jeśli ob1 i obj2 są dwoma równymi wskaźnikami. Jeśli chcemy porównać obiekty, na które wskazują wskaźniki, a nie same wskaźniki, należy przedefiniować metodę [equals] oraz metodę [hashCode]. Ta ostatnia musi zwracać tę samą wartość dla dwóch obiektów, które metoda [equals] uznaje za równe;
  • wiersze 42–51: dwa obiekty typu [AbstractEntity] lub pochodne zostaną uznane za równe, jeśli ich klucze główne [id] są równe;
  • wiersze 35–38: metoda [hashCode] rzeczywiście zwraca tę samą wartość dla dwóch identycznych obiektów typu [AbstractEntity], a zatem posiadających ten sam klucz główny [id];
  • wiersze 55–63: metoda [toString] zwraca ciąg znaków jSon obiektu [this]. Jeśli obiekt ten odnosi się do klasy potomnej, metoda ta zwróci wówczas ciąg znaków jSON klasy potomnej. Dzięki temu nie musimy tworzyć metody [toString] w klasach potomnych;

11.3.5.2. Entyteta JPA [Produit]

Klasa [Produit] jest encją JPA powiązaną z wierszem tabeli [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 {

    // właściwości
    @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;

    // kategoria
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;

    // konstruktory
    public Produit() {

    }

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

    // metody pobierające i ustawiające
...
}
  • wiersz 12: adnotacja [@Entity] określa, że klasa [Produit] jest jednostką zarządzaną przez warstwę [JPA];
  • wiersz 13: adnotacja [@Table(name = "PRODUITS")] wskazuje, że klasa [Produit] jest obrazem odpowiadającym wierszowi w tabeli [PRODUITS] w bazie danych;
  • wiersz 14: nazwa filtra jSON, który ma zostać zastosowany do encji. Zobaczymy, że właściwość [categorie] z wiersza 13 nie zawsze jest dostępna. Należy ją wówczas wykluczyć z reprezentacji obiektu o nazwie jSON. W tym celu potrzebujemy filtra. W filtrze o nazwie [jsonFilterCategorie] określimy zatem, czy chcemy uwzględnić właściwość [categorie], czy nie;
  • wiersz 18: adnotacja [@Column] powiązuje pole [nom] z kolumną [NOM] w tabeli [PRODUITS]. Gdy pole ma taką samą nazwę jak powiązana kolumna, adnotację [@Column] można pominąć. Tak jest w tym przypadku;
  • wiersze 31–33: kategoria produktu;
  • wiersz 31: adnotacja [@ManyToOne] wskazuje, że kolumna adnotacji z wiersza 32 [@JoinColumn(name = "CATEGORIE_ID")] jest kluczem obcym tabeli [PRODUITS] wentytetu [Produit] w tabeli [CATEGORIES] powiązanej z entytetem z wiersza 33. Adnotacja ta musi odnosić się do entytetu JPA. Zatem klasą w wierszu 33 musi być jednostka JPA;
  • wiersz 31: adnotacja [fetch = FetchType.LAZY] wymaga, aby podczas pobierania produktu z tabeli [PRODUITS] jego kategoria (wiersz 33) nie była pobierana od razu (lazy loading). Jest ona wówczas pobierana przy pierwszym wywołaniu metody [getCategorie]. Atrybut ten nie ma charakteru obowiązkowego. Wykorzystywana implementacja JPA może go zignorować. Ponieważ właściwość [categorie] może występować lub nie, wprowadziliśmy filtr jSON w wierszu 14. Istniejące implementacje JPA (Hibernate, Eclipselink, OpenJPA) nie obsługują tej adnotacji w ten sam sposób. Hibernate rozszerza pierwotną metodę [getCategorie] (która ogranicza się do zwracania pola categorie) poprzez wywołanie metody SGBD w celu pobrania kategorii. Aby było to możliwe, połączenie z metodą SGBD, użyte początkowo do pobrania produktu, musi być nadal otwarte; w przeciwnym razie wystąpi wyjątek.

11.3.5.3. Entyteta JPA [Categorie]

Klasa [Categorie] jest encją JPA powiązaną z wierszem tabeli [CATEGORIES]:

 

Jej kod jest następujący:


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 {

    // właściwości
    @Column(name = "NOM")
    private String nom;

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

    // konstruktory
    public Categorie() {

    }

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

    // metody
    public void addProduit(Produit produit) {
        // dodajemy produkt
        produits.add(produit);
        // ustalamy kategorię
        produit.setCategorie(this);
    }

    // metody pobierające i ustawiające
...
}
  • wiersze 21–22: nazwa kategorii;
  • wiersze 25–26: produkty z tej kategorii;
  • wiersz 25: adnotacja [@OneToMany] stanowi relację odwrotną do relacji [@ManyToOne], którą spotkaliśmy w encji [Produit]. Atrybut [mappedBy = "categorie"] wskazuje pole encji [Produit], na które odnosi się relacja odwrotna [@ManyToOne]. Atrybut [cascade = { CascadeType.ALL }] wymaga, aby operacje (persist, merge, remove) wykonywane na @Entity [Categorie] były kaskadowo stosowane do [produits] w wierszu 26. Można określić kaskadę częściową za pomocą stałych [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
  • wiersz 25: atrybut [fetch = FetchType.LAZY] określa, że po pobraniu kategorii z tabeli [CATEGORIES] jej produkty nie są pobierane od razu. Zostaną one pobrane przy pierwszym wywołaniu metody [getProduits]. Istniejące implementacje JPA (Hibernate, Eclipselink, OpenJPA) nie obsługują tej adnotacji w ten sam sposób. Hibernate rozszerza pierwotną metodę [getProduits] (która ogranicza się do zwracania pola produits) poprzez wywołanie metody SGBD w celu pobrania produktów z danej kategorii. Aby było to możliwe, połączenie z metodą SGBD, użyte początkowo do pobrania kategorii, musi być nadal otwarte. Ten atrybut ma charakter obowiązkowy. Implementacja JPA nie może go zignorować. Ponieważ właściwość [produits] może być zainicjowana lub nie, wprowadziliśmy filtr jSON w wierszu 17, który pozwoli nam wskazać, czy chcemy tę właściwość, czy nie;
  • wiersz 26: typ [Set] jest interfejsem. Typ [HashSet] jest klasą implementującą ten interfejs. Implementuje ona kolekcję elementów o nazwie ensemble. Zbiór nie może zawierać dwóch identycznych obiektów. W tym przypadku obiekty są typu [Produit]. Zatem w zbiorze nie mogą znajdować się dwa identyczne obiekty. Ponieważ metoda [equals] klasy nadrzędnej [AbstractEntity] została przedefiniowana tak, aby uznać dwa produkty za identyczne, jeśli mają ten sam klucz główny, to pole [produits] nie może zawierać dwóch produktów o tym samym kluczu głównym;
  • wiersze 38–43: metoda [addProduit] umożliwia dodanie produktu do kategorii;

11.3.6. Warstwa [Spring Data]

Warstwa

[DAO]

Warstwa

[console]

Warstwa

[JPA]

Kurtka

[JDBC]

Warstwa

[Spring Data]

Spring 4

SGBD

  

Interfejs [CategoriesRepository] zarządza dostępem do tabeli [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> {

    // kategoria wraz z produktami
    @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);

    // kategoria bez produktów, określona przez nazwę
    public Categorie findByNom(String nom);
}
  • wiersz 8: interfejs [CrudRepository] został wykorzystany i wyjaśniony w paragrafie 11.2.3. Przypominamy, że:
    • pierwszym typem interfejsu jest jednostka JPA zarządzana w celu zapewnienia dostępu do CRUD (findOne, findAll, zapis, usunięcie, deleteAll),
    • drugi typ to klucz główny encji JPA, w tym przypadku liczba całkowita [Long];
  • wiersz 12: metoda z wiersza 12 jest realizowana przez zapytanie JPQL (Java Persistence Query Language) z wiersza 11. Zapytanie to dotyczy encji JPA. W takim zapytaniu:
    • tabele są zastępowane przez powiązane z nimi encje JPA;
    • kolumny są zastępowane przez pola encji JPA użytych w zapytaniu;
  • wiersz 11: zapytanie JPQL zwraca kategorię wraz z jej produktami. Przypomnijmy, że w encji [Categorie] pole [produits] miało atrybut [fetch = FetchType.LAZY] (lazy loading). W zapytaniu JPQL wymuszamy ładowanie produktów za pomocą słowa kluczowego [fetch]. Parametr ?1 zapytania zostanie zastąpiony podczas wykonywania wartością pierwszego parametru metody z wiersza 12, a więc parametrem [Long id];
  • wiersze 14–15: analogiczna metoda dla kategorii identyfikowanej na podstawie nazwy;
  • wiersz 18: metoda [findByNom] zostanie automatycznie zaimplementowana przez [Spring Data], ponieważ typ [Category] posiada pole [nom];

Interfejs [ProduitsRepository] zarządza dostępem do tabeli [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> {

    // produkt wraz z kategorią
    @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);

    // produkt bez kategorii, określony przez nazwę
    public Produit findByNom(String nom);
}

Wyjaśnienia są takie same jak w przypadku interfejsu [CategoriesRepository].

Interfejsy te zostaną zaimplementowane przez klasy wygenerowane przez interfejs [Spring Data] w trakcie wykonywania projektu. Takie klasy nazywane są [proxy]. Domyślnie metody klasy implementacyjnej są wykonywane w ramach transakcji. Fakt, że interfejsy te dziedziczą po klasie [CrudRepository], sprawia, że są one komponentami Spring.

11.3.7. Warstwa [DAO]

Warstwa

[DAO]

Warstwa

[console]

Warstwa

[JPA]

Kurtka

[JDBC]

Kombinezon

[Spring Data]

Spring 4

SGBD

  

Interfejs [IDao] warstwy [DAO] wygląda następująco:


package spring.data.dao;

import java.util.List;

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

public interface IDao {

    // wstawienie listy produktów
    public List<Produit> addProduits(List<Produit> produits);

    // usunięcie wszystkich produktów
    public void deleteAllProduits();

    // aktualizacja listy produktów
    public List<Produit> updateProduits(List<Produit> produits);

    // pobieranie wszystkich produktów
    public List<Produit> getAllProduits();

    // dodanie listy kategorii
    public List<Categorie> addCategories(List<Categorie> categories);

    // usunięcie wszystkich kategorii
    public void deleteAllCategories();

    // aktualizacja listy kategorii
    public List<Categorie> updateCategories(List<Categorie> categories);

    // pobieranie wszystkich kategorii
    public List<Categorie> getAllCategories();

    // konkretny produkt wraz z kategorią lub bez niej
    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // określona kategoria z produktami lub bez
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);
}

Przyjęto tutaj zasadę, że każda metoda, która modyfikuje obiekty przekazane jako parametry wejściowe, musi zwrócić je w wyniku. Powód wprowadzenia tej zasady wyjaśniono w paragrafie 4.2: pozwala ona warstwie i jej klientowi znajdować się w dwóch oddzielnych instancjach JVM, a tym samym działać w modelu klient-serwer.

Implementacja [Dao] tego interfejsu wygląda następująco:


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));
        }
    }

}
  • wiersz 16: adnotacja [@Component] sprawia, że klasa [Dao] staje się komponentem Spring;
  • wiersze 19–23: wstrzyknięcie referencji do obu interfejsów [CrudRepository] z [Spring Data]. Wstrzyknięcie to nastąpi podczas instancjonowania obiektów Springa, zazwyczaj na początku wykonywania projektu Springa;
  • w wierszach 28 i 46 należy zauważyć, że metoda [save] z interfejsu [produitsRepository] jest wykorzystywana zarówno do wstawiania, jak i aktualizacji produktów. Metoda [Spring Data] wykorzystuje klucz podstawowy produktu, aby ustalić, czy ma wykonać wstawienie, czy aktualizację. Jeśli klucz podstawowy ma wartość [null], będzie to wstawienie, w przeciwnym razie aktualizacja;
  • wiersz 82: wykorzystuje się metodę [Lists.newArrayList] z biblioteki Guava w celu uzyskania listy produktów. Metoda [produitsRepository.findAll()] zwraca typ [Iterable<Produit>];
  • wiersz 28: metoda [produitsRepository.save(produits)] zwraca typ [Iterable<Produit>]. To samo dotyczy pozostałych operacji [save] tej klasy;

W powyższej klasie [Dao] wyjątki, które mogą wystąpić, są enkapsulowane w następującym typie [DaoException]:


package spring.data.dao;

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

// klasa wyjątków dla aplikacji „Wybory”
// wyjątek jest niekontrolowany

public class DaoException extends RuntimeException implements Serializable {

    // numer seryjny ID
    private static final long serialVersionUID = 1L;

    // pola lokalne
    private int code;
    private List<String> erreurs;

    // konstruktory
    public DaoException() {
        super();
    }

    public DaoException(int code, Throwable e) {
        // element nadrzędny
        super(e);
        // lokalny
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public DaoException(int code, String message, Throwable e) {
        // nadrzędny
        super(message, e);
        // lokalny
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public DaoException(int code, String message) {
        // nadrzędny
        super(message);
        // lokalny
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public DaoException(int code, List<String> erreurs) {
        // nadrzędny
        super();
        // lokalny
        this.code = code;
        this.erreurs = erreurs;
    }

    // lista komunikatów o błędach związanych z wyjątkiem
    private List<String> getErreursForException(Throwable th) {
        // pobierana jest lista komunikatów o błędach związanych z wyjątkiem
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // pobieramy komunikat tylko wtedy, gdy !=null i nie jest pusty
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // następna przyczyna
            cause = cause.getCause();
        }
        return erreurs;
    }

    // metody pobierające i ustawiające
...
}
  • wiersz 10: klasa ta wywodzi się z klasy [RuntimeException] i w związku z tym jest wyjątkiem niekontrolowanym;
  • wiersz 16: kod błędu;
  • wiersz 17: lista komunikatów o błędach związanych ze stosem wyjątków, które spowodowały wyjątek [DaoException];
  • wiersze 59–76: prywatna metoda [getMessagesForException] pozwala uzyskać listę komunikatów o błędach powiązanych z wyjątkami ze stosu wyjątków. Możliwe jest bowiem nakładanie wyjątków na stos za pomocą następujących konstruktorów klasy Exception:
    • Exception(String message, Throwable cause): tworzy wyjątek z komunikatem oraz wyjątkiem, który chcemy enkapsulować;
    • Exception(Throwable cause): tworzy wyjątek zawierający wyjątek, który ma zostać enkapsulowany;

Typ [Throwable] jest klasą nadrzędną klasy [Exception]. Jeśli powyższe konstruktory są wywoływane wielokrotnie, ostateczny wyjątek zawiera wówczas kilka wyjątków. Mówimy wtedy o stosie wyjątków.

  • ostatnią przyczynę wyjątku e1 uzyskuje się za pomocą wyrażenia [e1.getCause()];
  • przedostatnią przyczynę wyjątku e1 uzyskuje się za pomocą wyrażenia [e1.getCause().getCause()];
  • kontynuuje się w ten sposób, aż do uzyskania wyrażenia [getCause()==null];

11.3.8. Konfiguracja projektu Spring

  

Klasa [DaoConfig] konfiguruje warstwę [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 {

    // stałe
    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" };

    // źródło danych [tomcat-jdbc]
    @Bean
    public DataSource dataSource() {
        // źródło danych TomcatJdbc
        DataSource dataSource = new DataSource();
        // konfiguracja dostępu JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // początkowo otwarte połączenie
        dataSource.setInitialSize(1);
        // wynik
        return dataSource;
    }

    // dostawca 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();
    }

    // Menedżer transakcji
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

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

}

Podobną konfigurację omówiono w punkcie 11.2.5. Dodaliśmy do niej następujące adnotacje Spring:

  • wiersz 17: adnotacja [@EnableJpaRepositories] służy do wskazania pakietów, w których znajdują się interfejsy [CrudRepository] i [Spring Data];
  • wiersz 18: klasa ta jest klasą konfiguracyjną Springa. Ta informacja jest istotna. Jeśli ją usuniemy, projekt nadal działa. Jednak w dalszej części dokumentu, podczas tworzenia projektów opartych na tym projekcie, niektóre z nich przestaną działać, jeśli usuniemy adnotację z wiersza 18;
  • wiersz 19: adnotacja [@ComponentScan] wskazuje pakiety, w których znajdują się obiekty Springa. Są to klasy opatrzone adnotacją [@Component, @Service, @Controller, ...]. W tym miejscu komponent Springa [Dao] zostanie znaleziony i zainicjowany;
  • wiersze 73–76: zdefiniowaliśmy bean reprezentujący tablicę pakietów, które mają zostać przeszukane w celu znalezienia instancji JPA. Umożliwi to projektowi importującemu klasę [DaoConfig] przedefiniowanie tego beana, a tym samym zmianę skanowanych pakietów (wiersz 59). W dalszej części dokumentu zajmiemy się tą kwestią;

Klasa [AppConfig] konfiguruje cały projekt:


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 {
    // filtry jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtry
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // wynik
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtry
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // wynik
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtry
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // wynik
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtry
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // wynik
        return mapper;
    }
}
  • wiersz 11: klasa ta jest klasą konfiguracyjną Springa;
  • wiersz 12: która importuje bean'y zdefiniowane przez klasę [DaoConfig], którą właśnie omówiliśmy;
  • warstwa [console] wykorzystuje mapery jSON, które są tutaj zdefiniowane;
  • wiersze 14–64: definiują pięć mapowników jSON;
  • wiersze 15–18: filtr jSON [jsonMapper] nie posiada żadnych filtrów;
  • wiersze 20–30: filtr jSON [jsonMapperCategorieWithProduits] umożliwia serializację/deserializację obiektu [Categorie] wraz z jego produktami;
  • wiersze 32–42: filtr jSON [jsonMapperProduitWithCategorie] umożliwia serializację/deserializację obiektu [Produit] wraz z jego kategorią;
  • wiersze 43–53: filtr jSON [jsonMapperCategorieWithoutProduits] umożliwia serializację/deserializację obiektu [Categorie] bez jego produktów;
  • wiersze 55–64: filtr jSON [jsonMapperProduitWithoutCategorie] umożliwia serializację/deserializację obiektu [Produit] bez jego kategorii;

Należy zauważyć, że tworząc filtr jSON dla encji T, należy skonfigurować nie tylko filtr encji T, ale również filtry encji Ti, które ona sama może zawierać.

11.3.9. Warstwa [console]

Warstwa

[DAO]

Warstwa

[console]

Warstwa

[JPA]

Kurtka

[JDBC]

Warstwa

[Spring Data]

Spring 4

SGBD

  

Klasa [Main] ma następującą postać:


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 {
            // instancjonowanie kontekstu 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);
            // --------------------------------------------------------------------------------------
            // opróżnianie bazy danych
            log("Vidage de la base de données", 1);
            // opróżnianie tabeli [CATEGORIES] – w wyniku kaskadowego efektu tabela [PRODUITS] zostanie opróżniona
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // wypełnianie tabel
            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);
            }
            // dodano kategorię – w wyniku kaskadowego działania produkty również zostaną wstawione
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // lista kategorii
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // lista produktów
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // kategoria 1 wraz z produktami
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // produkt [produit14] wraz z kategorią
            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);
            // tabela [CATEGORIES] zostaje opróżniona – w konsekwencji tabela [PRODUITS] również zostanie opróżniona
            dao.deleteAllCategories();
            // wyświetlenie bazy danych
            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);
            // wstawiamy dane
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // dodanie kategorii – w wyniku kaskadowego działania produkty również zostaną dodane
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // weryfikacja
            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) {
                // zakończono
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }

    // wyświetlenie elementu typu T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // wyświetlenie listy elementów typu 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) {
        // wyświetla komunikat
        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);
    }
}
  • wiersz 25: utworzenie instancji beanów Spring na podstawie klasy konfiguracyjnej [AppConfig];
  • wiersze 26–33: pobieranie odwołań do mapperów jSON. Wykorzystuje się następującą sygnaturę metody [ApplicationContext].getBean:
    • [ApplicationContext].getBean(String id, Class klasa): którą stosuje się, gdy istnieje wiele beanów typu [classe]. W takim przypadku podaje się identyfikator żądanego beana. Jeśli został on zdefiniowany za pomocą adnotacji [@Bean], jego identyfikatorem jest nazwa metody opatrzonej adnotacją. Jeśli został zdefiniowany za pomocą adnotacji [@Bean(« identifiant »], jego identyfikatorem jest ten podany w adnotacji;
  • wiersz 34: pobranie odwołania z warstwy [DAO];
  • wiersze 37–39: opróżnienie bazy danych. Opróżnia się tabelę kategorii (wiersz 39). Ponieważ wpisano:

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

gdy kategoria zostanie usunięta, wszystkie powiązane z nią produkty również zostaną usunięte;

  • wiersze 43–53: wypełnianie tabeli dwiema kategoriami po 5 produktów każda. W wierszu 50 wstawienie obu kategorii spowoduje jednocześnie wstawienie ich produktów, ponownie dlatego, że zapisaliśmy [cascade = { CascadeType.ALL }];
  • wiersz 58: wyświetlamy kategorie. Używamy mapera jSON [jsonMapperCategorieWithoutProduits], aby wyświetlić kategorie bez powiązanych z nimi produktów. Metoda [dao.getAllCategories()] wyświetla bowiem kategorie bez powiązanych z nimi produktów (lazy loading);
  • wiersz 61: wyświetlane są produkty bez przypisanych do nich kategorii. Metoda [dao.getAllProduits()] wyświetla bowiem produkty bez przypisanych do nich kategorii (lazy loading);
  • wiersze 63–65: wyświetlają kategorię o nazwie [categorie1] wraz z jej produktami (eager loading);
  • wiersze 67–69: wyświetlają produkt wraz z kategorią;
  • wiersze 71–81: wszystkie ceny produktów z kategorii [categorie1] są podwyższane o 10%;
  • wiersze 91–101: dodaje się kategorię z dwoma produktami o tej samej nazwie. Jednak w tabeli [PRODUITS] obowiązuje ograniczenie unikalności w kolumnie [NOM]. Wstawienie drugiego produktu zostanie zatem odrzucone i zgłoszony zostanie wyjątek. Metoda [dao.addProduits] jest jednak wykonywana w ramach transakcji. Fakt, że drugie wstawienie kończy się niepowodzeniem, musi zatem spowodować również cofnięcie wstawienia pierwszego produktu, a także ich kategorii [cat1]. Właśnie to chcemy zweryfikować;
  • wiersze 119–121: metoda generyczna umożliwiająca wyświetlenie ciągu jSON dla dowolnego elementu typu T. Serializacja jSON jest kontrolowana przez mapper przekazany jako parametr;
  • wiersze 124–128: analogiczna metoda, tym razem dla listy elementów typu T;

Wykonanie klasy [Main] daje następujące wyniki (z pominięciem logów Springa):


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é
  • wiersze 4–17: kategorie i produkty wstawione do tabeli;
  • wiersze 18–19: kategoria wraz z produktami;
  • wiersze 20–21: produkt wraz z kategorią;
  • wiersze 22–26: aktualizacja cen niektórych produktów. W wierszu 24 widać, że ceny rzeczywiście wzrosły o 10%;
  • wiersze 27–36: dodanie kategorii [cat1] wraz z dwoma produktami o tej samej nazwie. Widać, że tabela wygląda tak samo przed (wiersze 28–29) i po dodaniu (wiersze 35–36), co wskazuje, że wszystkie operacje wstawiania w ramach tej transakcji zostały rzeczywiście cofnięte;
  • wiersze 31–34: wyjątek, który wystąpił podczas wstawiania drugiego produktu i spowodował niepowodzenie całej transakcji;

11.3.10. Test jednostkowy JUnit

  

Klasa [Test01] wygląda następująco:


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 {

    // warstwa [DAO]
    @Autowired
    private IDao dao;

    // filtry 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() {
        // przed każdym testem czyści się bazę
        log("Vidage de la base de données", 1);
        // opróżnia się tabelę [CATEGORIES] – w konsekwencji zostanie opróżniona tabela [PRODUITS]
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // wypełnia się tabele
        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);
        }
        // dodajemy kategorię – w wyniku kaskadowego działania produkty również zostaną wstawione
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // lista kategorii
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // lista produktów
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // kilka sprawdzeń
        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());
    }
...
    // -------------- metody prywatne
    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;
    }

    // wyświetlanie elementu typu T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // wyświetlanie listy elementów typu 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) {
        // wyświetla komunikat
        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) {
        // tytuł
        System.out.println(String.format("%s : ", title));
        // komunikaty
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}
  • wiersz 27: test jednostkowy jest skonfigurowany przez klasę [AppConfig], przedstawioną już w paragrafie 11.3.8;
  • wiersze 32–33: wstawienie odwołania do warstwy [DAO];
  • wiersze 36–50: wstrzyknięcie pięciu mapperów jSON;
  • wiersze 60–71: po wyczyszczeniu bazy (wiersz 57) baza danych jest wypełniana dwiema kategoriami, z których każda zawiera 5 produktów. Metoda ta jest wykonywana przed każdym testem z powodu adnotacji [@Before] w wierszu 52;
  • wiersze 75–93: wyświetla zawartość bazy danych;
  • wiersze 95–101: pobiera kategorię wraz z jej produktami, kategorię identyfikowaną po nazwie;
  • wiersze 103–109: pobierają kategorię bez produktów, kategorię identyfikowaną po nazwie;
  • wiersze 111–120: żądanie produktu wraz z kategorią, produkt identyfikowany na podstawie numeru;
  • wiersze 122–130: pobierają produkt bez kategorii, produkt identyfikowany na podstawie numeru;
  • wiersze 133–184: prywatne metody wspólne dla różnych testów;

Zadanie do wykonania: uruchom test. Powinien zakończyć się powodzeniem.


11.3.11. Zarządzanie logami

Logi aplikacji konsolowej lub testu JUnit są konfigurowane za pomocą następującego pliku [logback.xml]:

  

Plik musi nosić nazwę [logback.xml] i znajdować się w ścieżce Classpath projektu. W tym celu umieszczono go tutaj, w folderze [src/main/resources], który jest częścią ścieżki Classpath. Jego zawartość jest następująca:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- enkodery są domyślnie przypisane do typu
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- kontrola poziomu logów -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • wiersz 12: tag [<root level="info">] wyświetla logi poziomu [info]. Zamiast [info] można wpisać:
    • [debug]: jest to najbardziej szczegółowy poziom logów. Zaleca się stosowanie go podczas fazy debugowania projektu, ponieważ zawiera bardzo interesujące logi dotyczące komunikacji między klientem a serwerem. Jest to sposób na zrozumienie tego, co dzieje się „pod maską”;
    • [off]: brak jakichkolwiek logów;
    • [warn]: pośredni poziom logów, na którym Spring wyświetla anomalie, które niekoniecznie są błędami. Należy je sprawdzić, jeśli nie uzyskujemy oczekiwanego wyniku;

Zadanie do wykonania: zmień poziom w wierszu 12 na [debug], a następnie uruchom test jednostkowy. Zwróć uwagę na różnicę w logach.


11.3.12. Generowanie archiwum Maven projektu

Aby zainstalować archiwum projektu w lokalnym repozytorium Maven, wykonaj następujące czynności [1-3]:

Archiwum zostanie wygenerowane przy użyciu identyfikatorów znalezionych w pliku [pom.xml]:


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

Lokalizację lokalnego repozytorium Maven można znaleźć w konfiguracji programu Eclipse:

 

Można wówczas sprawdzić, czy artefakt Maven został poprawnie zainstalowany:

 

Od tej pory inne lokalne projekty Maven będą mogły korzystać z tego archiwum.