Współczesny świat deweloperski jest pełen różnych specyfikacji mających na celu ułatwienie życia. Znając narzędzia, możesz wybrać właściwy. Nie wiedząc o tym, możesz utrudnić sobie życie. Ta recenzja uchyli zasłonę tajemnicy nad koncepcją JPA - Java Persistence API. Mam nadzieję, że po przeczytaniu będziecie chcieli jeszcze głębiej zanurzyć się w ten tajemniczy świat.
Wstęp
Jak wiemy, jednym z głównych zadań programów jest przechowywanie i przetwarzanie danych. W starych, dobrych czasach ludzie po prostu przechowywali dane w plikach. Gdy jednak potrzebny jest jednoczesny dostęp do odczytu i edycji, gdy występuje obciążenie (tj. kilka żądań przychodzi w tym samym czasie), przechowywanie danych po prostu w plikach staje się problemem. Aby uzyskać więcej informacji o tym, jakie problemy rozwiązują bazy danych i w jaki sposób, radzę przeczytać artykuł „
Jak skonstruowana jest baza danych ”. Oznacza to, że decydujemy się na przechowywanie naszych danych w bazie danych. Od dawna Java może współpracować z bazami danych za pomocą interfejsu API JDBC (The Java Database Connectivity). Więcej o JDBC możesz przeczytać tutaj: „
JDBC, czyli tam, gdzie wszystko się zaczyna ”. Czas jednak mijał i programiści za każdym razem borykali się z koniecznością pisania tego samego typu i niepotrzebnego kodu „konserwacyjnego” (tzw. kodu Boilerplate) dla trywialnych operacji zapisywania obiektów Java w bazie danych i odwrotnie, tworzenia obiektów Java z wykorzystaniem danych z bazy danych. Baza danych. A potem, aby rozwiązać te problemy, narodziła się taka koncepcja jak ORM.
ORM - Mapowanie obiektowo-relacyjne lub przetłumaczone na rosyjskie mapowanie obiektowo-relacyjne. Jest to technologia programowania, która łączy bazy danych z koncepcjami obiektowych języków programowania. W uproszczeniu ORM to połączenie między obiektami Java a rekordami w bazie danych:
ORM to zasadniczo koncepcja, zgodnie z którą obiekt Java może być reprezentowany jako dane w bazie danych (i odwrotnie). Zostało ono zawarte w postaci specyfikacji JPA - Java Persistence API. Specyfikacja jest już opisem interfejsu API Java, który wyraża tę koncepcję. Specyfikacja mówi nam, w jakie narzędzia musimy być wyposażeni (czyli jakie interfejsy możemy przepracować), aby pracować zgodnie z koncepcją ORM. I jak te środki wykorzystać. Specyfikacja nie opisuje implementacji narzędzi. Dzięki temu możliwe jest zastosowanie różnych implementacji dla jednej specyfikacji. Można to uprościć i powiedzieć, że specyfikacja jest opisem API. Tekst specyfikacji JPA można znaleźć na stronie internetowej Oracle: „
JSR 338: JavaTM Persistence API ”. Dlatego, aby móc korzystać z JPA, potrzebujemy implementacji, z którą będziemy korzystać z technologii. Implementacje JPA nazywane są także dostawcami JPA. Jedną z najbardziej godnych uwagi implementacji JPA jest
Hibernate . Dlatego proponuję to rozważyć.
Tworzenie projektu
Ponieważ JPA dotyczy języka Java, będziemy potrzebować projektu w języku Java. Moglibyśmy samodzielnie stworzyć strukturę katalogów i samodzielnie dodać niezbędne biblioteki. Ale o wiele wygodniej i poprawniej jest używać systemów do automatyzacji montażu projektów (tj. w istocie jest to po prostu program, który będzie za nas zarządzał montażem projektów. Twórz katalogi, dodawaj niezbędne biblioteki do ścieżki klas itp. .). Jednym z takich systemów jest Gradle. Więcej o Gradle możesz przeczytać tutaj: „
Krótkie wprowadzenie do Gradle ”. Jak wiemy, funkcjonalność Gradle (tj. to, co może zrobić) jest implementowana przy użyciu różnych wtyczek Gradle. Użyjmy Gradle i wtyczki „
Gradle Build Init Plugin ”. Uruchommy polecenie:
gradle init --type java-application
Gradle zrobi za nas niezbędną strukturę katalogów i utworzy podstawowy deklaratywny opis projektu w skrypcie budującym
build.gradle
. Mamy więc wniosek. Musimy przemyśleć, co chcemy opisać lub zamodelować za pomocą naszej aplikacji. Skorzystajmy z jakiegoś narzędzia do modelowania, na przykład:
app.quickdatabasediagrams.com W tym miejscu warto powiedzieć, że to, co opisaliśmy, to nasz „model domeny”. Domena to „obszar tematyczny”. Ogólnie rzecz biorąc, domena to po łacinie „posiadanie”. W średniowieczu tak nazywano tereny będące własnością królów lub panów feudalnych. A w języku francuskim stało się słowem „domena”, co po prostu tłumaczy się jako „obszar”. W ten sposób opisaliśmy nasz „model domeny” = „model podmiotu”. Każdy element tego modelu jest swego rodzaju „esencją”, czymś z prawdziwego życia. W naszym przypadku są to podmioty: Kategoria (
Category
), Temat (
Topic
). Utwórzmy osobny pakiet dla encji, na przykład z nazwą model. Dodajmy tam klasy Java opisujące encje. W kodzie Java takimi encjami są zwykłe
POJO , które mogą wyglądać tak:
public class Category {
private Long id;
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
Skopiujmy zawartość klasy i utwórzmy klasę przez analogię
Topic
. Będzie się różnił jedynie tym, co wie o kategorii, do której należy. Dodajmy zatem
Topic
do klasy pole kategorii i metody pracy z nim:
private Category category;
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
Teraz mamy aplikację Java, która ma własny model domeny. Teraz czas zacząć łączyć się z projektem JPA.
Dodanie JPA
Jak więc pamiętamy, JPA oznacza, że zapiszemy coś w bazie danych. Dlatego potrzebujemy bazy danych. Aby w naszym projekcie skorzystać z połączenia z bazą danych musimy dodać bibliotekę zależności umożliwiającą połączenie z bazą danych. Jak pamiętamy korzystaliśmy z Gradle, który stworzył dla nas skrypt budujący
build.gradle
. Opiszemy w nim zależności jakich potrzebuje nasz projekt. Zależności to te biblioteki, bez których nasz kod nie może działać. Zacznijmy od opisu zależności przy łączeniu się z bazą danych. Robimy to w ten sam sposób, w jaki byśmy to zrobili, gdybyśmy pracowali tylko z JDBC:
dependencies {
implementation 'com.h2database:h2:1.4.199'
Teraz mamy bazę danych. Możemy teraz dodać do naszej aplikacji warstwę odpowiedzialną za mapowanie naszych obiektów Java na koncepcje baz danych (od Java do SQL). Jak pamiętamy, wykorzystamy do tego implementację specyfikacji JPA o nazwie Hibernate:
dependencies {
implementation 'com.h2database:h2:1.4.199'
implementation 'org.hibernate:hibernate-core:5.4.2.Final'
Teraz musimy skonfigurować JPA. Jeśli przeczytamy specyfikację i sekcję „8.1 Jednostka trwałości”, będziemy wiedzieć, że jednostka trwałości to pewnego rodzaju kombinacja konfiguracji, metadanych i bytów. Aby JPA działała, musisz opisać w pliku konfiguracyjnym przynajmniej jedną jednostkę trwałości, która nazywa się
persistence.xml
. Jego lokalizacja jest opisana w rozdziale specyfikacji „8.2 Opakowanie jednostkowe trwałości”. Zgodnie z tą sekcją, jeśli mamy środowisko Java SE, to musimy umieścić je w katalogu głównym katalogu META-INF.
Skopiujmy treść z przykładu podanego w specyfikacji JPA w
8.2.1 persistence.xml file
rozdziale „ ”:
<persistence>
<persistence-unit name="JavaRush">
<description>Persistence Unit For test</description>
<class>hibernate.model.Category</class>
<class>hibernate.model.Topic</class>
</persistence-unit>
</persistence>
Ale to nie wystarczy. Musimy powiedzieć, kto jest naszym dostawcą JPA, tj. taki, który implementuje specyfikację JPA:
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
Teraz dodajmy ustawienia (
properties
). Niektóre z nich (zaczynające się od
javax.persistence
) to standardowe konfiguracje JPA i są opisane w specyfikacji JPA w sekcji „Właściwości 8.2.1.9”. Niektóre konfiguracje są specyficzne dla dostawcy (w naszym przypadku wpływają na Hibernate jako dostawcę Jpa. Nasz blok ustawień będzie wyglądał następująco:
<properties>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
<property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE" />
<property name="javax.persistence.jdbc.user" value="sa" />
<property name="javax.persistence.jdbc.password" value="" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
Teraz mamy konfigurację zgodną z JPA
persistence.xml
, istnieje dostawca JPA Hibernate i istnieje baza danych H2, a także istnieją 2 klasy, które są naszym modelem domeny. Sprawmy, żeby to wszystko w końcu zadziałało. W katalogu
/test/java
nasz Gradle uprzejmie wygenerował szablon testów jednostkowych i nazwał go AppTest. Wykorzystajmy to. Jak stwierdzono w rozdziale „7.1 Konteksty trwałości” specyfikacji JPA, podmioty w świecie JPA żyją w przestrzeni zwanej kontekstem trwałości. Ale nie współpracujemy bezpośrednio z kontekstem trwałości. W tym celu używamy
Entity Manager
lub „menedżer jednostki”. To on wie o kontekście i o tym, jakie byty w nim żyją. Wchodzimy w interakcję z
Entity Manager
„om”. Pozostaje więc tylko zrozumieć, skąd możemy to zdobyć
Entity Manager
? Zgodnie z rozdziałem „7.2.2 Uzyskiwanie menedżera jednostek zarządzanych przez aplikację” specyfikacji JPA, musimy użyć
EntityManagerFactory
. Uzbrójmy się zatem w specyfikację JPA i weźmy przykład z rozdziału „7.3.2 Uzyskanie fabryki Entity Manager w środowisku Java SE” i sformatujmy go w formie prostego testu jednostkowego:
@Test
public void shouldStartHibernate() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
EntityManager entityManager = emf.createEntityManager();
}
Ten test już wykaże błąd „Nierozpoznana wersja JPA Persistence.xml XSD”. Powodem jest to, że
persistence.xml
musisz poprawnie określić używany schemat, jak podano w specyfikacji JPA w sekcji „Schemat trwałości.xml 8.3”:
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
version="2.2">
Poza tym ważna jest kolejność elementów. Dlatego
provider
należy go określić przed wystawieniem klas. Następnie test przebiegnie pomyślnie. Zakończyliśmy bezpośrednie połączenie JPA. Zanim przejdziemy dalej, pomyślmy o pozostałych testach. Każdy z naszych testów będzie wymagał
EntityManager
. Upewnijmy się, że każdy test ma swój własny
EntityManager
na początku wykonania. Ponadto chcemy, aby baza danych była za każdym razem nowa. Z uwagi na to, że korzystamy z
inmemory
opcji wystarczy zamknąć
EntityManagerFactory
. Tworzenie
Factory
jest kosztowną operacją. Ale w przypadku testów jest to uzasadnione. JUnit pozwala określić metody, które zostaną wykonane przed (Przed) i po (Po) wykonaniu każdego testu:
public class AppTest {
private EntityManager em;
@Before
public void init() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
em = emf.createEntityManager();
}
@After
public void close() {
em.getEntityManagerFactory().close();
em.close();
}
Teraz przed wykonaniem jakiegokolwiek testu zostanie utworzony nowy
EntityManagerFactory
, co będzie wiązało się z utworzeniem nowej bazy danych, ponieważ
hibernate.hbm2ddl.auto
ma znaczenie
create
. A z nowej fabryki dostaniemy nową
EntityManager
.
Podmioty
Jak pamiętamy, stworzyliśmy wcześniej klasy opisujące nasz model domeny. Mówiliśmy już, że to są nasze „esencje”. To jest Byt, którym będziemy zarządzać za pomocą
EntityManager
. Napiszmy prosty test, aby zapisać istotę kategorii:
@Test
public void shouldPersistCategory() {
Category cat = new Category();
cat.setTitle("new category");
em.persist(cat);
}
Ale ten test nie zadziała od razu, bo... otrzymamy różne błędy, które pomogą nam zrozumieć, czym są byty:
-
Unknown entity: hibernate.model.Category
Dlaczego Hibernate nie rozumie, co Category
to jest entity
? Rzecz w tym, że podmioty muszą być opisane zgodnie ze standardem JPA.
Klasy jednostek muszą być opatrzone adnotacją @Entity
, zgodnie z rozdziałem „2.1 Klasa jednostek” specyfikacji JPA.
-
No identifier specified for entity: hibernate.model.Category
Jednostki muszą mieć unikalny identyfikator, za pomocą którego można odróżnić jeden rekord od drugiego.
Zgodnie z rozdziałem „2.4 Klucze podstawowe i tożsamość jednostki” specyfikacji JPA, „Każdy podmiot musi posiadać klucz podstawowy”, tj. Każdy podmiot musi mieć „klucz podstawowy”. Taki klucz podstawowy musi być określony w adnotacji@Id
-
ids for this class must be manually assigned before calling save()
Identyfikator musi skądś pochodzić. Można go określić ręcznie lub można go uzyskać automatycznie.
Dlatego też, jak wskazano w rozdziałach „11.2.3.3 GeneratedValue” i „11.1.20 GeneratedValue Annotation”, możemy określić adnotację @GeneratedValue
.
Aby więc klasa kategorii stała się bytem, musimy wprowadzić następujące zmiany:
@Entity
public class Category {
@Id
@GeneratedValue
private Long id;
Ponadto adnotacja
@Id
wskazuje, którego użyć
Access Type
. Więcej o typie dostępu można przeczytać w specyfikacji JPA w rozdziale „2.3 Typ dostępu”. Ujmując to bardzo krótko, bo... określiliśmy
@Id
powyżej pola (
field
), wówczas typ dostępu będzie domyślny
field-based
, a nie
property-based
. Dlatego dostawca JPA będzie czytać i przechowywać wartości bezpośrednio z pól. Gdybyśmy umieścili
@Id
powyżej gettera, wówczas
property-based
zastosowany zostałby dostęp, tj. za pomocą gettera i settera. Uruchamiając test widzimy także jakie żądania wysyłane są do bazy danych (dzięki opcji
hibernate.show_sql
). Ale podczas zapisywania nie widzimy żadnych
insert
„”. Okazuje się, że tak naprawdę niczego nie zaoszczędziliśmy? JPA umożliwia synchronizację kontekstu trwałości i bazy danych metodą
flush
:
entityManager.flush();
Ale jeśli wykonamy to teraz, pojawi się błąd:
żadna transakcja nie jest w toku . A teraz nadszedł czas, aby dowiedzieć się, w jaki sposób JPA wykorzystuje transakcje.
Transakcje JPA
Jak pamiętamy, JPA opiera się na koncepcji kontekstu trwałości. To jest miejsce, w którym żyją istoty. I zarządzamy podmiotami poprzez
EntityManager
. Kiedy wykonujemy polecenie
persist
, umieszczamy obiekt w kontekście. Mówiąc dokładniej, mówimy
EntityManager
„y”, że należy to zrobić. Ale ten kontekst to tylko obszar przechowywania. Czasami nazywa się ją nawet „pamięcią podręczną pierwszego poziomu”. Ale musi być podłączony do bazy danych. Komenda
flush
, która poprzednio zakończyła się niepowodzeniem z powodu błędu, synchronizuje dane z kontekstu trwałości z bazą danych. Ale to wymaga transportu, a ten transport jest transakcją. Transakcje w JPA opisano w sekcji „7.5 Kontrolowanie transakcji” specyfikacji. Istnieje specjalne API do korzystania z transakcji w JPA:
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
Musimy dodać do naszego kodu zarządzanie transakcjami, które będzie uruchamiane przed i po testach:
@Before
public void init() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
em = emf.createEntityManager();
em.getTransaction().begin();
}
@After
public void close() {
if (em.getTransaction().isActive()) {
em.getTransaction().commit();
}
em.getEntityManagerFactory().close();
em.close();
}
Po dodaniu w logu wstawiania zobaczymy wyrażenie w SQL, którego wcześniej nie było:
Zmiany zgromadzone w
EntityManager
transakcji zostały zatwierdzone (potwierdzone i zapisane) w bazie danych. Spróbujmy teraz odnaleźć naszą esencję. Utwórzmy test, aby wyszukać jednostkę po jej identyfikatorze:
@Test
public void shouldFindCategory() {
Category cat = new Category();
cat.setTitle("test");
em.persist(cat);
Category result = em.find(Category.class, 1L);
assertNotNull(result);
}
W takim wypadku otrzymamy zapisaną wcześniej encję, jednak w logu nie zobaczymy zapytań SELECT. A wszystko opiera się na tym, co mówimy: „Entity Manager, proszę znajdź mi encję Kategoria o ID=1.” A menedżer encji najpierw szuka w jego kontekście (używa czegoś w rodzaju pamięci podręcznej), a dopiero jeśli go nie znajdzie, szuka w bazie danych. Warto zmienić ID na 2 (nie ma czegoś takiego, zapisaliśmy tylko 1 instancję), a zobaczymy, że
SELECT
żądanie się pojawi. Ponieważ w kontekście nie znaleziono żadnych encji, a
EntityManager
baza danych próbuje znaleźć encję, istnieją różne polecenia, których możemy użyć do kontrolowania stanu encji w kontekście. Przejście jednostki z jednego stanu do drugiego nazywa się cyklem życia jednostki
lifecycle
.
Cykl życia jednostki
Cykl życia encji opisany jest w specyfikacji JPA w rozdziale „3.2 Cykl życia instancji encji”. Ponieważ podmioty żyją w kontekście i są kontrolowane przez
EntityManager
, wówczas mówią, że podmioty są kontrolowane, tj. zarządzany. Przyjrzyjmy się etapom życia jednostki:
Category cat = new Category();
cat.setTitle("new category");
entityManager.persist(cat);
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
entityManager.detach(cat);
Category managed = entityManager.merge(cat);
entityManager.remove(managed);
A oto schemat konsolidacji:
Mapowanie
W JPA możemy opisać relacje podmiotów pomiędzy sobą. Pamiętajmy, że zajmując się naszym modelem domenowym, przyglądaliśmy się już powiązaniom między bytami między sobą. Następnie skorzystaliśmy z zasobu
QuickdataBasediagrams.com :
Ustanawianie połączeń między jednostkami nazywa się mapowaniem lub powiązaniem (mapowaniami skojarzeń). Poniżej przedstawiono rodzaje stowarzyszeń, które można zakładać za pomocą JPA:
Przyjrzyjmy się encji
Topic
opisującej temat. Co możemy powiedzieć o podejściu
Topic
do
Category
? Wiele z nich
Topic
będzie należeć do jednej kategorii. Dlatego potrzebujemy stowarzyszenia
ManyToOne
. Wyraźmy tę relację w JPA:
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
Aby zapamiętać, jakie adnotacje umieścić, możesz pamiętać, że ostatnia część odpowiada za pole, nad którym wskazana jest adnotacja.
ToOne
- konkretny przypadek.
ToMany
- zbiory. Teraz nasze połączenie jest jednokierunkowe. Sprawmy, żeby była to komunikacja dwustronna. Dodajmy do tego
Category
wiedzę o wszystkich
Topic
, którzy zaliczają się do tej kategorii. Musi kończyć się na
ToMany
, ponieważ mamy listę
Topic
. Czyli postawa „Za dużo” tematów. Pozostaje pytanie -
OneToMany
czy
ManyToMany
:
Dobrą odpowiedź na ten sam temat można przeczytać tutaj: „
Wyjaśnij relację ORM oneToMany, manyToMany, jakbym miał pięć lat ”. Jeśli kategoria ma związek z
ToMany
tematami, to każdy z tych tematów może mieć tylko jedną kategorię, wtedy będzie to
One
, w przeciwnym razie
Many
. Zatem
Category
lista wszystkich tematów będzie wyglądać następująco:
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "topic_id")
private Set<Topic> topics = new HashSet<>();
I nie zapomnijmy zasadniczo
Category
napisać gettera, aby uzyskać listę wszystkich tematów:
public Set<Topic> getTopics() {
return this.topics;
}
Relacje dwukierunkowe są bardzo trudne do automatycznego śledzenia. Dlatego JPA przenosi tę odpowiedzialność na programistę. Oznacza to dla nas, że ustanawiając
Topic
relację podmiotową z firmą
Category
, sami musimy zapewnić spójność danych. Odbywa się to po prostu:
public void setCategory(Category category) {
category.getTopics().add(this);
this.category = category;
}
Napiszmy prosty test sprawdzający:
@Test
public void shouldPersistCategoryAndTopics() {
Category cat = new Category();
cat.setTitle("test");
Topic topic = new Topic();
topic.setTitle("topic");
topic.setCategory(cat);
em.persist(cat);
}
Mapowanie to zupełnie odrębny temat. W ramach tego przeglądu warto zrozumieć, w jaki sposób można to osiągnąć. Więcej o mapowaniu możesz przeczytać tutaj:
JPQL
JPA wprowadza ciekawe narzędzie - zapytania w języku Java Persistence Query Language. Język ten jest podobny do SQL, ale wykorzystuje model obiektowy Java, a nie tabele SQL. Spójrzmy na przykład:
@Test
public void shouldPerformQuery() {
Category cat = new Category();
cat.setTitle("query");
em.persist(cat);
Query query = em.createQuery("SELECT c from Category c WHERE c.title = 'query'");
assertNotNull(query.getSingleResult());
}
Jak widać w zapytaniu użyliśmy odwołania do encji,
Category
a nie do tabeli. A także na polu tego podmiotu
title
. JPQL zapewnia wiele przydatnych funkcji i zasługuje na własny artykuł. Więcej szczegółów znajdziecie w recenzji:
Kryteria API
Na koniec chciałbym poruszyć kwestię API Criteria. JPA wprowadza narzędzie do dynamicznego budowania zapytań. Przykład wykorzystania Criteria API:
@Test
public void shouldFindWithCriteriaAPI() {
Category cat = new Category();
em.persist(cat);
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Category> query = cb.createQuery(Category.class);
Root<Category> c = query.from(Category.class);
query.select(c);
List<Category> resultList = em.createQuery(query).getResultList();
assertEquals(1, resultList.size());
}
Ten przykład jest równoznaczny z wykonaniem żądania „
SELECT c FROM Category c
”.
Interfejs API kryteriów jest potężnym narzędziem. Więcej na ten temat możesz przeczytać tutaj:
Wniosek
Jak widzimy, JPA zapewnia ogromną liczbę funkcji i narzędzi. Każdy z nich wymaga doświadczenia i wiedzy. Nawet w ramach przeglądu WZP nie sposób było wspomnieć o wszystkim, nie mówiąc już o szczegółowym nurkowaniu. Mam jednak nadzieję, że po przeczytaniu stało się jaśniejsze, czym jest ORM i JPA, jak działa i co można z nim zrobić. Cóż, na przekąskę oferuję różne materiały:
#Wiaczesław
GO TO FULL VERSION