Wykorzystano fragmenty wykładów M. Piotrowskiego i M. Wójcika JAVA PERSISTENCE API Waldemar Korłub Platformy Technologiczne KASK ETI Politechnika Gdańska
Java Persistence API 2 Specyfikacja dla bibliotek mapowania obiektowo-relacyjnego Implementowana m.in. przez: Hibernate, Eclipselink Wykorzystywana w wielu frameworkach i środowiskach Java SE, Java EE, Spring Framework, Play Framework Nie wymaga budowania skomplikowanych obiektów DAO (ang. Data Access Object) Obsługuje transakcje ACID Niezależna od dostawcy bazy danych Dostępne sterowniki dla wszystkich popularnych baz
3 Popularność baz danych
JPA: Klasy encyjne 4 Klasy mapowane na tabele w bazie danych Sposób mapowania specyfikowany adnotacjami Zwykłe POJO (Plain Old Java Object) Pola klasy encyjnej nie mogą być publiczne Każdy obiekt encyjny ma jednoznacznie identyfikujący go klucz główny Złożone klucze główne są reprezentowane przez odrębne klasy Muszą implementować metody hashcode() oraz equals()
Istotne adnotacje JPA 5 Na poziomie klasy: @Entity oznaczenie klasy jako encyjnej @Table własności tabeli w bazie danych, np.: name nazwa tabeli indexes dodatkowe indeksy (domyślny indeks dla klucza głównego) Na poziomie pól: @Id oznaczenie klucza głównego, np.: @Id Integer id; @GeneratedValue automatyczne generowanie wartości klucza głównego
Istotne adnotacje JPA 6 Na poziomie pól ciąg dalszy: @Column własności kolumny w bazie danych, np.: name nazwa kolumny nullable czy pole jest wymagane unique czy kolumna przechowuje unikalne wartości updatable czy wartość w kolumnie można modyfikować po zapisaniu nowego wiersza w tabeli @Temporal wymagane dla typów Date i Calendar Umożliwia wykorzystanie bazodanowych typów do przechowywania daty/czasu, jeśli baza je oferuje @Transient oznaczenie pól klasy, które mają zostać pominięte przy mapowaniu obiektowo-relacyjnym
@GeneratedValue 7 Atrybut strategy określa sposób generowania wartości kluczy głównych: GenerationType.IDENTITY MySQL: AUTO_INCREMENT PostgreSQL: SERIAL MS SQL: IDENTITY(1,1) GenerationType.SEQUENCE Wartości generowane przez sekwencję bazodanową np. Oracle Database: CREATE SEQUENCE invoice_seq START WITH 1;
@GeneratedValue 8 Atrybut strategy ciąg dalszy: GenerationType.TABLE Wartości generowane na podstawie pomocniczej tabeli GenerationType.AUTO PK_NAME USER_ID 117 ORDER_ID 159 PK_VALUE Implementacja JPA wybiera strategię w zależności od bazy danych
@GeneratedValue 9 Jeśli wartości kluczy są generowane automatycznie, następuje to dopiero przy zapisie encji w bazie danych Przed zapisem: id == null Uwaga na metody equals() i hashcode() Poprawne działanie przed i po zapisie encji Właściwe zachowanie kolekcji, np. Collection.contains(), HashSet Porównywanie odłączonych i zarządzanych instancji tego samego obiektu bazodanowego
Porównywanie klas encyjnych 10 z @GeneratedValue Możliwe 3 różne podejścia: equals()/hashcode() oparte na kluczu głównym encji Zapis do bazy danych zmienia zachowanie e()/hc() e()/hc() oparte na kluczu biznesowym a nie na kluczu bazodanowym Klucz biznesowy może być złożeniem pól biznesowych (nie musi być osobnym polem klasy) Zmiana wartości pól klucza biznesowego zmienia wyniki e()/hc() Osobne tożsamości bazodanowe i biznesowe e()/hc() pozostawione bez nadpisywania Problem z porównywaniem różnych instancji tego samego obiektu bazodanowego
Porównywanie klas encyjnych 11 Każdy z trzech wariantów e()/hc() dla klas encyjnych z @GeneratedValue posiada swoje wady które wynikają z obecności @GeneratedValue! Bez generowania kluczy po stronie JPA: @Id UUID id = UUID.randomUUID(); Id: 36b439bd-86fd-4464-a870-e9b66cc84dd4 Ten sam identyfikator przed i po zapisaniu w bazie danych e()/hc() mogą opierać się na wartości id Jak sprawdzić, czy encja była już zapisana w bazie danych?
@Id oraz @GeneratedValue 12 Najczęściej wykorzystywane w praktyce podejścia: Używamy generowania kluczy: @Id @GeneratedValue(strategy = IDENTITY) Integer id; i pamiętamy o ograniczeniach związanych z e()/hc() Rezygnujemy z @GeneratedValue: @Id UUID id = UUID.randomUUID(); i nie mamy problemów związanych z e()/hc()
Związki między encjami 13 Związki pomiędzy tabelami w bazie danych można odzwierciedlić jako powiązania między encjami Jednokierunkowe jedna z klas encyjnych zawiera odwołanie do drugiej Dwukierunkowe obie klasy encyjne posiadają wzajemne odwołania Pola powiązanych klas oznaczone adnotacjami: @OneToOne, @OneToMany, @ManyToOne, @ManyToMany Na powiązanych encjach można wykonywać kaskadowe operacje (atrybut cascade)
Przykłady klas encyjnych 14 @Entity public class Book { @Id UUID id = UUID.randomUUID(); @Entity public class Author { @Id UUID id = UUID.randomUUID(); } String title; @Column(unique = true) String ISBN; @Temporal(TIMESTAMP) Date releasedate; @ManyToMany( mappedby = "books") List<Author> authors; //...metody klasy... } @Column(nullable = false) String firstname; @Column(nullable = false) String lastname; @ManyToMany( cascade = {MERGE, REMOVE}) List<Book> books; //...metody klasy...
Związki dwukierunkowe 15 W relacyjnej bazie danych nie występuje koncepcja związku dwukierunkowego Związki modelowane przy użyciu kluczy obcych W obiektowym języku programowania dwukierunkowe związki są częstym rozwiązaniem Obecność atrybutu mappedby wskazuje, że pole reprezentuje drugą stronę relacji modelowanej przez inne pole Strona bez mappedby jest stroną właścicielską kontroluje związek między encjami
Związki między encjami 16 @Entity public class Employee { //...inne pola klasy... @ManyToOne Employee superior; //przełożony dwie strony tego samego związku (dwukierunkowego) @OneToMany(mappedBy = "superior") List<Employee> subordinates; //podwładni } @OneToMany List<Employee> deputies; //zastępcy związek jednokierunkowy
Schemat bazy danych 17 Na podstawie adnotacji na klasach encyjnych można wygenerować tabele w bazie danych Unikamy ręcznego przygotowywania tabel Wygenerowany schemat należy zawsze zweryfikować przed wdrożeniem produkcyjnym Jeśli tabele w bazie danych już istnieją, można wygenerować klasy encyjne z bazy danych Na podstawie kluczy obcych w tabelach generowane są powiązania @OneToOne, @OneToMany itd.
Unikanie szablonowego kodu 18 Klasy encyjne zawierają wiele szablonowych metod (ang. boilerplate code) Gettery/settery equals() i hashcode() Duża ilość takiego kodu utrudnia odnalezienie fragmentów nieszablonowych Biblioteka Lombok ogranicza ilość szablonowego kodu <dependency> <groupid>org.projectlombok</groupid> <artifactid>lombok</artifactid> <version>1.16.14</version> </dependency>
@EqualsAndHashCode(of = "id") @NoArgsConstructor @Entity public class Book { @Getter @Id UUID id = UUID.randomUUID(); } @Getter @Setter String title; @Getter @Setter Integer pagescount; @Getter @Setter @Column(unique = true) String ISBN; //... Tylko do odczytu Odczyt i zapis Tylko pole id w equals()/hc() Konstruktor domyślny 19 Lombok w klasach encyjnych
Jednostka trwałości (ang. Persistence Unit) 20 Grupuje klasy encyjne mapowane na tabele w tej samej bazie danych Aplikacja może definiować kilka jednostek trwałości (używać kilku baz danych) Określa sposób połączenia z bazą danych Nazwa hosta, port, nazwa bazy, login, hasło, sterownik JDBC do połączenia LUB Źródło danych (ang. DataSource) dostarczane przez środowisko wykonawcze (np. serwer aplikacji)
Jednostka trwałości (ang. Persistence Unit) 21 Standardowo konfigurowana w pliku persistence.xml (w katalogu META-INF projektu) Frameworki mogą udostępniać alternatywne sposoby konfiguracji, definiować ustawienia domyślne np. Spring Framework: application.properties + domyślna jednostka trwałości obejmująca wszystkie klasy encyjne projektu
Kontekst i tożsamość bazodanowa 22 Kontekst (ang. Persistence Context) Zbiór zarządzanych (ang. managed) obiektów encyjnych Zmiany w zarządzanych obiektach encyjnych są śledzone i zapisywane do bazy danych po zatwierdzeniu transakcji Tożsamość bazodanowa (ang. Persistence Identity) Tożsamość obiektu encyjnego wyrażona identyfikatorem powiązanym z istniejącym kluczem głównym w bazie danych
Stany obiektu encyjnego 23 Stan encji Związana z Persistence Context Posiada Persistence Identity new managed detached removed* *removed przeznaczony do usunięcia przy zatwierdzeniu transakcji (obiekt istnieje w bazie danych dopóki transakcja nie zakończy się sukcesem)
EntityManager 24 Udostępnia podstawowe operacje na encjach Wywoływane operacje zmieniają stan obiektu encyjnego (new/managed/detached/removed) Podstawowe metody: zapis do bazy danych (new managed): void persist(object o) detached manager: <T> T merge(t entity) usunięcie encji (managed removed): void remove(object o) aktualizacja stan obiektu encyjnego na podstawie bazy: void refresh(object o)
EntityManager 25 Podstawowe metody ciąg dalszy: wyszukiwanie na podstawie klucza głównego: <T> T find(class<t> entityclass, Object key) EntityTransaction gettransaction() zwraca obiekt transakcji Większość frameworków umożliwia automatyczne zarządzanie transakcjami EntityTransaction: begin() rozpoczyna transakcję commit() kończy transakcję, zapisuje zmiany do bazy, odłącza zarządzane obiekty encyjne rollback() wycofuje transakcję, odłącza zarządzane obiekty encyjne
Cykl życia obiektu encyjnego 26 refresh New persist Managed commit/rollback merge Detached commit remove persist rollback persist remove refresh Removed merge IllegalArgument Exception
Pozyskanie instancji klasy EntityManager 27 Java SE: Fabryka EntityManagerFactory: nazwa persistence unit EntityManagerFactory factory = Persistence.createEntityManagerFactory("main_pu"); EntityManager em = factory.createentitymanager(); Fabryka EntityManagerFactory może być wykorzystywana przez wiele wątków równocześnie Instancja klasy EntityManager może być bezpiecznie używana tylko przez jeden wątek na raz Java EE/aplikacje internetowe: Środowisko wykonawcze (np. serwer aplikacji) dostarcza instancje klasy EntityManager
28 JPQL: Java Persistence Query Language
JPQL: Java Persistence Query Language 29 Obiektowo zorientowany język zapytań o składni nawiązującej do języka SQL Opiera się na zdefiniowanym w projekcie modelu klas encyjnych Umożliwia odpytywanie bazy danych (SELECT), aktualizację (UPDATE) i usuwanie (DELETE) encji Zapytania automatycznie tłumaczone na język SQL Z uwzględnieniem automatycznych złączeń tabel w czasie trawersowania związków między encjami
Podstawowe zapytania 30 @Entity public class Product { @Id UUID id = UUID.randomUUID(); } String name; String description; Integer price; Integer amount;
Podstawowe zapytania 31 Składnia zapytania SELECT: select_statement ::= select_clause from_clause [where_clause] [groupby_clause] [having_clause] [orderby_clause] Pobranie wszystkich produktów: SELECT p FROM Product p Pobranie produktów o określonej nazwie: SELECT p FROM Product p WHERE p.name = :name Pobranie produktów o nazwie zgodnej z wyrażeniem regularnym operator LIKE: SELECT p FROM Product p WHERE p.name LIKE :name
Podstawowe zapytania 32 Wyszukanie produktów, których opis zawiera frazę VT-x w dowolnym miejscu: SELECT p FROM Product p WHERE p.description LIKE '%VT-x%' Bez uwzględniania wielkości znaków: SELECT p FROM Product p WHERE LOWER(p.name) LIKE LOWER('%Laptop%') Pobranie produktów, których stan jest niski (poniżej 5 sztuk), posortowanych według nazw: SELECT p FROM Product p WHERE p.amount < 5 ORDER BY p.name
Wywoływanie zapytań 33 Bez parametrów: String querystring = "SELECT p FROM Product p"; Query query = em.createquery(querystring); List<Product> products = query.getresultlist(); Z parametrami: String querystring = "SELECT p FROM Product p WHERE p.name LIKE :name"; Query query = em.createquery(querystring); query.setparameter("name", "Laptop"); List<Product> products = query.getresultlist();
Wywoływanie zapytań 34 OFFSET oraz LIMIT: query.setfirstresult(21).setmaxresults(10); Pojedynczy rezultat: String querystring = "SELECT b FROM Book b WHERE b.isbn LIKE :isbn"; TypedQuery<Book> query = em.createquery(querystring, Book.class); query.setparameter("isbn", "999-999-9"); Book book = query.getsingleresult(); Dla zapytań UPDATE oraz DELETE: int changedrows = query.executeupdate();
Zapytania nazwane 35 Zapytania używane w wielu miejscach aplikacji można zdefiniować przy użyciu adnotacji @NamedQuery: @NamedQueries({ @NamedQuery(name = "Product.findAll", query = "SELECT p FROM Product p"), @NamedQuery(name = "Product.findByName", query = "SELECT p FROM Product p WHERE p.name = :name") }) @Entity public class Product { //...pola klasy... } Wywołanie zapytania: Query query = em.createnamedquery("product.findall");
Bazy danych: standalone vs embedded 36 W środowisku produkcyjnym typowo wykorzystuje się samodzielne bazy danych (standalone DBMS) np. Oracle Database, PostgreSQL, MySQL, MS SQL W czasie wytwarzania aplikacji/testów często wygodniejsze jest użycie bazy danych osadzonej w aplikacji (embedded) np.: Java DB, H2, HSQLDB (HyperSQL)
Baza danych w trybie embedded 37 Nie wymaga osobnej instalacji, konfiguracji, uruchomienia Baza danych startuje automatycznie razem z aplikacją Dane przechowywane na dysku lub w pamięci RAM Pamięć RAM: Szybszy dostęp Zawartość bazy czyszczona w chwili zamknięcia aplikacji Przydatne do testów integracyjnych
38 Pytania?