PRA05.rst

Pracownia Programowania

ORM

Uwaga! cały kod do Zadania 7 włącznie znajduje się na gałęzi hibernateStart w repozytorium https://github.com/WitMar/PRA2021-PRA2022 . Kod końcowy w gałęzi hibernateEnd.

Jeżeli nie widzisz odpowiednich gałęzi na GitHubie wykonaj Ctr+T, a jak to nie pomoże to wybierz z menu VCS->Git->Fetch.

ORM

ORM to skrótowe oznaczenie dla "mapowanie obiektowo-relacyjne" (od angielskiego Object-Relational Mapping). Chodzi więc o zamianę (mapowanie) danych w postaci tabelarycznej (relacji w bazie danych) na obiekty, albo w drugą stronę. Pomaga przyspieszyć proces rozwoju aplikacji, dbając o takie aspekty, jak zarządzanie transakcjami, automatyczne generowanie klucza głównego, zarządzanie połączeniami z bazą danych i tłumaczeniem zapytań na język SQL. Jest nowoczesnym podejściem do zagadnienia współpracy z bazą danych, wykorzystującym filozofię programowania obiektowego.

Elementy ORM
1. API do zarządzania cyklem życia obiektów
2. Mechanizm specyfikowania metadanych opisujących odwzorowanie klas na relacje w bazach danych
3. Język lub API do wykonywania zapytań. Najpopularniejsza implementacja technologii odwzorowania obiektowo-relacyjnego dla aplikacji Java to Hibernate (dla C# to LinQ)

Architektura

arch

Elementy:

* Konfiguracja : zwykle zapisywany w plikach hibernate.properties lub hibernate.cfg.xml. Jest używany przez Session Factory do ustawień połączenia z bazą i czasem mapowania obiektów (plik xml).
* Session factory : Hibernate wykorzystuje session factory do tworzenia sesji. Session Factory używa informacji konfiguracyjnych z wyżej wymienionych plików, aby odpowiednio utworzyć instancję obiektu sesji. Możemy tworzyć wiele sesji w ramach jednego połączenia.
* Sesja : reprezentuje interakcję między aplikacją a bazą danych w dowolnym momencie. Jest ona reprezentowana przez klasę org.hibernate.Session.
* Zapytanie (qyery) : Umożliwia aplikacjom wysyłanie zapytań do bazy danych o jeden lub więcej przechowywanych w niej obiektów. Hibernate udostępnia różne techniki zapytań do bazy danych, w tym NamedQuery i Criteria API.
* Pamięć podręczna pierwszego poziomu : reprezentuje domyślną pamięć podręczną używaną przez obiekt sesji Hibernate podczas interakcji z bazą danych. Jest również nazywany pamięcią podręczną sesji i buforuje obiekty w ramach bieżącej sesji. Wszystkie żądania z obiektu Session do bazy danych muszą przechodzić przez pamięć podręczną pierwszego poziomu lub pamięć podręczną sesji. Należy zauważyć, że pamięć podręczna pierwszego poziomu jest dostępna dopóki obiekt sesji aktywny.
* Transakcja : umożliwia osiągnięcie spójności danych i wycofanie w przypadku, gdy coś pójdzie nie tak.
* Pesristent object : są to zwykłe obiekty Java (POJO), które zostają utrwalone jako jeden z wierszy w powiązanej tabeli w bazie danych przez hibernate. Można je skonfigurować w plikach konfiguracyjnych (hibernate.cfg.xml lub hibernate.properties) lub z adnotacją @Entity.
* Pamięć podręczna drugiego poziomu: służy do przechowywania obiektów w sesjach. Musi to być jawnie włączona i konieczne jest zapewnienie dostawcy (implementacji) pamięci podręcznej. Jedną z popularnych implementacji pamięci podręcznej drugiego poziomu jest EhCache.

Zależności

Żeby korzystać z Hibernate potrzebne nam będzie na pewno pakiet hibernate-core oraz sterownik bazy danych.

 <!-- Hibernate resources -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>5.4.20.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.20.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.0.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.common</groupId>
    <artifactId>hibernate-commons-annotations</artifactId>
    <version>4.0.2.Final</version>
    <classifier>tests</classifier>
</dependency>
<dependency>
    <groupId>org.hibernate.javax.persistence</groupId>
    <artifactId>hibernate-jpa-2.0-api</artifactId>
    <version>1.0.1.Final</version>
</dependency>

<!-- Java Bind -->
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.2.4</version>
</dependency>

<!-- Database -->
<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.5.0</version>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.1.1</version>
</dependency>

W naszym projekcie będziemy korzystać z bazy HSQL (bazy zapisywanej w pamięci) ale w domu proszę uruchomić przykład z PostgreSQL lub jakąś bazą dostępną online np:

https://remotemysql.com/login.php

Zadanie 1

Dodaj zależności do pliku pom.xml.

Konfiguracja Hibernate

Konfiguracje Hibernate zapisujemy w pliku hibernate.cfg.xml, hibernate.properties lub persistence.xml .

Uwaga! W zależności od tego z jakich elementów korzystamy biblioteka przy wywołaniu szuka automatycznie określonych plików w ścieżce projektu w celu odnalezienia konfiguracji.

W pliku musimy wyspecyfikować:

szczegóły dostępu do bazy danych, adres, hasło, user, lokalizacja,
język stosowanych zapytań sql,
ew. listę klas mapowanych na tabele baz danych
dialect określa jak hibernate ma komunikować się z bazą danych
show_sql - true pokaże nam zapytania SQL tworzone przez hibernate (głównie do debugowania).
hbm2dll określa sposób zachowania względem schematu bazy danych. create oznacza że tworzymy za każdym razem schemat od nowa. Gdy już raz go stworzymy polecam zmienić wartość na validate lub update - ale update nie jest polecany gdyż może powodować dziwne błędy jak "table not found".

Przykładowe ustawienia dostępu do bazy danych dla bazy PostgreSQL:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" 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_1.xsd">
    <persistence-unit name="hibernate-dynamic" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- Configuring JDBC properties -->
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost:5432/postgres"/>
            <property name="javax.persistence.jdbc.user" value="postgres"/>
            <property name="javax.persistence.jdbc.password" value="postgres"/>
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />

            <!-- Hibernate properties -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.format_sql" value="false"/>
            <property name="hibernate.show_sql" value="true"/>

        </properties>
    </persistence-unit>
</persistence>

Uwaga! żeby uruchomić program z powyższymi ustawieniami w domu powinieneś mieć zainstalowany serwer PostgreSQL lub zmienić adres serwera w ustawieniach. Informacje o instalacji PostgreSQL powinieneś łatwo znaleźć w internecie.Do podejrzenia danych w bazie postgresql polecam wykorzystać program pgAdmin.

Konfiguracja wykorzystująca uczelnianą bazę PostgreSQL (change username and password to your own):

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" 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_1.xsd">
    <persistence-unit name="hibernate-dynamic" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- Configuring JDBC properties -->
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://psql.wmi.amu.edu.pl:5432/mw?ssl=true&amp;sslfactory=org.postgresql.ssl.NonValidatingFactory"/>
            <property name="javax.persistence.jdbc.user" value="mw"/>
            <property name="javax.persistence.jdbc.password" value="riustsdardswify"/>
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />

            <!-- Hibernate properties -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.format_sql" value="false"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Na zajęciach wykorzystywać będziemy bazę HSQL uruchamianą w pamięci komputera:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" 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_1.xsd">
    <persistence-unit name="hibernate-dynamic" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- Configuring JDBC properties -->
            <property name="javax.persistence.jdbc.url" value="jdbc:hsqldb:mem:PRA"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.driver" value="org.hsqldb.jdbc.JDBCDriver" />

            <!-- Hibernate properties -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.format_sql" value="false"/>
            <property name="hibernate.show_sql" value="true"/>

        </properties>
    </persistence-unit>
</persistence>

Zadanie 2

Dodaj do katalogu resources podkatalog META-INF a w nim plik persistence.xml wklej do niego powyższą zawartość.

WAŻNE, jeśli zobaczysz błąd:

    Initial SessionFactory creation failed.javax.persistence.PersistenceException: No Persistence provider for EntityManager named hibernate-dynamic
Exception in thread "main" java.lang.NullPointerException

Oznacza to najprawdopodobniej, że Hibernate nie może znaleźć ustawień (lub całego pliku), sprawdź ustawienia resource, ścieżki katalogów, nazwe entity managera itp.

Stany encji

Oprócz samego mapowania obiektowo-relacyjnego, jednym z problemów, które Hibernate miał rozwiązać, jest problem zarządzania obiektami podczas wykonywania programu. Pojęcie "persisted context" jest rozwiązaniem Hibernata dla tego problemu. Persisted contex może być traktowany jako kontener lub pamięć podręczna pierwszego poziomu dla wszystkich obiektów, które zostały załadowane lub zapisane w bazie danych podczas sesji.

Każda instancja obiektu w aplikacji pojawia się w jednym z trzech głównych stanów w odniesieniu do kontekstu trwałości sesji:

* przejściowy (transient) - ta instancja nie jest i nigdy nie była związana z sesją; ta instancja nie ma odpowiednich wierszy w bazie danych; zwykle jest to tylko nowy obiekt, który stworzyłeś, aby zapisać do bazy danych (nie posiada własnego id);
* persistent (managed) - ta instancja jest powiązana z unikalnym identyfikatorem w obiekcie Session. Może istnieć fizycznie w bazie danych lub nie. Każda dokonana na obiekcie zmiana będzie automatycznie śledzona i propagowana do bazy danych.
* odłączone (detached) - ta instancja była kiedyś podłączona do sesji (w stanie trwałym), ale teraz nie jest; instancja wchodzi w ten stan, jeśli usuniesz ją z kontekstu, wyczyścisz lub zamkniesz sesję.

Ważne jest, aby od samego początku zrozumieć, że wszystkie metody (persist, save, update, merge) nie powodują od razu odpowiednich instrukcji SQL UPDATE lub INSERT. Rzeczywiste zapisanie danych w bazie danych następuje po dokonaniu transakcji lub opróżnieniu sesji (flush()).

ass10

EntityManager vs SessionManager

Są dwa standardy obsługi sesji w hibernate, oparty na sesji i oparty na starszym standardzie JPA z wykorzystaniem EntityManagera.

W naszych przykładach będziemy używać obiektu Session.

Jeśli podczas pracy z Entity Managerem potrzebujesz obiektów Session możesz je również uzyskać za pomocą:

Session session = entityManager.unwrap( Session.class );
SessionImplementor sessionImplementor = entityManager.unwrap( SessionImplementor.class );

SessionFactory sessionFactory = entityManager.getEntityManagerFactory().unwrap( SessionFactory.class );

Metody Session

Get method

Metoda get służy do pobierania jednego obiektu na podstawie jego klucza. Kluczem do wyszukania obiektu jest pole, które ma adnotację @Id. Przykładowe użycie:

session.get(Employee.class, 1L);

session.byId( Employee.class ).load( 1L );

// get only reference not the whole object
book.setWorker( session.load( Employee.class, 1L ) );

pobierze nam obiekt typu Employee o identyfikatorze równym 1.

Jeśli ta metoda nie znajdzie obiektu w bazie danych, zwraca wartość null.

Możliwe jest również zwrócenie obiektu typu Optional z Java 8:

Optional<Employee> optionalPerson = session.byId( Employee.class ).loadOptional( 1L );

Pobieranie listy obiektów z listy id'ków

List<Employee> employees = session
            .byMultipleIds( Employee.class )
            .multiLoad( 1L, 2L, 3L );
Save method

Możesz użyć tej metody, aby wstawić nowy obiekt do stanu zarządzanego.

Employee emp = new Employee();
session.save( person );

Przenosi nowy obiekt do stanu zarządzanego. Metoda save sprawia, że ​​obiekt jest zarządzalny, tj. wszelkie zmiany w nim wprowadzone (w ramach jednej metody z adnotacją @Transactional lub metod, które ta metoda wywołuje) zostaną utrwalone w bazie danych.

Merge method

Za pomocą tej metody można przenieść istniejący obiekt ze stanu detached do stanu zarządzanego. Przykład:

Employee person = session.byId( Employee.class ).load( 1L );
//Clear the Session so the person entity becomes detached
session.clear();
person.setName( "Mr. John Doe" );

person = (Employee) session.merge( person );

Weryfikacja stanu obiektu za pomocą Hibernate API

boolean contained = session.contains( person );
Delete method

Metoda delete służy do usuwania obiektów.

session.delete( person );

More information:

Przykład

Klasa zapisująca i czytająca element z bazy danych:

class Manager {

   public static void main(String[] args) {

    System.out.println("Start");

    EntityManager entityManager = null;

    EntityManagerFactory entityManagerFactory = null;

    try {

        // FACTORY NAME HAS TO MATCHED THE ONE FROM PERSISTED.XML !!!
        entityManagerFactory = Persistence.createEntityManagerFactory("hibernate-dynamic");

        entityManager = entityManagerFactory.createEntityManager();
        Session session = entityManager.unwrap(Session.class);

        //New transaction
        session.beginTransaction();

        // Create Employee
        Employee emp = createEmployee();

        // Save in First order Cache (not database yet)
        session.save(emp);

        Employee employee = session.get(Employee.class, emp.getId());
        if (employee == null) {
            System.out.println(emp.getId() + " not found! ");
        } else {
            System.out.println("Found " + employee);
        }

        System.out.println("Employee " + employee.getId() + " " + employee.getFirstName() + employee.getLastName());

        employee.setLastName("NowakPRE" + new Random().nextInt()); // No SQL needed
        //changeFirstGuyToNowak(session);

        //Commit transaction to database
        session.getTransaction().commit();

        System.out.println("Done");

        session.close();

    } catch (Throwable ex) {
        // Make sure you log the exception, as it might be swallowed
        System.err.println("Initial SessionFactory creation failed." + ex);
    } finally {
        entityManagerFactory.close();
    }

}
Refresh

Możesz także odświeżyć obiekt, aby zsynchronizować stan obiektu ze stanem z bazy danych

((SessionImpl) session).refresh(emp.getAddress());
emp.getAddress()

Zadanie 3

Odśwież obiekt przed updatem oraz po zakonczeniu transakcji zobacz w consoli jakie operacje zostaną wykonane.

session.refresh(employee);

Zadanie 4

Zobacz czy zmiana imienia pracownika wykona zapytania na bazie danych. Co stanie się jeżeli uruchomimy metodę z query?

Flush

Aby wymuśić synchronizacje persistence context, czyli przesłanie stanu obiektów do bazy danych można wykonać metodę flush(). Stan kontekstu jest też czasem synchronizowany automatycznie za nas:

   From time to time the Session will execute the SQL statements needed to synchronize the JDBC connection's state with the state of objects held in memory. This process, flush, occurs by default at the following points

- before some query executions

- from org.hibernate.Transaction.commit()

- from Session.flush()

Mapowanie - adnotacje

Oznaczenie odwzorowania między tabelami bazy danych a klasą realizowane jest za pomocą mechanizmu adnotacji.

@Entity adnotacja służąca do określenia, że dana klasa ma być odwzorowana na encję bazy danych.
@Table określa właściwości tabeli (np. nazwę, definicja ograniczeń itp.), jeżeli nie będzie tej adnotacji nazwą tabeli będzie nazwa klasy
@Id oznaczamy klucz główny tabeli
@Column określa właściwości kolumny tabeli (np. nazwę, czy musi być unikalne, czy może być puste), gdy nie jest zdefiniowana kolumna będzie miała taką nazwę jak pole klasy.
@GeneratedValue oznacza że wartość tego pola będzie automatycznie generowana
package hibernate.model;

import javax.persistence.*;

@Entity
@Table(name = "EMPLOYEE", uniqueConstraints = {
        @UniqueConstraint(columnNames = {"first_name","last_name"})})
public class Employee {

    @Id @GeneratedValue
    @Column(name = "id")
    private int id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "salary")
    private int salary;

    @Column(name = "PESEL", nullable = false, unique = true)
    private int pesel;

    public Employee() {}

    public int getId() {
        return id;
    }

    public void setId( int id ) {
            this.id = id;
    }

    public String getFirstName() {
         return firstName;
    }

    public void setFirstName( String first_name ) {
        this.firstName = first_name;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName( String last_name ) {
        this.lastName = last_name;
    }

    public int getSalary() {
        return salary;
    }

        public void setSalary( int salary ) {
        this.salary = salary;
    }

    public int getPesel() {
        return pesel;
    }

    public void setPesel(int pesel) {
        this.pesel = pesel;
    }
}

Zadanie 5

Dodaj pakiet hibernate i w nim pakiet model. Utwórz w środku powyższą klasę.

Inheritance class mapping

W ramach tabeli w bazie danych możemy stosować mechanizm obiektowego dziedziczenia.

@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public static class Account {

    @Id
    private Long id;

    private String owner;

    private BigDecimal balance;

    private BigDecimal interestRate;

    //Getters and setters are omitted for brevity

}

@Entity(name = "DebitAccount")
public static class DebitAccount extends Account {

    private BigDecimal overdraftFee;

    //Getters and setters are omitted for brevity

}

@Entity(name = "CreditAccount")
public static class CreditAccount extends Account {

    private BigDecimal creditLimit;

    //Getters and setters are omitted for brevity

}

More :

Collections mapping

Proste kolekcje typów prostych możemy definiować za pomocą adnotacji @ElementCollection

@Entity(name = "Employee")
public static class Employee {

    @Id
    private Long id;

    @ElementCollection
    private List<String> phones = new ArrayList<>();

    ...

}

Exercise 3

Dodaj kolekcję numerów telefonów do Pracownika, zobacz jak są mapowane w bazie danych przez Hibernate.

W pliku log4j.properties wklej wiersz

log4j.logger.org.hibernate=info

Określa poziom komunikatów logowania z hibernacji. Zobacz, co się zmieniło.

Zapytania

Queries

Native Query

Możesz wyrażać zapytania w natywnym dialekcie SQL swojej bazy danych.

List<Object[]> persons = session.createNativeQuery(
    "SELECT * FROM Employee" )
.getResultList();
HQL

Hibernate Query Language (HQL) i Java Persistence Query Language (JPQL) to języki podobne do SQL do tworzenia zapytań w Hibernate. Aby wykonać zapytanie, musimy je najpierw utworzyć za pomocą metody session.createQuery, a następnie pobrać wynik. Aby pobrać listę pracowników musimy wykonać następujący kod:

List<Employee> persons = session.createQuery(
    "from Employee" )
.list();

by utworzyć zapytanie z parametrem stosujemy zapis:

session.createQuery(
             "SELECT c FROM Employee c WHERE c.lastName LIKE :firstName", Employee.class)
             .setParameter("firstName", "%" + firstName + "%").list();

Zapytanie jest reprezentowane przez obiekt Query:

Query query = session.createQuery("SELECT k FROM Employee k");
List<Employee> employees = query.getResultList();

Jeśli nasze zapytanie zwraca tylko jeden element, możemy użyć metody query.getSingleResult().

Uwaga! Jeżeli zapytanie do bazy nie zwraca wyników (wynik jest pusty), to:

metoda getResultList() zwróci null!
metoda getSingleResult() zgłasza wyjątek (NoResultException)! (jeśli mamy więcej niż jeden wynik, otrzymujemy również wyjątek - NonUniqueResultException)

Obiekty zwrócone w zapytaniu są w stanie persisted czyli wszystkie dokonane w nich zmiany zostaną automatycznie zapisane do bazy danych (jeśli zostaną uwzględnione w transakcji - obiekty spoza transakcji nie zostaną zapisane).

Exercise 5

dodaj metodę getThemAll() pobierającą wszystkich pracowników.

Uwaga! Należy pamiętać, że zapytanie używa nazwy pól, takie jakie są nazwy podane w klasie, a nie nazwy w bazie danych!

Z wyjątkiem nazw klas imetod Java, w zapytaniach nie jest rozróżniana wielkość liter. Tak więc SeLeCT jest tym samym, co sELEct, jest tym samym co SELECT, ale org.hibernate.eg.FOO i org.hibernate.eg.Foo są różne, podobnie jak foo.barSet i foo.BARSET.

Join query example

Zapytanie typu join w HQL

List<Person> persons = session.createQuery(
    "select distinct pr " +
    "from Person pr " +
    "join pr.phones ph " +
    "where ph.type = :phoneType", Person.class )
.setParameter( "phoneType", PhoneType.MOBILE )
.getResultList();
Criteria Query

Istnieje trzeci sposób tworzenia zapytania w Hibernate, który nazywa się Criteria Query, jest on bardziej obiektowy i trudniej w nim popełnić błąd typów, czy wywołania, możesz o nim przeczytać tutaj:

Zależności

Zależności OneToOne

Pomiędzy tabelami baz danych występują zależności takie jak 1-1, 1-, *-.

Cel: Zadefiniuj zależność między obiektami - niech jeden obiekt będzie polem drugiego.

Czasem nasz model wymaga by oprócz typów prostych dana encja zawierała odnośnik do innej encji. Np. pracownika chcemy połączyć z jego adresem.

Zadanie 6

Zdefiniuj klasę address mającą pola city, street, nr, houseNumber, postcode. Tak by wszystkie oprócz houseNumber nie mogły być puste oraz kod miał co najwyżej 5 znaków (wszystkie pola powinny być typu String). Dodaj autogenerowany klucz główny. Klasa encji musi zawsze mieć zdefiniowany bezparametrowy konstruktor!

@Entity
@Table(name = "ADDRESSES")
public class Address {

    @Id
    @GeneratedValue(generator = "gen")
    @SequenceGenerator(name = "gen", sequenceName = "author_seq")
    @Column(name = "id")
    private int id;

    @Column(nullable = false)
    String street;

    @Column(nullable = false)
    String city;

    @Column(length = 5, nullable = false)
    String nr;

    @Column(length = 5)
    String housenr;

    @Column(length = 5, nullable = false)
    String postcode;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getNr() {
        return nr;
    }

    public void setNr(String nr) {
        this.nr = nr;
    }

    public String getHousenr() {
        return housenr;
    }

    public void setHousenr(String housenr) {
        this.housenr = housenr;
    }

    public String getPostcode() {
        return postcode;
    }

    public void setPostcode(String postcode) {
        this.postcode = postcode;
    }
}

Dodaj w klasie Employee pole, oraz zdefiniuj w Managerze address dla pracownika Jan.

@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name="Address_ID", referencedColumnName = "id")
Address address;

Join column definiuje nazwy naszej kolumny oraz możemy zdefiniować nazwę kolumny z którą się łączy nasz element (kolumna ta musi być unikalna). Domyślnie łączymy się z kluczem głównym tabeli.

W OneToOne możemy definiować dodatkowe opcje, optional oznacza czy pole jest wymagane, CASCADE oznacza czy zmiany w polu adress danej osoby będą się propagować do tabeli Address (dostępne opcje MERGE, PERSIST, REFRESH, REMOVE, ALL).

Przykłady:

Pamiętaj by dodać do głównego kodu tworzenie Adresu dla pracownika i przypisaniu go do niego. Uwaga! Jeżeli chcemy żeby id generowane dla adresów były inne niż te dla pracowników to między @Id a @Column możemy wpisać:

@GeneratedValue(generator = "gen")
@SequenceGenerator(name="gen", sequenceName = "author_seq")

Jeżeli potrzebowalibyśmy odwzorowania w obie strony między encjami musimy użyć adnotacji @MappedBy.

Zadanie 7

Sprawdź jak wygląda jednostronne i obustronne mapowanie z punktu widzenia klasy. Czy obiekty są automatycznie dodawane do tabel?

Zadanie 8

Zobacz co się stanie, gdy nie zdefiniujemy wartości dla jednego z obowiązkowych pól.

Co się stanie gdy dla obiektu w stanie persisted wykonamy:

emp.getAddress().setStreet(null);

Zależności OneToMany, ManyToMany

W wypadku gdy chcemy aby obie klasy miały dowiązanie do siebie (np dziecko do rodzica a rodzic do dziecka) to w jednej klasie definiujemy adnotacje @OneToMany a w drugiej odwołujemy się do tego mapowania przez @ManyToOne).

Powiązanie @OneToMany łączy encję nadrzędną z co najmniej jedną encją podrzędną. Jeśli @OneToMany nie ma lustrzanej asocjacji @ManyToOne po stronie podrzędnej, asocjacja @OneToMany jest jednokierunkowa. Jeśli po stronie podrzędnej istnieje adnotacja @ManyToOne, to połączenie @OneToMany jest dwukierunkowe, a programista aplikacji może nawigować po tej relacji z obu końców.

@Entity
public class Parent {
// ...

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private Set<Child> children = new HashSet<Child>();

// ...
}

@Entity
public class Child {
    // ...

    @ManyToOne(fetch = FetchType.LAZY)
    private Parent parent;

    // ...
}

Exercise 9

Zmień zależność address - person na ManyToOne.

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name="address_id")
Address address;

@OneToMany(mappedBy = "address", cascade = CascadeType.ALL, fetch =FetchType.EAGER)
Set<Employee> employee = new HashSet<>();

Zadanie 10

Zobacz jaki bedzie efekt (zgubiliśmy cascade persist! zapisz obiekt niezależnie!).

FetchType określa czy w przypadku pobrania jednej z tabel mamy odpytywać bazę danych o obiekt z nią połączony, jeżeli tak wybieramy FetchType.EAGER jeżeli nie i zapytanie ma być wykonane tylko gdy będziemy odwoływać się do konkretnej wartości należy wybrać FetchType.LAZY.

W klasie Employee dodaj

public static Employee copyEmployee(Employee emp, int i) {
    Employee person = new Employee();
    person.setAddress(emp.getAddress());
    person.setLastName(emp.getLastName() + i);
    person.setFirstName(emp.getFirstName());
    person.setPesel(i);
    person.setSalary(emp.getSalary());
    return person;
}

W metodzie main

for (int i = 1; i < 10; i++) {
    session.save(Employee.copyEmployee(emp));
}

\\ old session
Set<Employee> listInAddress = session.get(Address.class, 1).getEmployee();

session.getTransaction().commit();
session.clear();

session.getTransaction().begin();
\\ new session
listInAddress = session.get(Address.class, 1).getEmployee();

Analogiczne podejście stosujemy gdy chcemy dokonać mapowania w postaci wiele do wiele.

Zadanie 11

Podejrzyj w debugerze zależność między adresem a osobami. Zmień zależność address - person na ManyToMany.

Aby dodać dane w relacji należy dodać element po stronie parenta (czyli obiektu BEZ oznaczenia mappedBy).

person.setAddress(new HashSet<>(emp.getAddress()));

Sprawdź jak realizowana jest ona w bazie danych.

Transakcje

Uwaga! Operacje bazodanowe w Sesji są grupowane w transakcje, co oznacza, że ​​baza danych jest synchronizowana z pozycjami w pamięci dopiero po zakończeniu transakcji.

Ważny! Jeśli wystąpi błąd podczas przetwarzania transakcji, żadna z operacji wykonanych w kodzie, który został przesłany do bazy danych. Dlatego warto tworzyć osobne transakcje dla wybranych funkcjonalności.

Ważne jest to, że w Hibernate operacje na bazie danych nie muszą być wykonywane w takiej kolejności, w jakiej pojawiają się w kodzie w momencie transakcji. W szczególności powoduje to, że w jednej transakcji nie możemy usunąć, a następnie dodać obiektu z tym samym kluczem głównym.

Kolejność operacji w dokumentacji:

Execute all SQL (and second-level cache updates) in a special order so that foreign-key constraints cannot be violated:

Inserts, in the order they were performed
Updates
Deletion of collection elements
Insertion of collection elements
Deletes, in the order they were performed

Zadanie 12

Zakomituj transakcję następnie usuń i dodaj pracownika o tym samym Id, zobacz co się stanie.

Stronnicowanie

Cel : Zdefiniuj query, które zwracają stronnicowane dane.

Często zdarza się, że mając wiele rekordów nie chcemy za każdym razem pobierać wszystkich z bazy danych ale chcielibyśmy podzielić je na mniejsze fragmenty (strony).

Najprostsza metoda oprogramowania stronicowania to konfiguracja zapytania query

Query query = session.createQuery("From EMPLOYEE");
int pageNumber = 1;
int pageSize = 10;
query.setFirstResult((pageNumber-1) * pageSize);
query.setMaxResults(pageSize);
List <Employee> empList = query.getResultList();

Wykorzystujemy metody:

setFirstResult(int): ustawia pozycje od której chcemy pobierać dane
setMaxResults(int): ustawia liczbę rekordów, które chcemy pobrać z bazy danych

Przydatna jest też wiedza na temat liczby stron na którą dzielą się dane

Query queryTotal = entityManager.createQuery ("Select count(e.id) from EMPLOYEE e");
long countResult = (long)queryTotal.getSingleResult();
int pageSize = 10;
int pageNumber = (int) ((countResult / pageSize) + 1);

Zadanie 13

Dodaj do kodu zapytanie pobierające wszystkich pracowników z podziałem na strony.

public List<Employee> getAllEmployeeByPage(int pagenr) {
    //calculate total number
    Query queryTotal = session.createQuery
            ("Select count(e) from Employee e");
    long countResult = (long)queryTotal.getSingleResult();

    //create query
    Query query = session.createQuery("Select e FROM Employee e");
    //set pageSize
    int pageSize = 10;
    //calculate number of pages
    int pageNumber = (int) ((countResult / pageSize) + 1);

    if (pagenr > pageNumber) pagenr = pageNumber;
    query.setFirstResult((pagenr-1) * pageSize);
    query.setMaxResults(pageSize);

    return query.getResultList();
}

Stwórz wielu pracowników za pomocą

for (int i = 1; i < 100; i++) {
    entityManager.persist(Employee.copyEmployee(emp));
}

Wywołaj kod w głównej klasie.

*

Wykorzystano materiały z:

https://kobietydokodu.pl/13-2-baza-danych-z-jpa-cz-2/

http://math.uni.lodz.pl/~kowalcr/Bazy/Temat8.pdf

https://docs.jboss.org/hibernate/orm/4.2/quickstart/en-US/html/index.html

http://www.baeldung.com/hibernate-save-persist-update-merge-saveorupdate

http://www.mkyong.com/hibernate/hibernate-one-to-one-relationship-example-annotation/

https://www.javaworld.com/article/2077819/java-se/understanding-jpa-part-2-relationships-the-jpa-way.html

http://www.baeldung.com/jpa-pagination