JPA v Jakarta EE

Z webovej aplikácie, ktorá beží na aplikačnom servri, potrebujeme väčšinou ukladať údaje do databázy. Môžeme použiť klasické JDBC, ako sme videli v prvej časti predmetu - načítavať a ukladať údaje pomocou SQL príkazov. Tento spôsob je však nepraktický, keďže načítané dáta musíme "ručne" parsovať z result listov do premenných objektov a naopak vypĺňať parametre SQL príkazov. Existuje aj pokročilejší spôsob - pomocou ORM frameworku. Ten má na starosti automatické mapovanie údajov v relačnej databáze na objekty javovských tried. Keď program zmení takéto objekty v pamäti, údaje sa automaticky ukladajú do databázy. V prípade viacerých používateľov je výhodné, ak všetci používatelia budú ukladať údaje cez tú istú aplikáciu na aplikačnom servri a preto "nacachované" údaje budú stále platné a netreba ich vždy vyťahovať z databázy. Použitie takéhoto frameworku je náročné, keďže nato, aby programátor vedel, že sa jeho aplikácia bude vždy správať tak, ako potrebuje, mal by vedieť, čo presne a kedy framework s dátami robí - kedy ich udržuje v cache, kedy sa automaticky refreshujú a podobne. V našom kurze sa uspokojíme s tým, keď túto technológiu aspoň trochu spoznáme a vyskúšame si ju pri vytvorení jednoduchej databázovej aplikácie.

Vytvoríme aplikáciu na evidenciu knižiek v domácej knižnici. Každá knižka bude mať svojho autora, jeden autor môže byť autorom viacerých kníh (relácia many-to-one). Knižky sú uložené v nejakej skrinke na nejakej polici (relácia one-to-one). Celú skupinu knižiek si môže prísť požičať kamarát, pričom každá knižka môže byť požičaná aj viackrát (relácia many-to-many). Tieto relácie vytvoríme pomocou príslušných anotácií, ktoré JPA definuje.

Štruktúra našej databázy bude nasledujúca (použijeme MySQL, ale pre iný databázový systém by to bolo zrejme rovnaké):



mysql> show tables;
+----------------+
| Tables_in_ee   |
+----------------+
| author         |
| book           |
| borrowed_items |
| borrowing      |
| locality       |
+----------------+
7 rows in set (0.00 sec)

mysql> describe author;
+-------------+-------------+------+-----+---------+----------------+
| Field       | Type        | Null | Key | Default | Extra          |
+-------------+-------------+------+-----+---------+----------------+
| id          | int         | NO   | PRI | NULL    | auto_increment |
| name        | varchar(30) | YES  |     | NULL    |                |
| lastname    | varchar(30) | YES  |     | NULL    |                |
| nationality | varchar(3)  | YES  |     | NULL    |                |
| born_year   | int         | YES  |     | NULL    |                |
+-------------+-------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

mysql> describe book;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int          | NO   | PRI | NULL    | auto_increment |
| title    | varchar(100) | YES  |     | NULL    |                |
| author   | int          | YES  |     | NULL    |                |
| year     | int          | YES  |     | NULL    |                |
| genre    | varchar(30)  | YES  |     | NULL    |                |
| borrowed | tinyint(1)   | YES  |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

mysql> describe locality;
+---------+-------------+------+-----+---------+----------------+
| Field   | Type        | Null | Key | Default | Extra          |
+---------+-------------+------+-----+---------+----------------+
| id      | int         | NO   | PRI | NULL    | auto_increment |
| cabinet | varchar(30) | YES  |     | NULL    |                |
| shelf   | varchar(30) | YES  |     | NULL    |                |
| id_book | int         | YES  |     | NULL    |                |
+---------+-------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> describe borrowing;
+---------+-------------+------+-----+---------+----------------+
| Field   | Type        | Null | Key | Default | Extra          |
+---------+-------------+------+-----+---------+----------------+
| id      | int         | NO   | PRI | NULL    | auto_increment |
| to_whom | varchar(50) | YES  |     | NULL    |                |
| since   | date        | YES  |     | NULL    |                |
| until   | date        | YES  |     | NULL    |                |
| state   | tinyint(1)  | YES  |     | NULL    |                |
+---------+-------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

mysql> describe borrowed_items;
+--------------+------+------+-----+---------+-------+
| Field        | Type | Null | Key | Default | Extra |
+--------------+------+------+-----+---------+-------+
| id_borrowing | int  | YES  |     | NULL    |       |
| id_book      | int  | YES  |     | NULL    |       |
+--------------+------+------+-----+---------+-------+
2 rows in set (0.00 sec)
    

SQL príkazy na vytvorenie tabuliek a vloženie niekoľkých riadkov dát:

create table author (id int auto_increment primary key, name varchar(30), lastname varchar(30),
                     nationality varchar(3), born_year int(4));
create table book (id int auto_increment primary key, title varchar(100), author int, year int(4),
                   genre varchar(30), borrowed tinyint(1));
create table locality (id int auto_increment primary key, cabinet varchar(30), shelf varchar(30), id_book int);
create table borrowing (id int auto_increment primary key, to_whom varchar(50),
                        since date, until date, state tinyint(1));
create table borrowed_items (id_borrowing int, id_book int);

insert into author set name="Josef", lastname="Lada", nationality="CZE", born_year=1887;
insert into book set title="Do světa", author=1, year=1935, genre="fairy tale", borrowed=0;
insert into book set title="O Mikešovi", author=1, year=1934, genre="fairy tale", borrowed=0;
insert into author set name="Ondřej", lastname="Sekora", nationality="CZE", born_year=1899;
insert into book set title="Ferda Mravenec", author=2, year=1936, genre="fairy tale", borrowed=0;
insert into book set title="Trampoty brouka Pytlíka", author=2, year=1939, genre="fairy tale", borrowed=0;
    


Reláciu many-to-many reprezentujeme pomocou samostatnej tabuľky borrowed-items. Ostatné tabuľky obsahujú entity. Po vytvorení projektu je potrebné vytvoriť DataSource a ConnectionPool, čo sa robí konfiguráciou v súbore src/main/webapp/WEB-INF/glassfish-properties.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
<jdbc-connection-pool name="java:app/jdbc/eeDataSource"
                      res-type="javax.sql.DataSource"
                      datasource-classname="com.mysql.cj.jdbc.MysqlDataSource"
                      pool-resize-quantity="2"
                      max-pool-size="32"
                      steady-pool-size="8">
    <property name="URL" value="jdbc:mysql://meno.servra.com:3306/meno_databazy"/>
    <property name="User" value="pouzivatel"/>
    <property name="Password" value="heslo"/>
    <property name="autoReconnect" value="true" />
    <property name="useSSL" value="false" />
</jdbc-connection-pool>

<jdbc-resource enabled="true" jndi-name="java:app/jdbc/eeDBresource" pool-name="java:app/jdbc/eeDataSource">
    <description>DataSource jdbc/eeDataSource</description>
</jdbc-resource>
</resources>
    


Okrem toho potrebujeme nastaviť persistence unit, cez ktorý sa bude diať celá komunikácia s databázou v kóde. Ten sa obracia na príslušný DataSource, ktorý sme definovali vyššie.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">

    <persistence-unit name="ee">
        <jta-data-source>java:app/jdbc/eeDBresource</jta-data-source>
        <class>ee.books.db.Author</class>
        <class>ee.books.db.Book</class>
        <class>ee.books.db.Borrowing</class>
        <class>ee.books.db.Locality</class>

    </persistence-unit>
</persistence>
    


Vymenovanie tried je potrebné preto, aby boli automaticky nájdené všetky JPA anotácie, ktoré sa v nich nachádzajú, keďže entity sú umiestnené v samostatnom pod-package db.

Aplikácia bude pozostávať z nasledujúcich častí: Vytvoríme nasledujúce JSF stránky: Entity budú dekorované anotáciami @Entity a @Table. Okrem toho môžeme vytvoriť pomenované dopyty pomocou anotácie @NamedQuery. Do Intellij odporúčame doinštalovať plugin JPA Buddy, pomocou ktorého entitné triedy môžeme vygenerovať automaticky, napríklad trieda Author bude po vygerovaní a pridaní pár ďalších drobností (vzťahu k entite Book @OneToMany, pomenovanému dopytu listAuthors a konštruktora, ktorý inicializuje kolekciu autorových kníh na prázdny zoznam), vyzerať takto:
package ee.books.db;

import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "author")
@NamedQueries({
        @NamedQuery(
                name="listAuthors",
                query="SELECT a FROM Author a"
        ) })
public class Author {

    public Author()
    {
        books = new ArrayList();
    }
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Integer id;

    @Column(name = "name", length = 30)
    private String name;

    @Column(name = "lastname", length = 30)
    private String lastName;

    @Column(name = "nationality", length = 3)
    private String nationality;

    @Column(name = "born_year")
    private Integer bornYear;


    @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE)
    private List<Book> books;


    /* gettery a settery pre jednotlive properties */

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLastName() {
        return lastName;
    }

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

    public String getNationality() {
        return nationality;
    }

    public void setNationality(String nationality) {
        this.nationality = nationality;
    }

    public Integer getBornYear() {
        return bornYear;
    }

    public void setBornYear(Integer bornYear) {
        this.bornYear = bornYear;
    }

    public List<Book> getBooks() {
        return books;
    }
}
    

V tejto aplikácii využijeme zaujímavé vlastnosti JSF - dáta budeme renderovať do tabuliek, pričom niektoré stĺpce tabuliek budú obsahovať zoznamy (napr. zoznam knižiek príslušného autora). Okrem toho využijeme možnosť odovzdať parametre pri kliknutí na linku v príslušnom riadku tabuľky do metódy v request-scoped beane a umožníme používateľovi vyberať zo zoznamu entít prezentovaných v dropboxe. Na ukážku si pozrime súbor books.xhtml so zoznamom kníh a formulárom na pridanie novej knihy:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:f="http://xmlns.jcp.org/jsf/core">
<f:view>
    <h:head>
        <h:outputStylesheet library="css" name="library-table.css"  />
    </h:head>
    <h:body>
        <h3>Books in our little library</h3>

        (<i><h:link outcome="index" value="main menu" /></i>)<br/><br/>

        <h:dataTable value="#{library.books}" var="book"
                     styleClass="library-table"
                     headerClass="library-table-header"
                     rowClasses="library-table-odd-row,library-table-even-row">
            <h:column>
                <f:facet name="header">Author</f:facet>
                #{book.author.name} #{book.author.lastName}   
            </h:column>
            <h:column>
                <f:facet name="header">Title</f:facet>
                <b>#{book.title}</b>   
            </h:column>
            <h:column>
                <f:facet name="header">Published</f:facet>
                #{book.year}   
            </h:column>
            <h:column>
                <f:facet name="header">Genre</f:facet>
                #{book.genre}   
            </h:column>
            <h:column>
                <f:facet name="header">Locality</f:facet>
                #{book.locality == null?"---":(book.locality.cabinet.concat("-").concat(book.locality.shelf))}
                   
            </h:column>
            <h:column>
                <f:facet name="header">Borrowed</f:facet>
                #{(book.borrowed==1)?"yes":"no"}
                   
            </h:column>
            <h:column>
                <f:facet name="header">Action</f:facet>
                <h:form>
                    <h:commandLink action="#{library.deleteBook()}" value="Delete">
                        <f:param name="idBook" value="#{book.id}" />
                    </h:commandLink>
                       
                    <h:commandLink action="#{library.toSetLocality()}" value="Set locality">
                        <f:param name="idBook" value="#{book.id}" />
                    </h:commandLink>
                       
                    <h:commandLink action="#{library.borrowOrReturn()}"
                            value="#{(book.borrowed==1)?'Return':'Borrow'}">
                        <f:param name="idBook" value="#{book.id}" />
                        <f:param name="borrowed" value="#{book.borrowed}" />
                    </h:commandLink>
                </h:form>
            </h:column>
        </h:dataTable>

        <br/><br/>
        Insert new book: <br/><br/>
        <h:form id="newbook">
            <table><tr><td>
                Author:</td><td>
                <h:selectOneMenu value="#{library.bookAuthor}">
                    <f:selectItems value="#{library.authors}" var="author"
                                   itemLabel="#{author.name} #{author.lastName}" itemValue="#{author.id}" />
                </h:selectOneMenu></td></tr><tr><td>
                Title:</td>
                <td><h:inputText id="booktitle" value="#{library.bookTitle}" required="true">
                    <f:validateLength maximum="100" />
                </h:inputText> <h:message for="booktitle" style = "color:red"/></td></tr><tr><td>
                Year published:</td><td><h:inputText id="bookpublished" value="#{library.bookPublished}" required="true">
                <f:validateLength minimum="4" maximum="4"/>
            </h:inputText> <h:message for="bookpublished" style = "color:red"/></td></tr><tr><td>
                Genre:</td><td>
                <h:inputText id="bookgenre" value="#{library.genre}" required="true">
                    <f:validateLength maximum="30"/>
                </h:inputText> <h:message for="bookgenre" style="color:red"/></td></tr><tr><td colspan="2">
                <h:commandButton value="Insert" action="#{library.insertBook()}"/>
            </td></tr></table>
        </h:form>
    </h:body>
</f:view>
</html>
    


Čitateľovi odporúčame preštudovať jednotlivé súbory celého nášho cvičného projektu a prečítať si podrobnosti v oficiálnom Java EE tutoriali od Oracle: Java Platform, Enterprise Edition: The Java EE Tutorial, Persistence.

Zodpovedajúci projekt pre Intellij:
- books: books-jpa-jakarta-9.zip (a bit outdated version...)
- books: books-24.zip (a new version updated for Glassfish 7 - recommended) - you may want to install Jakarta EJB and JPA Buddy plugins in IDEA