Home > Fragmenty szkoleń > Szkolenie: Programowanie JavaEE, JPA lab. 1

Szkolenie: Programowanie JavaEE, JPA lab. 1

Szkolenie IT Szkolenie: Programowanie JavaEE, lab. 1

Lab 1. JPA

Java Persistence API (JPA) to niezależna od dostawcy specyfikacja mapowania obiektów Java na tabele relacyjnych baz danych. Implementacje tej specyfikacji umożliwiają twórcom aplikacji abstrakcję z określonego produktu bazodanowego, z którym pracują, i pozwalają im wdrażać operacje CRUD (tworzenie, odczytywanie, aktualizowanie i usuwanie), tak aby ten sam kod działał na różnych produktach bazodanowych. Mechanizmy JPA nie tylko obsługują kod, który współdziała z bazą danych (kod JDBC), ale także odwzorowują dane na struktury obiektów używane przez aplikację.

JPA składa się z trzech podstawowych komponentów:

Encje: W aktualnych wersjach JPA są to zwykłe stare obiekty Java (POJO). Starsze wersje JPA wymagały dziedziczenia z klas dostarczanych przez JPA, ale ponieważ te podejścia jest trudniejsze do testowania z powodu twardych zależności od struktury narzucanej przez framework, nowsze wersje JPA wyeliminowały tego typu zależności.

Metadane obiektowo-relacyjne: Twórca aplikacji musi dostarczyć odwzorowanie między klasami Java i ich atrybutami na tabele i kolumny bazy danych. Można to zrobić za pomocą dedykowanych plików konfiguracyjnych lub w nowszej wersji frameworka także za pomocą adnotacji.

Java Persistence Query Language (JPQL): Ponieważ JPA ma na celu wydobycie danych z określonego produktu bazodanowego, framework zapewnia również dedykowany język zapytań, który można wykorzystać zamiast SQL. To dodatkowe tłumaczenie z JPQL na SQL pozwala implementacjom frameworka na obsługę różnych dialektów bazy danych i pozwala twórcy aplikacji na implementowanie zapytań w sposób niezależny od bazy danych

W tym ćwiczeniu omawiamy różne aspekty JPA i opracujemy prostą aplikację Java SE, która przechowuje i pobiera dane z relacyjnej bazy danych.

Użyjemy następujących bibliotek / środowisk:

  • maven> = 3.0 jako środowisko kompilacji
  • JPA 2.1 w wersji Java Enterprise Edition (JEE) 7.0
  • Hibernacja jako implementacja JPA (4.3.8.Final)
  • H2 jako relacyjna baza danych w wersji 1.3.176

Utworzenie projektu:

Pierwszym krokiem będzie utworzenie prostego projektu. Możemy utowrzyć go korzystając  korzystając z maven w linii poleceń:

mvn archetype:create -DgroupId=jpa.entity -DartifactId=jpa

Możemy również wykorzystać szkielet projektu dostarczony z ćwiczeniem lub użyć dowolnego IDE. Biblioteki, od których zależy nasza implementacja, są dodawane do sekcji zależności pliku pom.xml w następujący sposób:

<properties>

    <jee.version>7.0</jee.version>

    <h2.version>1.3.176</h2.version>

    <hibernate.version>4.3.8.Final</hibernate.version>

</properties>

 

<dependencies>

    <dependency>

        <groupId>javax</groupId>

        <artifactId>javaee-api</artifactId>

        <version>${jee.version}</version>

        <scope>provided</scope>

    </dependency>

    <dependency>

        <groupId>com.h2database</groupId>

        <artifactId>h2</artifactId>

        <version>${h2.version}</version>

    </dependency>

    <dependency>

        <groupId>org.hibernate</groupId>

        <artifactId>hibernate-entitymanager</artifactId>

        <version>${hibernate.version}</version>

    </dependency>

</dependencies>

EntityManager i Persistence Unit

Teraz zaczynamy wdrażać naszą pierwszą funkcjonalność JPA. Zacznijmy od prostej klasy, która udostępnia metodę run() wywoływaną w metodzie main() aplikacji:

public class Main {

    private static final Logger LOGGER = Logger.getLogger(„JPA”);

 

    public static void main(String[] args) {

        Main main = new Main();

        main.run();

    }

 

    public void run() {

        EntityManagerFactory factory = null;

        EntityManager entityManager = null;

        try {

            factory = Persistence.createEntityManagerFactory(„PersistenceUnit”);

            entityManager = factory.createEntityManager();

            persistPerson(entityManager);

        } catch (Exception e) {

            LOGGER.log(Level.SEVERE, e.getMessage(), e);

            e.printStackTrace();

        } finally {

            if (entityManager != null) {

                entityManager.close();

            }

            if (factory != null) {

                factory.close();

            }

        }

    }

}

Prawie wszystkie interakcje z JPA odbywają się za pośrednictwem EntityManager. Aby uzyskać instancję EntityManager, musimy utworzyć instancję EntityManagerFactory. Zwykle potrzebujemy tylko jednego EntityManagerFactory dla jednej „jednostki trwałości” na aplikację. Jednostka trwałości to zestaw klas JPA zarządzanych wraz z konfiguracją bazy danych w pliku o nazwie persistence.xml:

<?xml version=„1.0” encoding=„UTF-8” ?>

<persistence 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 version=„1.0”>

 

    <persistence-unit name=„PersistenceUnit” transaction-type=„RESOURCE_LOCAL”>

        <provider>org.hibernate.ejb.HibernatePersistence</provider>

 

        <properties>

            <property name=„connection.driver_class” value=„org.h2.Driver”/>

            <property name=„hibernate.connection.url” value=„jdbc:h2:~/jpa;AUTOCOMMIT=OFF;USER=sa;PASSWORD=123”/>

            <property name=„hibernate.dialect” value=„org.hibernate.dialect.H2Dialect”/>

            <property name=„hibernate.hbm2ddl.auto” value=„create”/>

            <property name=„hibernate.show_sql” value=„true”/>

            <property name=„hibernate.format_sql” value=„true”/>

        </properties>

    </persistence-unit>

</persistence>

Ten plik jest tworzony w folderze src / main / resource / META-INF projektu maven. Jak widać, definiujemy jedną jednostkę trwałości o nazwie PersistenceUnit, która ma typ transakcji RESOURCE_LOCAL. Typ transakcji określa sposób obsługi transakcji w aplikacji.

W naszej przykładowej aplikacji chcemy je obsługiwać samodzielnie, dlatego podajemy tutaj RESOURCE_LOCAL. Gdy używasz kontenera JEE, kontener jest odpowiedzialny za skonfigurowanie EntityManagerFactory i udostępnia tylko EntityManager. Kontener obsługuje także początek i koniec każdej transakcji. W takim przypadku podasz wartość JTA.

Ustawiając dostawcę (provider) na org.hibernate.ejb.HibernatePersistence, wybieramy implementację JPA, której chcemy użyć.

Następnym krokiem jest poinformowanie dostawcy JPA o bazie danych z której chcemy korzystać. Odbywa się to poprzez określenie sterownika JDBC, z którego powinien korzystać Hibernate. Ponieważ chcemy korzystać z bazy danych H2 (www.h2database.com), właściwość connection.driver_class jest ustawiana na wartość org.h2.Driver.

Aby umożliwić Hibernate tworzenie połączeń z bazą danych, musimy również podać URL połączenia. H2 udostępnia opcję utworzenia bazy danych w jednym pliku, którego ścieżka jest podana w adresie URL JDBC. Dlatego adres URL JDBC jdbc: h2: ~ / jpa mówi H2, aby utworzył w katalogu domowym użytkownika plik o nazwie jpa.h2.db.

H2 to szybka baza danych o otwartym kodzie źródłowym, którą można łatwo osadzić w aplikacjach Java jako pojedynczy plik jar o rozmiarze około 1,5 MB. Sprawia to, że ​​nadaje się do naszego celu, jakim jest stworzenie prostej przykładowej aplikacji do zademonstrowania użycia JPA. W projektach, które wymagają rozwiązań, które skalują się lepiej dla dużych ilości danych, prawdopodobnie wybrałbyś inny produkt bazodanowy, ale w przypadku niewielkiej ilości danych, które mają silne relacje, H2 jest dobrym wyborem.

Następną rzeczą, którą musimy powiedzieć Hibernate, jest dialekt JDBC, którego powinien użyć. Ponieważ Hibernate zapewnia dedykowaną implementację dialektu dla H2, wybieramy tę z właściwością hibernate.dialect. Dzięki temu dialektowi Hibernate może tworzyć odpowiednie instrukcje SQL dla bazy danych H2.

Na koniec udostępniamy trzy opcje, które przydają się podczas tworzenia nowej aplikacji, ale które nie byłyby używane w środowiskach produkcyjnych. Pierwszą z nich jest właściwość hibernate.hbm2ddl.auto, która mówi Hibernate, aby podczas uruchamiania tworzył wszystkie tabele od podstaw. Jeśli tabela już istnieje, zostanie usunięta. W naszej przykładowej aplikacji jest to dobra funkcja, ponieważ możemy polegać na tym, że baza danych jest pusta na początku i że wszystkie zmiany wprowadzone w schemacie od naszego ostatniego uruchomienia aplikacji znajdują odzwierciedlenie w schemacie.

Drugą opcją jest hibernate.show_sql, która mówi Hibernate, aby wydrukował każdą instrukcję SQL, która jest wysyłana do bazy danych w wierszu poleceń. Po włączeniu tej opcji możemy łatwo prześledzić wszystkie instrukcje i sprawdzić, czy wszystko działa zgodnie z oczekiwaniami. I w końcu mówimy Hibernate, aby ładnie wydrukował SQL dla lepszej czytelności, ustawiając właściwość hibernate.format_sql na true.

Po skonfigurowaniu pliku persistence.xml wracamy do naszego kodu Java przytoczonego wyżej.

Po uzyskaniu instancji EntityManagerFactory i instancji EntityManager możemy użyć ich w metodzie persistPerson, aby zapisać pierwsze dane w bazie danych. Pamiętaj, że po wykonaniu naszej pracy musimy zamknąć zarówno EntityManager, jak i EntityManagerFactory.

Transakcje

EntityManager reprezentuje jednostkę trwałości, dlatego w aplikacjach RESOURCE_LOCAL będziemy potrzebować tylko jednej instancji EntityManager. Jednostka trwałości jest pamięcią podręczną, która przetrzymuje część stanu przed zapisem w bazie danych, a także przechowuje samo połączenie z bazą danych. Aby zapisać dane w bazie danych, musimy zatem przekazać je do EntityManager, a następnie do podstawowej pamięci podręcznej. W przypadku, gdy chcesz utworzyć nowy wiersz w bazie danych, odbywa się to poprzez wywołanie metody persist() na EntityManager, jak pokazano w następującym kodzie:

private void persistPerson(EntityManager entityManager) {

    EntityTransaction transaction = entityManager.getTransaction();

    try {

        transaction.begin();

        Person person = new Person();

        person.setFirstName(„Homer”);

        person.setLastName(„Simpson”);

        entityManager.persist(person);

        transaction.commit();

    } catch (Exception e) {

        if (transaction.isActive()) {

            transaction.rollback();

        }

    }

}

Ale zanim będziemy mogli wywołać persist (), musimy otworzyć nową transakcję, wywołując transaction.begin () na nowym obiekcie transakcji pobranym z EntityManager. Jeśli pominiemy to wywołanie, Hibernate wyrzuci wyjątek IllegalStateException, który mówi nam, że zapomnieliśmy uruchomić metodę persist () w ramach transakcji:

java.lang.IllegalStateException: Transaction not active at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:70)

    at jpa.Main.persistPerson(Main.java:87)

Po wywołaniu persist () musimy zatwierdzić transakcję, tzn. Przesłać dane do bazy danych i tam zapisać. Jeśli wyjątek zostanie zgłoszony w bloku try, musimy wycofać transakcję, którą rozpoczęliśmy wcześniej. Ale ponieważ możemy wycofać tylko aktywne transakcje, musimy sprawdzić wcześniej, czy bieżąca transakcja jest już uruchomiona, ponieważ może się zdarzyć, że wyjątek zostanie zgłoszony w wywołaniu transaction.begin ().

Tabele

Klasa Person jest mapowana do tabeli bazy danych T_PERSON przez dodanie adnotacji @Entity:

@Table(name = „T_PERSON”)

public class Person {

    private Long id;

    private String firstName;

    private String lastName;

 

    @Id

    @GeneratedValue

    public Long getId() {

        return id;

    }

 

    public void setId(Long id) {

        this.id = id;

    }

 

    @Column(name = „FIRST_NAME”)

    public String getFirstName() {

        return firstName;

    }

 

    public void setFirstName(String firstName) {

        this.firstName = firstName;

    }

 

    @Column(name = „LAST_NAME”)

    public String getLastName() {

        return lastName;

    }

 

    public void setLastName(String lastName) {

        this.lastName = lastName;

    }

}

Dodatkowa adnotacja @Table jest opcjonalna, ale można jej użyć do określenia konkretnej nazwy tabeli. W naszym przykładzie chcemy, aby wszystkie tabele miały prefiks T_, dlatego mówimy Hibernate, aby utworzył tabelę T_PERSON. Tabela T_PERSON ma trzy kolumny: ID, FIRST_NAME, LAST_NAME.

Informacje te są przekazywane dostawcy JPA z adnotacjami @Column. Nawet ta adnotacja jest opcjonalna. JPA użyje wszystkich właściwości klasy Java, które mają metodę settera i gettera i utworzą dla nich kolumny, o ile nie wykluczysz ich, dodając do nich adnotacje @Transient. Z drugiej strony możesz określić więcej informacji dla każdej kolumny, używając innych atrybutów, które zawiera adnotacja @Column

@Column(name = „FIRST_NAME”, length = 100, nullable = false, unique = false)

Powyższy przykład ogranicza długość ciągu znaków do 100 znaków, stwierdza, że kolumna nie może zawierać wartości pustych i nie jest unikalna. Próba wstawienia wartości null jako w adnotowanej kolumnie tabeli spowodowałaby naruszenie ograniczenia w bazie danych i spowodowałaby wycofanie bieżącej transakcji.

Dwie adnotacje @Id i @GeneratedValue informują JPA, że ta wartość jest kluczem podstawowym dla tej tabeli i że powinna być generowana automatycznie.

W powyższym przykładowym kodzie dodaliśmy adnotacje JPA do metod getter dla każdego pola, które powinno być mapowane do kolumny bazy danych. Innym sposobem byłoby adnotowanie pola bezpośrednio zamiast metody gettera.

Te dwa sposoby są mniej więcej równorzędne, jedyna różnica odgrywa rolę, gdy chcesz zastąpić adnotacje pól w podklasach. Jak zobaczymy w dalszej części labu, możliwe jest rozszerzenie istniejącej encji w celu dziedziczenia jej pól. Kiedy umieścimy adnotacje JPA na poziomie pola, nie możemy ich przesłonić, jak możemy uczynić, zastępując odpowiednią metodę gettera.

Trzeba także zwrócić uwagę, aby zachować sposób opisywania obiektów tak samo dla jednej hierarchii encji. Można mieszać adnotacje pól i metod w ramach jednego projektu JPA, ale w ramach jednego podmiotu i wszystkich jego podklas sposób musi być spójny. Jeśli chcesz zmienić sposób adnotowania w hierarchii podklasy, możesz użyć adnotacji JPA Access, aby określić, że wybrana podklasa używa innego sposobu opisywania pól i metod:

@Entity

@Table(name = „T_GEEK”)

@Access(AccessType.PROPERTY)

public class Geek extends Person {

               

Nie dodawaj jeszcze kodu źródłowego encji Geek, najpierw bowiem możesz spróbować skompilować kod aplikacji i go uruchomić.

Po uruchomieniu aplikacji powinieneś zobaczyć w logu mniej więcej takie komunikaty:

Hibernate: drop table T_PERSON if exists

Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id))

Hibernate: insert into T_PERSON (id, FIRST_NAME, LAST_NAME) values (null, ?, ?)

Możemy sprawdzić, czy wszystko jest w porządku, używając powłoki dostarczanej z H2. Aby użyć tej powłoki, potrzebujemy tylko archiwum jar h2-1.4.199.jar:

>java cp h21.4.199.jar org.h2.tools.Shell url jdbc:h2:~/jpa

 

 

sql> select * from T_PERSON;

ID | FIRST_NAME | LAST_NAME

1  | Homer      | Simpson

(4 rows, 4 ms)

Podpowiedź: Sprawdź czy URL połączeniowy jest poprawny

Dziedziczenie

Po ukończeniu konfiguracji i pierwszym uruchomieniu aplikacji zwracamy się w stronę bardziej złożonych przypadków użycia. Załóżmy, że chcemy przechowywać obok osób informacje o geekach i ich ulubionym języku programowania. Jako że maniacy są także osobami, modelujemy to w świecie Java jako podklasową relację do Osoby:

@Entity

@Table(name = „T_GEEK”)

public class Geek extends Person {

                private String favouriteProgrammingLanguage;

 

                @Column(name = „FAV_PROG_LANG”)

                public String getFavouriteProgrammingLanguage() {

                                                return favouriteProgrammingLanguage;

                }

 

                public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {

                                this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;

                }

 

}

Dodanie adnotacji @Entity i @Table do klasy pozwala Hibernate utworzyć nową tabelę T_GEEK, jednak ku naszemu zaskoczeniu po uruchomieniu aplikacji dostajemy w logu wpis:

Hibernate: create table T_PERSON (DTYPE varchar(31) not null, id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), FAV_PROG_LANG varchar(255), primary key (id))

Widzimy, że Hibernate tworzy jedną tabelę dla obu encji i umieszcza w kolumnie o nazwie DTYPE informację, czy w rekordzie przechowywaliśmy Osobę czy Geka. Zachowajmy trochę maniaków w naszej bazie danych (dla lepszej czytelności pominęliśmy blok, który łapie każdy wyjątek i wycofuje transakcję):

private void persistGeek(EntityManager entityManager) {

    EntityTransaction transaction = entityManager.getTransaction();

    transaction.begin();

    Geek geek = new Geek();

    geek.setFirstName(„Gavin”);

    geek.setLastName(„Coffee”);

    geek.setFavouriteProgrammingLanguage(„Java”);

    entityManager.persist(geek);

    geek = new Geek();

    geek.setFirstName(„Thomas”);

    geek.setLastName(„Micro”);

    geek.setFavouriteProgrammingLanguage(„C#”);

    entityManager.persist(geek);

    geek = new Geek();

    geek.setFirstName(„Christian”);

    geek.setLastName(„Cup”);

    geek.setFavouriteProgrammingLanguage(„Java”);

    entityManager.persist(geek);

    transaction.commit();

}

Sprawdź, jakie wiersze zawiera po wykonaniu tej metody tabela bazy danych T_PERSON.

Nie w każdej sytuacji chcesz mieć jedną tabelę dla wszystkich typów, które chcesz przechowywać w swojej bazie danych. Jest tak zwłaszcza w przypadku, gdy różne typy nie mają wszystkich wspólnych kolumn. Dlatego JPA pozwala określić, jak rozłożyć różne kolumny. Dostępne są trzy opcje:

SINGLE_TABLE: Ta strategia odwzorowuje wszystkie klasy na jedną tabelę. W konsekwencji każdy wiersz ma wszystkie kolumny dla wszystkich typów, baza danych wymaga dodatkowej pamięci dla pustych kolumn. Z drugiej strony ta strategia przynosi korzyść, gdyż zapytanie nigdy nie musi korzystać z łączenia, a zatem może być znacznie szybsze.

JOINED: Ta strategia tworzy dla każdego typu osobną tabelę. Każda tabela zawiera zatem tylko stan zmapowanego obiektu. Aby załadować jedną encję, dostawca JPA musi załadować dane dla tej jednostki ze wszystkich tabel, do których jednostka jest odwzorowana. Takie podejście redukuje ilość miejsca do przechowywania, ale z drugiej strony wprowadza kwerendy łączące, które mogą znacznie zmniejszyć prędkość zapytania.

TABLE_PER_CLASS: Podobnie jak strategia JOINED, ta strategia tworzy oddzielną tabelę dla każdego typu encji. Jednak w przeciwieństwie do strategii JOINED tabele te zawierają wszystkie informacje niezbędne do załadowania tej jednostki. W związku z tym żadne kwerendy łączące nie są konieczne do załadowania encji, ale wprowadzają w sytuacjach, w których konkretna podklasa nie jest znana, dodatkowe zapytania SQL w celu jej określenia.

…….

Po więcej informacji zapraszamy na szkolenie.

Artykuły na Dev-Academy.pl

Może Cię również zainteresować: