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):
Typ T musi zatem posiadać pole o nazwie [something]. W ten sposób metoda
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:
- 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:
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:
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.





































