6. Spring Data JPA Hibernate
6.1. Introduction
Wykorzystamy bazę danych [dbproduitscategories] zarządzaną przez projekt [spring-jdbc-04] i zaimplementujemy dwa interfejsy [IDao<Categorie>, IDao<Produit>] zdefiniowane w tym projekcie. Pozwoli nam to na kilka rzeczy:
- porównać kody implementacyjne;
- korzystać z tej samej warstwy testowej;
- porównać wydajność obu implementacji;
![]() |
- warstwa [JDBC] jest zaimplementowana przez projekt [mysql-config-jdbc] omówiony w punkcie 3.3;
Przechodzimy teraz do pozostałych warstw.
6.2. Konfiguracja środowiska pracy
Za pomocą STS zaimportuj projekt [mysl-config-jpa-hibernate] [1], który znajduje się w folderze [<exemples>/spring-database-config/mysql/eclipse] [2]:
![]() |
Ten projekt konfiguruje warstwę [Spring JPA Hibernate] projektu. Każda implementacja JPA ma swój własny projekt konfiguracyjny.
Następnie zaimportuj projekt [spring-jpa-generic] [1] znajdujący się w folderze [<exemples>/spring-database-generic/spring-jpa] [2]:
![]() |
Po wykonaniu tej czynności zresetuj środowisko Maven (Alt-F5) dla wszystkich projektów znajdujących się w folderze [Package Explorer]:
![]() |
Następnie, aby sprawdzić środowisko robocze, uruchom konfigurację uruchomieniową o nazwie [spring-jpa-generic-JUnitTestDao-hibernate]:
![]() |
Ta konfiguracja uruchamia test o nazwie [JUnitTestDao]. Test ten musi zakończyć się powodzeniem:
![]() |
6.3. Projekt konfiguracji warstwy JPA
![]() |
Zadaniem tego projektu jest skonfigurowanie warstwy JPA w poniższej architekturze:
![]() |
6.3.1. Konfiguracja Maven
Projekt jest projektem Maven i jest skonfigurowany za pomocą następującego pliku [pom.xml]:
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>configuration mysql openjpa</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- zmienne zależności ********************************************** -->
<!-- JPA dostawca -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- stałe zależności ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Spring Context -->
<!-- konfiguracja dziedziczona JDBC -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</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>
- wiersze 5–7: artefakt Maven wygenerowany przez ten projekt. Projekty konfiguracyjne innych implementacji JPA (Eclipselink i OpenJpa) będą korzystać z tego samego artefaktu. Oznacza to, że w danym momencie aktywny może być tylko jeden z tych projektów. Należy zatem unikać sytuacji, w której wszystkie te projekty znajdują się w [Package Explorer]. Wystarczy tylko jeden;
- wiersze 10–14: nadrzędny projekt Maven, który określa wersje większości zależności niezbędnych dla projektu;
- wiersze 19–22: biblioteka Hibernate;
- wiersze 25–28: biblioteka Spring Data;
- wiersze 32–34: projekt konfiguracyjny warstwy JPA opiera się na projekcie konfiguracyjnym warstwy JDBC, który definiuje między innymi sterownik JDBC dla używanego SGBD oraz dane bazy danych, z której należy korzystać;
- wiersze 35–39: projekt konfiguracji warstwy JDBC zawiera bibliotekę [Spring JDBC], która została tutaj zastąpiona biblioteką [Spring Data JPA]. W związku z tym zaleca się, aby nie uwzględniać jej w zależnościach projektu. Jeśli jednak pozostanie, nie spowoduje to żadnych błędów;
Ostatecznie zależności projektu są następujące:
![]() |
6.3.2. Konfiguracja Spring
![]() |
Klasa [ConfigJpa] konfiguruje projekt Spring:
package generic.jpa.config;
import javax.persistence.EntityManagerFactory;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
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;
@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {
// dostawca JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// pakiety encji JPA
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// źródło danych
@Bean
public DataSource dataSource() {
// źródło danych TomcatJdbc
DataSource dataSource = new DataSource();
// konfiguracja dostępu JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// początkowo otwarte połączenia
dataSource.setInitialSize(5);
// wynik
return dataSource;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
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 18: klasa ta jest klasą konfiguracyjną Spring;
- wiersz 19: importuje ona komponenty zdefiniowane przez klasę konfiguracyjną [ConfigJdbc], która posłużyła do skonfigurowania projektu Spring [mysql-config-jdbc]. Są to filtry jSON;
- wiersze 23–30: definiują używaną implementację JPA, w tym przypadku implementację Hibernate (wiersz 25);
- wiersz 26: można włączyć lub wyłączyć wyświetlanie operacji SQL wykonywanych przez implementację Hibernate;
- wiersz 27: podaje się Hibernate połączony SGBD. Ta konfiguracja jest istotna. Pozwala ona Hibernate na korzystanie z dialektu SQL z SGBD MySQL, łącznie z jego zastrzeżonymi elementami. Ponadto dostarcza to Hibernate informacji o typach SQL oraz obiektach SGBD, z których będzie mógł korzystać. To właśnie ta zdolność implementacji JPA do dostosowania się do konkretnego SGBD zapewnia jej dużą przenośność między SGBD;
- wiersz 28: Hibernate może wygenerować lub nie tabele docelowej bazy danych na podstawie znalezionych encji JPA. Generowanie to ma miejsce tylko wtedy, gdy tabele nie istnieją. Jeśli już istnieją, nie podejmuje się żadnych działań. Wykorzystamy tę możliwość generowania tabel, gdy przedstawimy, w jaki sposób wygenerowano skrypty SQL służące do tworzenia różnych baz danych wykorzystanych w niniejszym dokumencie;
- wiersz 33: pakiet, w którym znajdują się encje JPA z bazy [dbproduitscategories];
- wiersze 36–49: źródło danych [tomcat-jdbc] powiązane z bazą [dbproduitscategories];
- wiersze 52–60: bean o nazwie [entityManagerFactory] (musi nosić właśnie taką nazwę) jest beanem, który utworzy obiekt [EntityManager] zarządzający kontekstem trwałości JPA. Wszystkie operacje JPA przechodzą przez niego. Wykorzystanie [Spring Data JPA] sprawia, że sami nigdy nie będziemy korzystać z tego obiektu. Musimy go jednak skonfigurować. Musi on znać następujące informacje:
- używaną implementację JPA (wiersz 55);
- używane źródło danych (wiersz 57);
- entytety JPA z tego źródła (wiersz 56);
- wiersz 58: inicjuje obiekt EntityManager przy użyciu tych informacji;
- wiersz 59: zwraca singleton [entityManagerFactory];
- wiersze 63–68: definiują menedżera transakcji. Musi on nosić nazwę [transactionManager];
- wiersz 65: tworzony jest menedżer transakcji o nazwie JPA;
- wiersz 66: jest on powiązany ze źródłem danych z wiersza 37 za pośrednictwem komponentu [entityManagerFactory] (wiersze 53 i 57);
Jedynie bean z wierszy 23–30 jest zależny od zastosowanej implementacji JPA. Pozostałe beany opierają się następnie na nim.
6.3.3. Entities warstwy [JPA]
![]() |
![]() |
Bazą docelową jest baza [dbproduitscategories] wraz z dwiema tabelami: [CATEGORIES] i [PRODUITS]. Zauważyliśmy, że zawiera ona również trzy inne tabele o nazwie [USERS, ROLES, USERS_ROLES], które będą wykorzystywane do zabezpieczenia serwisu internetowego, który zostanie wdrożony w sieci. Na razie pominęliśmy te tabele. Przypomnijmy dla porządku strukturę tabel [CATEGORIES] i [PRODUITS]:
Tabela [PRODUITS] ma następującą strukturę:
![]() |
- [ID]: autoinkrementujący klucz główny tabeli [2];
- [NOM]: unikalna nazwa produktu [4];
- [PRIX]: cena produktu;
- [DESCRIPTION]: opis produktu;
- [VERSIONING] to numer wersji produktu. Jego wersja początkowa to 1 [3]. Za każdym razem, gdy produkt zostanie zmodyfikowany, jego numer wersji zostanie zwiększony przez kod obsługujący tabelę;
- [CATEGORIE_ID]: klucz obcy w tabeli [CATEGORIES], określający kategorię, do której należy produkt;
![]() |
- w [1-3], klucz obcy [CATEGORIE_ID] z tabeli [PRODUITS]. Odnosi się do kolumny [ID] w tabeli [CATEGORIES] [4-5];
- gdy kategoria zostanie usunięta, wszystkie powiązane z nią produkty również zostaną usunięte ([6]). Należy zwrócić na to szczególną uwagę, ponieważ jest to wykorzystywane przy tworzeniu warstwy [DAO] opartej na bazie [dbproduitscategories];
Tabela kategorii [CATEGORIES] ma następujący wygląd:
![]() |
- [ID]: autoinkrementujący klucz główny;
- [VERSIONING]: numer wersji kategorii;
- [NOM]: unikalna nazwa kategorii;
Teraz opiszemy encje JPA, [Produit] i [Categorie], które są obrazami tabel [PRODUITS] i [CATEGORIES].
![]() |
6.3.3.1. Interfejs [AbstractCoreEntity]
Interfejs [AbstractCoreEntity] jest implementowany przez jednostki JPA, [Categorie] i [Produit]:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// metody pobierające i ustawiające dla pól [id], [version], [entityType]
public Long getId();
public void setId(Long id);
public Long getVersion();
public void setVersion(Long version);
public enum EntityType {
PROXY, POJO
}
public EntityType getEntityType();
public void setEntityType(EntityType entityType);
}
Ten interfejs, zaimplementowany przez dwie jednostki JPA, służy po prostu do wyliczenia metod odczytu i zapisu pól [id], [version] oraz [entityType] tych jednostek. Rola pola [entityType] zostanie wyjaśniona w dalszej części;
6.3.3.2. Entyteta JPA [Produit]
Klasa [Produit] to jednostka JPA powiązana z wierszem tabeli [PRODUITS]:
![]() |
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_PRODUITS)
@JsonFilter("jsonFilterProduit")
public class Produit implements AbstractCoreEntity {
// właściwości
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// właściwości
@Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
private String nom;
@Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
private Long idCategorie;
@Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
private double prix;
@Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
private String description;
// kategoria
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// producenci
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
this.id = id;
this.version = version;
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// podpis
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// ------------------------------------------------------------
// przedefiniowanie [equals] i [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// metody pobierające i ustawiające
...
public void setCategorie(Categorie categorie) {
// typ encji
if (entityType == EntityType.PROXY) {
throw new ProxyException(1005, new RuntimeException(
"On ne peut changer la catégorie d'un produit de type [PROXY]"), simpleClassName);
}
this.categorie = categorie;
}
}
- wiersz 21: adnotacja [@Entity] sprawia, że klasa [Produit] staje się encją zarządzaną przez warstwę [JPA]. Można również zapisać [@Entity(name="MonProduit")], co nadaje jednostce nazwę [MonProduit]. W przypadku braku tej informacji nazwa jednostki jest zgodna z nazwą klasy, w tym przypadku [Produit]. Takie nazewnictwo staje się konieczne, gdy wśród jednostek znajdują się dwie klasy z różnych pakietów o tej samej nazwie;
- wiersz 22: adnotacja [@Table(name = "PRODUITS")] wskazuje, że klasa [Produit] jest obiektem odpowiadającym wierszowi w tabeli [PRODUITS] w bazie danych;
- wiersz 23: nazwa filtra jSON, który ma zostać zastosowany do encji. Zobaczymy, że właściwość [categorie] z wiersza 58 nie zawsze jest dostępna. Należy ją zatem 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 26: adnotacja [@Id] sprawia, że pole z adnotacją staje się polem powiązanym z kluczem głównym tabeli z wiersza 19;
- wiersz 27: adnotacja [@GeneratedValue(strategy = GenerationType.IDENTITY)] określa tryb automatycznego generowania klucza głównego w tabeli [PRODUITS]. Określa go atrybut [strategy]. Istnieją różne tryby:

Strategia [IDENTITY] nie jest dostępna dla wszystkich SGBD. Spośród sześciu przetestowanych SGBD była ona dostępna dla SGBD i [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.5]. W przypadku pozostałych dwóch [Oracle Express 11g Release 2, Firebird 2.5.4] konieczne było zastosowanie strategii [SEQUENCE]. W celu zapewnienia przenośności między implementacjami JPA nie należy stosować strategii [AUTO], która pozostawia wybór strategii generowania klucza głównego w gestii danej implementacji JPA. Zatem w przypadku MySQL 5 i strategii [AUTO]:
- Hibernate wybiera strategię [IDENTITY] z trybem [AUTO_INCREMENT] dla klucza głównego;
- EclipseLink wybiera strategię [TABLE], która domyślnie tworzy tabelę o nazwie [SEQUENCE], do której należy skierować zapytanie, aby uzyskać klucze główne.
Ostatecznie struktura bazy danych zarządzanej przez te dwie implementacje JPA nie jest taka sama. Jeśli została wygenerowana przez Hibernate, nie będzie mogła być wykorzystana przez EclipseLink i odwrotnie.
- wiersz 28: adnotacja [@Column(name="ID"] określa nazwę kolumny tabeli [PRODUITS], którą należy powiązać z polem [id];
- wiersz 29: jako klucz główny stosuje się typ [Long] zamiast [long]. Klucze główne [null] mają bowiem szczególne znaczenie dla JPA. W związku z tym w tym miejscu lepiej jest użyć typu obiektowego zamiast typu prostego;
- wiersz 31: 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) różną od tej w bazie danych (V1+1);
- wiersz 36: typ encji. Będą dwa: POJO i PROXY. Domyślnie instancja produktu będzie miała typ POJO (Plain Old Java Object). W niektórych przypadkach instancje [Produit] pobrane z bazy danych będą miały typ [PROXY]. Będzie to miało miejsce w sytuacji, gdy właściwość [Categorie categorie] w wierszu 58 nie zostanie zainicjowana kategorią z powodu atrybutu [fetch = FetchType.LAZY] w wierszu 56. W tym przypadku implementacje JPA, które będą testowane, różnią się:
- [Hibernate, OpenJPA]: próba uzyskania dostępu do kategorii produktu typu [PROXY] powoduje wyjątek. W Hibernate termin „proxy” oznacza instancję JPA uzyskaną w trybie [LAZY]. Dlatego użyłem tego terminu do określenia tego typu encji;
- [EclipseLink]: dostęp do kategorii produktu typu [PROXY] powoduje wyszukanie tej kategorii w bazie danych i nie występuje wyjątek;
Ponieważ chciałem mieć warstwę testową niezależną od używanej implementacji JPA, chciałem poznać typ każdej encji: POJO lub PROXY. Dlatego dodałem pole [entityType] do encji JPA;
- wiersz 35: adnotacja [@Transient] wskazuje, że implementacja JPA powinna zignorować to pole. Nie istnieje ono bowiem w tabelach SGBD;
- wiersz 40: klasa [Produit] zgłasza wyjątek typu [ProxyException], który wymaga nazwy klasy;
- wiersz 38: podobnie jak poprzednio wskazano, że implementacja JPA powinna zignorować to pole;
- wiersz 39: adnotacja [@JsonIgnore] wskazuje, że serializator/deserializator jSON instancji [Produit] musi zignorować to pole;
- wiersz 43: adnotacja [@Column] powiązuje pole [nom] z kolumną [NOM] w tabeli [PRODUITS]. Gdy pole ma taką samą nazwę jak powiązana kolumna (bez rozróżniania wielkości liter), adnotację [@Column] można pominąć. Tak jest w tym przypadku. Atrybuty [unique = true, length = 30, nullable = false] są używane tylko wtedy, gdy implementacja JPA ma wygenerować tabelę [CATEGORIES] na podstawie encji [Produit]. Zostaną one przekształcone przez atrybuty SQL i [UNIQUE, VARCHAR(30), NOT NULL], co sprawia, że kolumna [NOM] będzie miała maksymalnie 30 znaków, będzie unikalna w tabeli i nie może przyjmować wartości NULL;
- wiersze 46–47: pole [idCategorie] powiązane z kolumną [CATEGORIE_ID]. Do jego atrybutów powrócimy nieco później;
- wiersze 49–50: pole [prix] jest powiązane z kolumną [PRIX];
- wiersze 52–53: pole [description] jest powiązane z kolumną [DESCRIPTION];
- wiersze 56–58: kategoria produktu;
- wiersz 56: adnotacja [@ManyToOne] wskazuje, że kolumna adnotacji z wiersza 57 [@JoinColumn(name = "CATEGORIE_ID")] jest kluczem obcym tabeli [PRODUITS]entytetu [Produit] w tabeli [CATEGORIES] powiązanej z entytetem z wiersza 58. Adnotacja ta musi odnosić się do entytetu JPA. Zatem klasą w wierszu 58 musi być jednostka JPA;
- wiersz 56: adnotacja [fetch = FetchType.LAZY] wymaga, aby podczas pobierania produktu z tabeli [PRODUITS] jego kategoria (wiersz 58) nie była pobierana od razu (lazy loading). Jest ona wówczas pobierana przy pierwszym wywołaniu metody [getCategorie]. W tym celu, podczas wykonywania, warstwa JPA rozszerza pierwotną metodę [getCategorie] (która ogranicza się do zwracania pola categorie) poprzez wywołanie metody SGBD w celu pobrania kategorii – jest to technika zwana „proxyingiem”. Implementacje JPA różnią się między sobą pod względem realizacji tej funkcji, jak wspomniano powyżej. Atrybut ten nie jest obowiązkowy. Wykorzystywana implementacja JPA może go zignorować. Ponieważ właściwość [categorie] może występować lub nie, wprowadziliśmy filtr jSON w wierszu 23. Kolumna łącząca [CATEGORIE_ID] w tabeli [PRODUITS] jest automatycznie aktualizowana podczas wstawiania lub aktualizacji produktu. Otrzymuje ona wartość z pola [categorie.getId()], gdzie [categorie] jest polem z wiersza 58. Specyfikacja JPA nakłada ograniczenie, zgodnie z którym ta kolumna łącząca nie może być aktualizowana w żaden inny sposób. W związku z tym narzuca ona atrybuty [insertable = false, updatable = false] z wiersza 46, które powodują, że kolumna [CATEGORIE_ID] (czyli kolumna łącząca) powiązana z polem [idCategorie] nie może być modyfikowana przez pole [idCategorie]. Możliwe będzie jedynie przeniesienie kolumny [CATEGORIE_ID] do pola [idCategorie];
- wiersze 91–104: równość między encjami [Produit] definiuje się jako równość między ich kluczami głównymi [id];
- wiersze 108–115: aby zapewnić przenośność naszej warstwy testowej, będziemy jednolicie obsługiwać encje [PROXY] w trzech implementacjach: JPA i [Hibernate, EclipseLink, OpenJpa]. W przypadku typu [Produit], będącego podtypem typu [PROXY], zabronimy zmiany wartości pola [categorie]. Klasa [ProxyException] wygląda następująco:
![]() |
package generic.jpa.infrastructure;
import generic.jdbc.infrastructure.UncheckedException;
public class ProxyException extends UncheckedException {
private static final long serialVersionUID = 7278276670314994574L;
public ProxyException() {
}
public ProxyException(int code, Throwable e, String simpleClassName) {
super(code, e, simpleClassName);
}
}
Na zakończenie analizy tej jednostki należy zauważyć, że adnotacje i ich atrybuty są wykorzystywane w dwóch wyraźnie odrębnych przypadkach:
- do tworzenia tabel bazy danych;
- do ich wykorzystywania. W tym przypadku implementacja JPA oczekuje, że tabele będą takie, jak te, które sama wygenerowałaby. Nie można zatem powiązać poprzedniej jednostki [Produit] z dowolną tabelą [PRODUITS]. Musi ona posiadać co najmniej (może mieć również inne) cechy charakterystyczne tabeli [PRODUITS], którą sama by wygenerowała. Podczas pracy z JPA najlepiej jest zacząć od pustej bazy danych, w której pozwolimy JPA wygenerować tabele. Kwestię tego generowania omówimy nieco później. Skrypt SQL dostarczony dla SGBD i MySQL został wygenerowany na podstawie tabel wygenerowanych przez JPA.
Wszystkie atrybuty jednostki [Produit] są wykorzystywane do generowania tabeli [PRODUITS]. Po zakończeniu tego procesu atrybuty generowania, takie jak [unique = true, length = 30, nullable = false], nie są już wykorzystywane podczas przetwarzania tabel.
6.3.3.3. Entyteta JPA [Categorie]
Klasa [Categorie] jest jednostką JPA powiązaną z wierszem tabeli [CATEGORIES]:
![]() |
Jej kod jest następujący:
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategorie")
public class Categorie implements AbstractCoreEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// właściwości
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// powiązane produkty
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// konstruktorzy
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
this.id = id;
this.version = version;
this.nom = nom;
this.produits = produits;
}
// sygnatura
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// metody
public void addProduit(Produit produit) {
// typ jednostki
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// dodanie produktu
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// dodajemy produkt
produits.add(produit);
// ustalanie kategorii
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// ------------------------------------------------------------
// przedefiniowanie [equals] i [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// metody pobierające i ustawiające
...
}
- wiersz 24: klasa jest encją JPA;
- wiersz 25: powiązana z tabelą [CATEGORIES];
- wiersz 26: reprezentacja jSON encji [Categorie] jest sterowana przez filtr o nazwie [jsonFilterCategorie]. Należy go skonfigurować przed każdym żądaniem reprezentacji jSON tej jednostki. Filtr [jsonFilterCategorie] zostanie wykorzystany do wykluczenia lub uwzględnienia w reprezentacji jSON encji [Categorie] pola [produits] z wiersza 40;
- wiersze 29–32: pole [id] jest powiązane z kluczem głównym [ID] w tabeli [CATEGORIES]. Wybranym trybem generowania jest tryb [IDENTITY], a zatem tryb [AUTO_INCREMENT] dla MySQL;
- wiersze 34–36: pole [version] jest powiązane z kolumną wersjonowania [VERSIONING] w tabeli [CATEGORIES];
- wiersze 38–39: typ encji [Categorie];
- wiersze 41–43: nazwa zwykła klasy [Categorie];
- wiersze 46–47: pole [nom] jest powiązane z kolumną [NOM] w tabeli [CATEGORIES]. Nadano mu atrybuty JPA i [unique = true, length = 30, nullable=false], aby podczas generowania tabeli [CATEGORIES] kolumna [NOM] posiadała atrybuty SQL i [UNIQUE, VARCHAR(30), NOT NULL];
- wiersze 50–51: produkty należące do tej kategorii;
- wiersz 50: adnotacja [@OneToMany] stanowi relację odwrotną do relacji [@ManyToOne], którą spotkaliśmy w encji [Produit]. Atrybut [mappedBy = "categorie"] wskazuje pole encji [Produit], na którym umieszczono adnotację odwrotnej relacji [@ManyToOne]. Atrybut [cascade = { CascadeType.ALL }] wymaga, aby operacje (persist, merge, remove) wykonywane na @Entity [Categorie] były kaskadowo stosowane do [produits] w wierszu 51. Można określić kaskadę częściową za pomocą stałych [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
- wiersz 50: atrybut [fetch = FetchType.LAZY] określa, że po pobraniu kategorii z tabeli [CATEGORIES] jej produkty nie są od razu pobierane. Są one pobierane dopiero przy pierwszym wywołaniu metody [getProduits]. W tym celu podczas wykonywania warstwa JPA 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. 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 26, który pozwoli nam wskazać, czy chcemy tę właściwość, oraz typ encji w wierszu 39;
- wiersze 71–88: metoda [addProduit] pozwala dodać produkt do kategorii;
- wiersze 73–76: w celu ujednolicenia zarządzania proxy między różnymi implementacjami JPA zdecydowano, że nie można dodawać produktów do encji [Categorie] typu PROXY;
- wiersze 92–112: dwa obiekty typu [Categorie] będą uznawane za równe, jeśli mają ten sam klucz główny [id];
6.3.4. Plik [persistence.xml]
![]() |
Aplikacje JPA muszą zdefiniować pewne właściwości używanego dostawcy JPA, a także encje JPA, które mają być używane, w pliku [META-INF/persistence.xml] znajdującym się w ścieżce Classpath aplikacji. W powyższym przykładzie plik ten umieszczono w folderze [src/main/resources], który faktycznie stanowi część ścieżki Classpath projektu Eclipse. Podczas korzystania z JPA w połączeniu ze Springiem niektóre informacje, które powinny znajdować się w pliku [persistence.xml], są umieszczane w innych miejscach, w klasach konfiguracyjnych Springa. W aplikacji Spring JPA to Spring steruje plikiem JPA. W przypadku Spring JPA z Hibernate plik [persistence.xml] można zredukować do najprostszej postaci:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
- wiersze 1–5: plik [persistence.xml] musi zawierać tag nadrzędny <persistence>. Atrybuty tego tagu w wierszu 2 nie będą wykorzystywane w tej aplikacji;
- plik trwałości może definiować jedną lub więcej jednostek trwałości za pomocą tagu <persistence-unit> (wiersz 4). Jednostka trwałości zarządza dostępem do konkretnej bazy danych. Jeśli aplikacja obsługuje jednocześnie dwie bazy danych, będzie miała dwie jednostki trwałości;
- wiersz 4: jednostka trwałości ma nazwę [attribut name], obsługuje typ transakcji [attribut transaction-type], posiada właściwości oraz definiuje encje powiązane z tabelami bazy danych zarządzanej przez tę jednostkę trwałości. Ponieważ w tym przypadku dostęp do bazy danych będzie zarządzany przez jednostkę [Spring JPA Hibernate], te dwie ostatnie informacje można umieścić w innym miejscu. Istnieją dwa rodzaje transakcji:
- [RESOURCE_LOCAL]: transakcje są zarządzane przez samą aplikację. Tak jest w tym przypadku, gdzie to Spring będzie zarządzać transakcjami;
- [JTA] (Java Transaction API): to kontener EJB (Enterprise Java Bean), w którym uruchomiona jest aplikacja, automatycznie zarządza transakcjami na podstawie adnotacji Java znalezionych w kodzie. W tej konfiguracji tak nie jest;
W dalszej części zobaczymy, że zawartość tego pliku [persistence.xml] zależy od używanej implementacji JPA.
6.4. Projekt [spring-jpa-generic]
Przypomnijmy sobie, co chcemy osiągnąć. Chcemy zaimplementować następującą architekturę:
![]() |
w której warstwa [DAO] implementowałaby interfejs [IDao<Produit>, IDao<Categorie>] omówiony w rozdziale 4. Chodzi o porównanie dwóch implementacji tego interfejsu:
- jedna zbudowana przy użyciu Springa JDBC;
- druga zbudowana przy użyciu Springa JPA;
W powyższej architekturze:
- warstwa [JDBC] jest zaimplementowana przez projekt [mysql-config-jdbc] omówiony w punkcie 3.3;
- warstwa [JPA] jest zaimplementowana przez projekt [mysql-config-jpa-hibernate] omówiony w paragrafie 6.3;
Projekt [spring-jpa-generic] zapewnia implementację warstw [DAO] i [Spring Data].
![]() |
6.4.1. Konfiguracja Maven
Projekt [spring-jpa-generic] jest projektem Maven skonfigurowanym przez następujący plik [pom.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jpa-generic</name>
<description>démo spring data avec tables de catégories et de produits</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- konfiguracja JPA dla SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</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>
- wiersze 22–26: projekt ma tylko jedną zależność – od projektu, który konfiguruje warstwę [JPA] aplikacji i który właśnie przeanalizowaliśmy. Jest to aplikacja generyczna:
- zmianę na SGBD dokonuje się poprzez zmianę projektu konfiguracyjnego warstwy [JDBC];
- zmianę implementacji JPA dokonuje się poprzez zmianę projektu konfiguracyjnego warstwy [JPA];
Ostatecznie zależności są następujące:
![]() |
6.4.2. Konfiguracja Spring
![]() |
Klasa [AppConfig] konfiguruje projekt Spring:
package spring.data.config;
import generic.jpa.config.ConfigJpa;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
- wiersz 11: klasa ta jest klasą konfiguracyjną Spring;
- wiersz 10: adnotacja [@EnableJpaRepositories] służy do wskazania pakietów zawierających interfejsy [CrudRepository] z biblioteki Spring Data. Dzięki temu stają się one komponentami Springa, które mogą być wstrzykiwane do innych komponentów Springa;
- wiersz 12: adnotacja [@ComponentScan] wskazuje, że należy przeszukać pakiet [spring.data.dao] w poszukiwaniu komponentów Spring. Zostaną znalezione komponenty [DaoCategorie] i [DaoProduit];
- wiersz 13: importowane są bean klasy konfiguracyjnej [ConfigJpa]. Znajduje się tam bean implementacji JPA wykorzystywanej (Hibernate, Eclipselink, OpenJpa), źródło danych do wykorzystania, EntityManager, który będzie zarządzał operacjami JPA, menedżer transakcji;
6.4.3. Warstwa [Spring Data]
![]() |
![]() |
6.4.3.1. Interfejs [CategoriesRepository]
Interfejs [CategoriesRepository] zarządza dostępem do tabeli [CATEGORIES]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Categorie;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
// kategoria wraz z produktami
@Query("select c from Categorie c left join fetch c.produits where c.id=?1")
public Categorie getLongCategorieById(Long id);
@Query("select c from Categorie c left join fetch c.produits where c.nom=?1")
public Categorie getLongCategorieByName(String nom);
@Query("select c from Categorie c where c.nom in ?1")
public List<Categorie> getShortCategoriesByName(Iterable<String> names);
@Query("select c from Categorie c where c.id in ?1")
public List<Categorie> getShortCategoriesById(Iterable<Long> ids);
@Query("select distinct c from Categorie c left join fetch c.produits where c.id in ?1")
public List<Categorie> getLongCategoriesById(List<Long> names);
@Query("select distinct c from Categorie c left join fetch c.produits where c.nom in ?1")
public List<Categorie> getLongCategoriesByName(List<String> names);
@Query("select c from Categorie c")
public List<Categorie> getAllShortCategories();
@Query("select distinct c from Categorie c left join fetch c.produits")
public List<Categorie> getAllLongCategories();
}
- wiersz 10: interfejs [CrudRepository] został wykorzystany i wyjaśniony w paragrafie 5.1.3. Przypominamy, że:
- pierwszym typem parametru interfejsu jest entyteta JPA zarządzana dla operacji dostępu CRUD (findOne, findAll, zapis, usunięcie, deleteAll),
- drugim typem parametru interfejsu jest klucz główny encji JPA, w tym przypadku liczba całkowita [Long];
Metody interfejsu są implementowane za pomocą zapytań JPQL (Java Persistence Query Language). 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żywanych w zapytaniu;
Weźmy na przykład wiersze 31–32: metoda w wierszu 32 zwraca wszystkie kategorie z bazy w ich skróconej wersji. Jest ona zaimplementowana przez zapytanie JPQL (Java Persistence Query Language) z wiersza 31, które bardzo przypomina swój odpowiednik SQL. Aby zgłębić temat JPQL, warto zapoznać się z [ref2] (patrz punkt 1.2).
Metody interfejsu [CategoriesRepository] są następujące:
- wiersze 13–14: metoda [getLongCategorieById] zwraca rozszerzoną wersję kategorii, do której odwołuje się klucz podstawowy [id], tj. 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 16–17: metoda [getLongCategorieByName] zwraca pełną wersję kategorii, na którą odwołuje się jej nazwa [nom];
- wiersze 19–20: metoda [getShortCategoriesByName] zwraca skrócone wersje kategorii, do których odwołuje się za pomocą ich nazw. Pole [produits] tych kategorii nie jest równe null. Zawiera ono odwołanie do proxy (klasy utworzonej przez implementację JPA), którego rolą jest zwracanie produktów z danej kategorii po wywołaniu. Wywołanie tej metody poza kontekstem trwałości JPA powoduje wyjątek (Hibernate i OpenJpa, ale nie EclipseLink). Z tego powodu nie będziemy korzystać z pola [produits] w skróconej wersji kategorii;
- wiersze 22–23: metoda [getShortCategoriesById] zwraca skrócone wersje kategorii, do których odwołują się ich klucze główne [id];
- wiersze 25–26: metoda [getLongCategoriesById] zwraca długie wersje kategorii, do których odwołują się ich klucze główne [id];
- wiersze [28-29]: metoda [getLongCategoriesByName] zwraca długie wersje kategorii, do których odwołują się ich nazwy;
- wiersze 31–32: metoda [getAllShortCategories] zwraca skrócone wersje wszystkich kategorii;
- wiersze 34–35: metoda [getAllLongCategories] zwraca pełne nazwy wszystkich kategorii;
Uwaga: nie wszystkie implementacje JPA akceptują tę samą składnię co JPQL. W związku z tym następująca składnia jest akceptowana przez Hibernate i EclipseLink, ale nie przez OpenJpa:
@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
OpenJpa nie akceptuje powyższego aliasu [p].
6.4.3.2. Interfejs [ProduitsRepository]
Interfejs [ProduitsRepository] zarządza dostępem do tabeli [PRODUITS]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
@Transactional()
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
// produkt wraz z kategorią
@Query("select p from Produit p left join fetch p.categorie where p.id=?1")
public Produit getLongProduitById(Long id);
@Query("select p from Produit p left join fetch p.categorie where p.nom=?1")
public Produit getLongProduitByName(String nom);
@Query("select p from Produit p where p.id in ?1")
public List<Produit> getShortProduitsById(List<Long> ids);
@Query("select p from Produit p where p.nom in ?1")
public List<Produit> getShortProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie where p.id in ?1")
public List<Produit> getLongProduitsById(List<Long> ids);
@Query("select distinct p from Produit p left join fetch p.categorie where p.nom in ?1")
public List<Produit> getLongProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie")
public List<Produit> getAllLongProduits();
@Query("select p from Produit p")
public List<Produit> getAllShortProduits();
}
- Wiersze [15-16]: metoda [getLongProduitById] zwraca pełną wersję produktu zidentyfikowanego za pomocą klucza głównego [id], a więc wraz z jego kategorią. Przypomnijmy, że w encji [Produit] pole [categorie] miało atrybut [fetch = FetchType.LAZY] (lazy loading). W zapytaniu JPQL wymuszamy załadowanie kategorii za pomocą słowa kluczowego [fetch];
- wiersze 18–19: metoda [getLongProduitByName] zwraca pełną wersję produktu zidentyfikowanego na podstawie nazwy;
- wiersze 21–22: metoda [getShortProduitsById] zwraca skróconą wersję produktów zidentyfikowanych za pomocą klucza głównego [id]. W tej skróconej wersji pole [categorie] nie ma wartości null. Zawiera ono odwołanie do proxy wygenerowanego przez implementację JPA, które w przypadku wywołania pobierze kategorię produktu. Wywołanie to może nastąpić wyłącznie w kontekście trwałości JPA. Wykonanie go w innym miejscu powoduje wyjątek (Hibernate i OpenJpa, ale nie EclipseLink). Dlatego w warstwie [DAO] ani nigdzie indziej nie będziemy używać pola [categorie] produktu w jego wersji skróconej. W skróconej wersji produktu inicjowane jest pole [idCategorie]. Jego wartością jest klucz podstawowy kategorii, do której należy produkt. Pozwala to później na pobranie tej kategorii z warstwy [DAO] za pomocą metody [DaoCategorie. getShortCategoriesById(idCategorie)];
- wiersze 24–25: metoda [getShortProduitsByName] zwraca skróconą wersję produktów zidentyfikowanych na podstawie ich nazw;
- wiersze 27–28: metoda [getLongProduitsById] zwraca długą wersję produktów zidentyfikowanych na podstawie ich kluczy głównych;
- wiersze 30–31: metoda [getLongProduitsByName] zwraca pełną wersję produktów zidentyfikowanych na podstawie ich nazw;
- wiersze 33–34: metoda [getAllLongProduits] zwraca pełne nazwy wszystkich produktów;
- wiersze 36–37: metoda [getAllShortProduits] zwraca skróconą wersję wszystkich produktów;
Interfejsy te zostaną zaimplementowane przez klasy wygenerowane przez implementację JPA w trakcie wykonywania projektu. Klasy takie nazywane są klasami [proxy]. Domyślnie metody interfejsu [CrudRepository] są wykonywane w ramach transakcji. Fakt, że interfejsy [ProduitsRepository, CategoriesRepository] dziedziczą po klasie [CrudRepository], sprawia, że są one komponentami Spring. W związku z tym mogą być wstrzykiwane do innych komponentów Spring.
6.4.4. Warstwa [DAO]
![]() |
![]() |
6.4.4.1. Interfejs [IDao<T>]
Interfejs [IDao<T>] to ten sam, który został już omówiony w ramach implementacji warstwy [DAO] wykonanej przy użyciu Springa JDBC (patrz punkt 4.7);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
public interface IDao<T extends AbstractCoreEntity> {
// lista wszystkich podmiotów T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// poszczególnych podmiotów – wersja skrócona
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// poszczególne jednostki – wersja długa
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// aktualizacja wielu jednostek
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// usunięcie wszystkich elementów
public void deleteAllEntities();
// usunięcie wielu elementów
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
6.4.4.2. Klasa abstrakcyjna [AbstractDao]
![]() |
Klasa abstrakcyjna [AbstractDao] jest klasą nadrzędną dla klas implementujących warstwę [DAO]:
- klasa [DaoProduit], która implementuje interfejs [IDao<Produit>] i zarządza dostępem do tabeli [PRODUITS];
- klasa [DaoCategorie], która implementuje interfejs [IDao<Categorie>] i zarządza dostępem do tabeli [CATEGORIES];
Jej kod jest zgodny z opisem w paragrafie 4.8, z następującym drobnym wyjątkiem: żadna metoda nie posiada atrybutu [@Transactional], który powoduje, że metoda jest wykonywana w ramach transakcji. Wykorzystujemy tutaj fakt, że interfejsy [CrudRepository] biblioteki Spring Data są domyślnie wykonywane w ramach transakcji.
6.4.4.3. Klasa [DaoCategorie]
![]() |
Klasa [DaoCategorie] implementuje interfejs [IDao<Categorie>] w następujący sposób:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Categorie> getAllShortEntities() {
try {
return setShortCategoriesType(categoriesRepository.getAllShortCategories());
} catch (Exception e) {
throw new DaoException(211, e, simpleClassName);
}
}
private List<Categorie> setShortCategoriesType(List<Categorie> categories) {
for (Categorie categorie : categories) {
categorie.setEntityType(EntityType.PROXY);
}
return categories;
}
@Override
public List<Categorie> getAllLongEntities() {
try {
return categoriesRepository.getAllLongCategories();
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
categoriesRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return categoriesRepository.getLongCategoriesById(ids);
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return categoriesRepository.getLongCategoriesByName(names);
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
categoriesRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
categoriesRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(212, e, simpleClassName);
}
}
}
- wiersz 17: adnotacja [@Component] sprawia, że klasa [DaoCategorie] staje się komponentem Spring;
- wiersz 18: klasa [DaoCategorie] dziedziczy po klasie [AbstractDao<Categorie>], co powoduje, że implementuje ona interfejs [IDao<Categorie>];
- wiersze 20–24: wstrzykiwanie referencji do obu interfejsów [CrudRepository] z [Spring Data]. Wstrzykiwanie to nastąpi podczas instancjonowania obiektów Springa, zazwyczaj na początku wykonywania projektu Springa;
- wszystkie metody klasy przekazują wykonanie zadań metodom o tych samych nazwach w interfejsach [CrudRepository];
- wszystkie metody, które zwracają encje w ich skróconej wersji, wskazują to poprzez ustawienie typu encji na [EntityType.PROXY] (wiersze 29, 63, 72);
Metoda [saveEntities] zasługuje na wyjaśnienie:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// odnotowujemy produkty, które zostaną dodane
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// przy okazji przywracamy (w razie potrzeby) relację produkt --> kategoria
produit.setCategorie(categorie);
}
}
}
// zapisujemy kategorie / produkty
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// aktualizujemy pole [idCategorie] dla dodanych produktów
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// wynik
return categories;
}
- wiersz 2: kategorie przekazane jako parametry to zarówno kategorie do wstawienia ([id==null]), jak i do modyfikacji ([id!=null]);
- wiersz 20: kategorie są zapisywane na stałe za pomocą metody [categoriesRepository.save(entities)]. Podczas testów stwierdzono, że pole [idCategorie] dla produktów zapisanych na stałe (id==null) nie jest wypełnione. Aby rozwiązać ten problem, w wierszach 4–17 odnotowujemy produkty, które mają zostać wstawione, a po ich zapisaniu wypełniamy ich pole [idCategorie] (wiersze 25–27);
- wiersze 5–17: przeglądamy listę kategorii;
- wiersze 8–16: dla każdej kategorii przeglądamy jej listę produktów. Tutaj pojawia się trudność. Metoda [saveEntities] służy zarówno do zapisywania, jak i do modyfikowania kategorii. W tym drugim przypadku kategoria mogła zostać pobrana w wersji skróconej, a zatem w polu [produits] znajduje się odwołanie do metody proxy. Wykorzystanie jej z Hibernate powoduje wówczas wyjątek, ponieważ wykorzystywana kategoria nie znajduje się już w kontekście trwałości JPA, który został zamknięty wraz z zakończeniem transakcji metody, która zwróciła skrócone wersje kategorii. W tym momencie wykorzystuje się pole [EntityType] w encji [Categorie] w wierszu 8, aby sprawdzić, czy można uzyskać dostęp do listy produktów z tej kategorii;
- wiersz 14: produkt jest powiązany z kategorią. Zazwyczaj powinno to już mieć miejsce. Nie wiadomo jednak, w jaki sposób produkt ten został utworzony i czy został powiązany z kategorią. Aby więc uniknąć wszelkich problemów (aby zarządzać encją [Produit], encja JPA musi odwoływać się do encji [Categorie], z którą jest powiązana), sami tworzymy to powiązanie.
Porównując ten kod z kodem klasy [DaoProduit] w implementacji Spring JDBC (patrz punkt 4.9) można zauważyć, że biblioteka Spring Data JPA znacznie ułatwia pisanie kodu warstwy [DAO].
6.4.4.4. Klasa [DaoProduit]
![]() |
Klasa [DaoProduit] implementuje interfejs [IDao<Produit>] w następujący sposób:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Produit> getAllShortEntities() {
try {
return setShortProduitsType(produitsRepository.getAllShortProduits());
} catch (Exception e) {
throw new DaoException(102, e, simpleClassName);
}
}
private List<Produit> setShortProduitsType(List<Produit> produits) {
for (Produit produit : produits) {
produit.setEntityType(EntityType.PROXY);
}
return produits;
}
@Override
public List<Produit> getAllLongEntities() {
try {
return produitsRepository.getAllLongProduits();
} catch (Exception e) {
throw new DaoException(117, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
produitsRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsById(ids));
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsByName(names));
} catch (Exception e) {
throw new DaoException(104, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsById(ids));
} catch (Exception e) {
throw new DaoException(105, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsByName(names));
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
private List<Produit> linkLongProduitsToCategories(List<Produit> produits) {
for (Produit produit : produits) {
Categorie categorie = produit.getCategorie();
if (categorie != null) {
produit.setCategorie(categorie);
produit.setIdCategorie(categorie.getId());
}
}
return produits;
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// przywracamy (w razie potrzeby) powiązanie między produktem a jego kategorią
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// zapisuje się produkty
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
produitsRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(113, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
produitsRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(118, e, simpleClassName);
}
}
}
Kod jest analogiczny do kodu klasy [DaoCategorie]:
- w przypadku długich nazw kategorii testy wykazały, że pole [idCategorie] produktów nie jest wypełnione. Metoda [linkLongProduitsToCategories] w wierszach 96–105 rozwiązuje ten problem;
- metoda [saveEntities] w wierszach 108–121 dodaje nowe produkty lub modyfikuje istniejące. Warstwa JPA wymaga, aby każda jednostka [Produit] była powiązana z jednostką [Categorie]. Ponieważ nie wiadomo, czy użytkownik to zrobił, wykonujemy to samodzielnie w wierszach 110–113. Wystarczy powiązać [Produit] z jednostką [Categorie], której klucz główny jest równy polu [idCategorie] w [Produit]. Podczas testów okazuje się, że pojawia się błąd, jeśli jako wersję kategorii wprowadzimy null. Dlatego przypisujemy jej tutaj wartość 0, ale można wpisać dowolną wartość. Poza kluczem głównym żadne pole encji [Categorie] nie jest wymagane w warstwie JPA do wstawienia/modyfikacji encji [Produit];
6.4.5. Warstwa testowa
![]() |
![]() |
Powyższe testy są identyczne z testami w implementacji Spring JDBC. W razie potrzeby należy zapoznać się z następującymi stronami:
- [JUnitTestCheckArguments]: punkt 4.11.1;
- [JUnitTestDao]: punkt 4.11.2;
- [JUnitTestPushTheLimits]: punkt 4.11.3;
Stosujemy następujące konfiguracje uruchomieniowe:
![]() | ![]() |
![]() | ![]() |
Wyniki uzyskane w poszczególnych testach są następujące:
![]() | ![]() |
![]() |
W przypadku [1], test [JUnitTestPushTheLimits] z implementacją Spring Data JPA Hibernate oraz w [2] z implementacją Spring JDBC. Widać, że ta ostatnia jest wydajniejsza. Dochodzimy zatem do pierwszego wniosku: znacznie łatwiej jest opracować warstwę [DAO] przy użyciu Spring Data JPA, ale jest ona mniej wydajna niż implementacja Spring JDBC.
Test [JUnitTestProxies] jest testem fikcyjnym JUnit. Ma on na celu pokazanie zachowania każdej implementacji JPA w stosunku do proxy, czyli skróconych wersji encji:
package spring.data.tests;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.data.config.AppConfig;
import spring.data.dao.IDao;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {
// warstwa [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@Before
public void clean() {
// przed każdym testem czyści się bazę danych
log("Vidage de la base de données", 1);
// opróżnia się tabelę [CATEGORIES], a następnie kaskadowo tabelę [PRODUITS]
daoCategorie.deleteAllEntities();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// wypełnia się tabele
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
categorie.setProduits(new ArrayList<Produit>());
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// dodaje się kategorię – w konsekwencji produkty również zostaną
// dodane
daoCategorie.saveEntities(categories);
// wynik
return categories;
}
@Test
public void getShortCategoriesByName1() {
// wypełnianie
fill(1, 1);
// test
log("getShortCategoriesByName1", 1);
Categorie categorie = daoCategorie.getShortEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getShortProduitsByName1() {
// wypełnianie
fill(1, 1);
// test
log("getShortProduitsByName1", 1);
Produit produit = daoProduit.getShortEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongCategoriesByName1() {
// wypełnianie
fill(1, 1);
// test
log("getLongCategoriesByName1", 1);
Categorie categorie = daoCategorie.getLongEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongProduitsByName1() {
// napełnianie
fill(1, 1);
// test
log("getLongProduitsByName1", 1);
Produit produit = daoProduit.getLongEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
private 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);
}
}
Uzyskane wyniki są następujące:
Vidage de la base de données --------------------------------
doNothing
Vidage de la base de données --------------------------------
getShortCategoriesByName1 --------------------------------
Catégorie de type : PROXY
Catégorie :
Exception : org.hibernate.LazyInitializationException, Message : failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongCategoriesByName1 --------------------------------
Catégorie de type : POJO
Catégorie :
1
Vidage de la base de données --------------------------------
getShortProduitsByName1 --------------------------------
Produit de type : PROXY
Nom de la catégorie du produit :
Exception : org.hibernate.LazyInitializationException, Message : could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongProduitsByName1 --------------------------------
Produit de type : POJO
Nom de la catégorie du produit :
categorie[0]
Widać tutaj, że podczas uzyskiwania dostępu do pola [Categorie.produits] w kategorii typu PROXY oraz do pola [Produit.categorie] w produkcie typu PROXY, w obu przypadkach (wiersze 7 i 17) pojawia się wyjątek typu [org.hibernate.LazyInitializationException].



































