Prikaži sadržaj

Programiranje baza podataka

10. Uvod u razvojno okruženje Hibernate

Ova stranica je pod konstrukcijom!
Ako pronađete grešku ili propust, molimo Vas da nam skrenete pažnju otvaranjem primedbe na zvaničnom GitHub repozitorijumu.

Prilikom izrade aplikacija, Java programeri se oslanjaju na objektno-orijentisane koncepte koji, kako im i samo ime kaže, počivaju na upotrebi objekata (i naravno, ostalih koncepata koji se dalje zasnivaju na njima, poput klase, učauravanja, interfejsi i drugi). Ovi objekti modeliraju poslovnu logiku iz realnog sveta, definisanu od strane naručilaca proizvoda. Podaci u memoriji, sa kojima aplikacija upravlja, nisu korisni ukoliko se ne mogu negde trajno skladištiti. Dodatno, veliki broj podataka počiva upravo iz nekih skladišta podataka, te je potrebno da aplikacije pristupaju takvim izvorima informacija. Tradicionalno, ali i dalje u ogromnoj meri, ovi podaci se zapisuju u relacionim bazama podataka zbog različitih prednosti koje su nam poznate iz dobro razrađene teorije relacionog računa na kojima ove baze podataka počivaju.

Ovim se otvara naredno pitanje — ako Java aplikacije rade sa podacima u memoriji koji su zapisani kao objekti, a naši podaci od interesa se skladište u relacionim bazama podataka koji su zapisani u tabelama, da li je moguće dizajnirati sistem koji će automatski izvršiti prevođenje podataka iz jednog oblika u drugi i obrnuto? Ovaj problem se naziva problem objektno-relacionog preslikavanja (engl. Object-Relation Mapping problem) i odgovor na pitanje je da može. U ovom poglavlju biće predstavljeno okruženje za razvoj Hibernate, koje omogućava Java programerima da u svojim aplikacijama implementiraju poslovnu logiku, dok se operacije niskog nivoa, kao što su čitanje, skladištenje, brisanje i menjanje podataka, izvršavaju u pozadini, čime se veliki deo posla olakšava.

In this chapter, we’ll think of the problems of data storage and sharing in the context of an object-oriented application that uses a domain model. Instead of directly working with the rows and columns of a java.sql.ResultSet, the business logic of an application interacts with the application-specific object-oriented domain model. If the SQL database schema of an online auction system has ITEM and BID tables, for example, the Java application defines Item and Bid classes. Instead of reading and writing the value of a particular row and column with the ResultSet API, the application loads and stores instances of Item and Bid classes.

At runtime, the application therefore operates with instances of these classes. Each instance of a Bid has a reference to an auction Item, and each Item may have a collection of references to Bid instances. The business logic isn’t executed in the database (as an SQL stored procedure); it’s implemented in Java and executed in the application tier. This allows business logic to use sophisticated object-oriented concepts such as inheritance and polymorphism.

Upotreba Hibernate okruženja za razvoj će biti prikazana kroz jedan veći primer, koji će biti izrađen deo-po-deo. Ovo znači da, pored toga što će se u projektu povećavati broj klasa sa (gotovo) svakim zahtevom, i same klase će biti proširivane kako bi zadovoljile sve zahteve.

Cilj ovog poglavlja jeste razvoj aplikacije koja ispunjava naredne zahteve (svi zahtevi se implementiraju nad poznatom bazom podataka STUD2020):

  1. Unos podataka o novom studijskom programu u tabelu STUDIJSKIPROGRAM sa narednim podacima:
Kolona Vrednost
Identifikator 102
Oznaka MATF_2020
Naziv Novi MATF studijski program u 2020. godini
ESPB 240
Nivo 1
Zvanje Diplomirani informaticar
Opis Novi studijski program na Matematickom fakultetu
  1. Čitanje podataka o prethodno unetom studijskom programu iz tabele STUDIJSKIPROGRAM.
  2. Ažuriranje podataka o prethodno unetom studijskom programu iz tabele STUDIJSKIPROGRAM.
  3. Brisanje podataka o prethodno unetom studijskom programu iz tabele STUDIJSKIPROGRAM.
  4. Unos podataka o novom ispitnom roku (jun 2020. godine) u tabelu ISPITNIROK.
  5. Brisanje podataka o prethodno unetom ispitnom roku iz tabele ISPITNIROK.
  6. Ispisivanje podataka o svim ispitnim rokovima.
  7. Ispisivanje podataka o ispitnom roku čija se oznaka roka i godina roka unose sa standarnog ulaza.
  8. Ispisivanje naziva svih studijskih programa. Nakon svakog naziva studijskog programa, ispisuju se indeks, ime i prezime svih studenata na tom studijskom programu. Dopuniti ispis o studentu tako da se za svakog studenta ispisuje i prosek.
  9. Za zadati indeks studenta ispisati nazive svih položenih predmeta i dobijene ocene. Implementirati dva metoda — jedan koji ne koristi Hibernate JPA Criteria API i drugi koji ga koristi.

Takođe, omogućiti da obrada svakog zahteva predstavlja zasebnu transakciju.

10.1 Podešavanje Hibernate projekta

Hibernate biblioteka se veoma jednostavno instalira. Sa veze https://hibernate.org/orm/ potrebno je preuzeti odgovarajuću verziju biblioteke. Mi ćemo raditi sa poslednjom stabilnom verzijom, a to je verzija 5.4 koja se može preuzeti klikom na dugme Download Zip archive sa ove veze. Preuzetu arhivu je potrebno otpakovati na neku lokaciju. Na virtualnoj mašini “BazePodataka2020”, ta lokacija je /opt/hibernate-5.4.22/. Dodatno, Java projekat koji budemo kreirali mora da sadrži informaciju i o implementaciji JDBC drajvera za DB2. Podešavanje Java projekta sa podrškom za razvoj Hibernate aplikacija se vrši narednim koracima:

  1. Otvoriti IBM Data Studio.
  2. Iz glavnog menija odabrati Window -> Perspective -> Open Perspective -> Other -> Java -> OK
  3. Iz glavnog menija odabrati File -> New -> Java Project

    1. U polje Project name uneti naziv Java projekta: poglavlje_10.
    2. Iz padajuće liste u tački Use an execution environment JRE odabrati JavaSE-1.8.
    3. Odabrati Finish.
  4. Desni klik na projekat koji je napravljen u Package Explorer -> Properties

    1. Odabrati iz leve liste tab Java Build Path.
    2. Odabrati iz gornje liste karticu Libraries.
    3. Podesiti podršku za JDBC:

      1. Odabrati Add External JARS.
      2. U prozoru koji se otvori odabrati: Other Locations -> Computer -> opt -> ibm -> db2 -> V11.5 -> java.
      3. Sa ove lokacije označiti datoteke db2jcc4.jar i db2jcc_licence_cu.jar, pa odabrati OK.
    4. Podesiti podršku za Hibernate:

      1. Odabrati Add Library.
      2. U prozoru koji se otvori odabrati: User Library -> Next -> User Libraries -> New -> Uneti naziv Hibernate -> OK.
      3. Sada je potrebno dodati sve pakete koji se nalaze u poddirektorijumima envers, jpa-metamodel-generator, osgi i required na putanji /opt/hibernate-5.4.10/lib/. Postupak će biti opisan za direktorijum envers, a Vi treba da ga ponovite korak-po-korak za sve ostale navedene direktorijume.

        1. Kliknuti na napravljenu biblioteku Hibernate (da bi Vam bilo omogućeno dugme Add External JARS).
        2. Odabrati Add External JARS.
        3. U prozoru koji se otvori odabrati: Other Locations -> Computer -> opt -> hibernate-5.4.10 -> lib -> envers.
        4. Sa ove lokacije označiti sve datoteke, pa odabrati OK.
      4. Kada su svi potrebni paketi uključeni, odabrati OK -> Finish -> OK.
    5. Ovim ste uspešno dodali podršku za JDBC i Hibernate aplikacije u Vašem projektu.

10.2 Podešavanje konekcije na bazu podataka

To create a connection to the database, Hibernate must know the details of our database, tables, classes, and other mechanics. This information is ideally provided as an XML file (usually named hibernate.cfg.xml) or as a simple text file with name/value pairs (usually named hibernate.properties).

For this exercise, we use XML style. We name this file hibernate.cfg.xml so the framework can load this file automatically.

The following snippet describes such a configuration file. Because we are using IBM DB2 as the database, the connection details for the IBM DB2 database are declared in this hibernate.cfg.xml file.

Da bismo napravili ovu datoteku, potrebno je da desnim klikom na naziv projekta otvorimo padajući meni iz kojeg biramo New -> Other i onda pronađemo XML File iz filtera XML. Klikom na Next, potrebno je da unesemo naziv datoteke — u ovom slučaju, to je hibernate.cfg.xml, odaberemo da se datoteka smesti u direktorijum src i odaberemo Finish.

Ova datoteka će se automatski otvoriti u XML pregledaču, ali je nama neophodno da bude otvorena kao tekstualna datoteka. Ovo se može uraditi desnim klikom na naziv datoteke hibernate.cfg.xml u Package Explorer pogledu, a zatim biranjem Open with -> Text Editor. U ovu datoteku je potrebno smestiti sledeće:

Datoteka: vezbe/primeri/poglavlje_10/src/hibernate.cfg.xml:

<?xml version="1.0" encoding="UTF-8"?>
<hibernate-configuration>
       <session-factory>
           <property name="connection.url">
               jdbc:db2://localhost:50000/STUD2020
           </property>
        <property name="connection.driver_class">
            com.ibm.db2.jcc.DB2Driver
        </property>
           <property name="hibernate.connection.username">
               student
           </property>
           <property name="hibernate.connection.password">
               abcdef
           </property>
           <property name="hibernate.dialect">
               org.hibernate.dialect.DB2Dialect
           </property>
    </session-factory>
</hibernate-configuration>

This file has enough information to get a live connection to an IBM DB2 database.

The preceding properties can also be expressed as name/value pairs. For example, here’s the same information represented as name/value pairs in a text file titled hibernate.properties:

hibernate.connection.driver_class = com.ibm.db2.jcc.DB2Driver
hibernate.dialect = org.hibernate.dialect.DB2Dialect
hibernate.connection.url = jdbc:db2://localhost:50000/STUD2020
hibernate.connection.username = student
hibernate.connection.password = abcdef

Property connection.url indicates the URL to which we should be connected; driver_class represents the relevant Driver class to make a connection, and the dialect indicates which database dialect we are using (IBM DB2, in this case). Similarly, connection.username and connection.password specifies the username and password for connecting to a database.

If you are following the hibernate.properties file approach, note that all the properties are prefixed with “hibernate” and follow a pattern — hibernate.* = value.

10.3 Fabrika sesija

Pre nego što pređemo na implementiranje klasa i definisanje preslikavanja između tabela u bazi podataka i klasa u programskom jeziku Java, pogledajmo kako se može kreirati jednostavan klijent koji će koristiti informacije iz hibernate.cfg.xml datoteke za kreiranje konekcije ka bazi podataka.

Hibernate’s native bootstrap API is split into several stages, each giving you access to certain configuration aspects. Building a SessionFactory looks like this:

Datoteka: vezbe/primeri/poglavlje_10/src/zadatak_10_1/HibernateUtil.java:

package zadatak_10_1;

import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

class HibernateUtil {
    private static SessionFactory sessionFactory = null;

    static {
        try {
            StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
            sessionFactory = new MetadataSources(registry).addAnnotatedClass(StudijskiProgram.class).buildMetadata()
                    .buildSessionFactory();
        } catch (Throwable e) {
            System.err.println("Session factory error");
            e.printStackTrace();

            System.exit(1);
        }
    }

    static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

First, create a StandardServiceRegistry:

StandardServiceRegistry registry = 
        new StandardServiceRegistryBuilder()
            .configure()
            .build();

StandardServiceRegistryBuilder helps you create the immutable service registry with chained method calls. Configure the services registry by calling the method configure on it. Finally, call the method build to create said service registry.

With the StandardServiceRegistry built and immutable, you can move on to the next stage: telling Hibernate which persistent classes are part of your mapping metadata. Configure the metadata sources as follows:

MetadataSources metadataSources = new MetadataSources(serviceRegistry);
metadataSources.addAnnotatedClass(StudijskiProgram.class);
MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder();

The MetadataSources API has many methods for adding mapping sources; check the Javadoc for more information. The next stage of the boot procedure is building all the metadata needed by Hibernate, with the MetadataBuilder you obtained from the metadata sources.

You can then query the metadata to interact with Hibernate’s completed configuration programmatically, or continue and build the final SessionFactory:

Metadata metadata = metadataBuilder.build();
SessionFactory sessionFactory = metadata.buildSessionFactory();

This builder helps you create the immutable service registry with chained method calls.

Opisani kod stavljamo u statički blok klase HibernateUtil kako bi se on izvršio prvi put kada se ova klasa bude koristila. U našem slučaju, to podrazumeva prvi put kada instanca fabrike sesija bude bila iskorišćena u main funkciji naše aplikacije.

Note that we don’t have to explicitly mention the mapping or configuration or properties files, because the Hibernate runtime looks for default filenames, such as hibernate.cfg.xml or hibernate.properties, in the classpath and loads them. If we have a nondefault name, make sure you pass that as an argument — like configure("my-hib-cfg.xml"), for example.

10.4 Objektno-relaciono preslikavanje jedne klase

Zadatak 10.1: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate redom:

  1. unosi podatak o novom studijskom programu u tabeli STUDIJSKIPROGRAM sa podacima iz naredne tabele,
  2. čita podatke o studijskom programu sa identifikatorom 102 iz tabele STUDIJSKIPROGRAM,
  3. ažurira podatke o studijskom programu sa identifikatorom 102 iz tabele STUDIJSKIPROGRAM,
  4. čita podatke o studijskom programu sa identifikatorom 102 iz tabele STUDIJSKIPROGRAM,
  5. briše podatke o studijskom programu sa identifikatorom 102 iz tabele STUDIJSKIPROGRAM,
  6. čita podatke o studijskom programu sa identifikatorom 102 iz tabele STUDIJSKIPROGRAM.
Kolona Vrednost
Identifikator 102
Oznaka MATF_2020
Naziv Novi MATF studijski program u 2020. godini
ESPB 240
Nivo 1
Zvanje Diplomirani informaticar
Opis Novi studijski program na Matematickom fakultetu

Rešenje: Da bismo rešili ovaj zadatak, potrebno je da kreiramo klasu StudijskiProgram i da joj dodamo svojstva koja odgovaraju kolonama u tabeli STUDIJSKIPROGRAM:

package zadatak_10_1;

class StudijskiProgram {
    private int id;
    private String Oznaka;
    private String Naziv;
    private int ESPB;
    private Integer Nivo;
    private String Zvanje;
    private String Opis;
}

Primetimo da je klasa StudijskiProgram definisana na nivou vidljivosti paketa. Za ovo smo se opredelili zbog toga što će implementacija svakog novog zahteva zahtevati novi paket u projektu poglavlje_10. Trenutne klase koje smo napisali - HibernateUtil i StudijskiProgram - čuvaju se u paketu zadatak_10_1 i važe samo za njega, tako da nema potrebe da budu javne vidljivosti.

Sada je potrebno da definišemo objektno-relaciono preslikavanje između klase StudijskiProgram i tabele STUDIJSKIPROGRAM u bazi podataka.

U Hibernate radnom okviru je moguće korišćenje dva pristupa za definisanje preslikavanja:

  1. Korišćenjem XML datoteka — Za svaku tabelu se definiše XML datoteka koja je struktuirana na odgovarajući način i koristi XML elemente i njihove atribute za definisanje preslikavanja.

  2. Korišćenjem Java anotacija — Koriste se Java konstrukti oblika @NazivAnotacije i njihova svojstva za definisanje preslikavanja.

Mi ćemo u daljem tekstu koristiti pristup zasnovan na Java anotacijama.

Hibernate uses the Java Persistence API (JPA) annotations. JPA is the standard specification dictating the persistence of Java objects. So the preceding annotations are imported from the javax.persistence package.

Each persistent object is tagged (at a class level) with an @Entity annotation. The @Table annotation declares our database table where these entities will be stored. Ideally, we should not have to provide the @Table annotation if the name of the class and the table name are the same (in our example, the class is StudijskiProgram, whereas the table name is STUDIJSKIPROGRAM, which is fine):

@Entity
@Table(name = "DA.STUDIJSKIPROGRAM")
class StudijskiProgram {
    ...

Sada je potrebno da definišemo preslikavanje kolona. All persistent entities must have their identifiers defined. The @Id annotation indicates that the variable is the unique identifier of the object instance (in other words, a primary key). When we annotate the id variable with the @Id annotation, as in the preceding example, Hibernate maps a field called id from our table StudijskiProgram to the id variable on the StudijskiProgram class:

@Entity
@Table(name = "DA.STUDIJSKIPROGRAM")
class StudijskiProgram {
    @Id
    private int id;
    ...

If your variable doesn’t match the column name, you must specify the column name using the @Column annotation. Dodatno, ukoliko kolona ne može imati NULL vrednosti u bazi podataka, potrebno je postaviti još i svojstvo nullable na vrednost false u anotaciji @Column:

@Entity
@Table(name = "DA.STUDIJSKIPROGRAM")
class StudijskiProgram {
    @Id
    private int id;

    @Column(name = "oznaka", nullable = false)
    private String Oznaka;

    @Column(name = "naziv", nullable = false)
    private String Naziv;

    @Column(name = "obimespb", nullable = false)
    private Integer ESPB;

    @Column(name = "idnivoa", nullable = false)
    private Integer Nivo;

    @Column(name = "zvanje", nullable = false)
    private String Zvanje;

    @Column(name = "opis", nullable = true)
    private String Opis;
    ...

Takođe, s obzirom da su ova svojstva deklarisana modifikatorom private, potrebno je implementirati metode za postavljanje i dohvatanje njihovih vrednosti. U ovu svrhu, može nam pomoći alat IBM Data Studio:

Ovim će nam biti generisane odgovarajuće metode. Možete istražiti sve mogućnosti ove opcije, kao što su generisanje samo postavljačkih ili dohvatačkih metoda, generisanje metoda samo za neka svojstva i drugo.

Cela implementacija klase StudijskiProgram data je u nastavku.

Datoteka: vezbe/primeri/poglavlje_10/src/zadatak_10_1/StudijskiProgram.java:

package zadatak_10_1;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

// Svaki trajni objekat mora biti dekorisan anotacijom @Entity. 
// Anotacija @Table je neophodna iako se ime klase i ime tabele ne razlikuju jer moramo specifikovati koja se shema koristi.
@Entity
@Table(name = "DA.STUDIJSKIPROGRAM")
class StudijskiProgram {
    // Anotacija @Id znaci da svojstvo koje ono dekorise
    // predstavlja jedinstveni identifikator instance objekta (tj. primarni
    // kljuc).
    // Ime polja i ime kolone u bazi je isto u ovom slucaju.
    @Id
    private int id;

    // Za svojstvo Oznaka, ime kolone u bazi podataka je "oznaka",
    // pa je dodatno ime kolone naglaseno kroz svojstvo name anotacije @Column.
    // Dodatno, u bazi oznaka ova kolona ne moze biti null,
    // pa dodajemo svojstvo nullable = false anotaciji @Column.
    @Column(name = "OZNAKA", nullable = false)
    private String Oznaka;

    @Column(name = "NAZIV", nullable = false)
    private String Naziv;

    @Column(name = "OBIMESPB", nullable = false)
    private Integer Espb;

    @Column(name = "IDNIVOA", nullable = false)
    private Integer Nivo;

    @Column(name = "ZVANJE", nullable = false)
    private String Zvanje;

    @Column(name = "OPIS", nullable = true)
    private String Opis;


    public StudijskiProgram() {
    }
    
    public StudijskiProgram(int id, String oznaka, String naziv, Integer espb, Integer nivo,
            String zvanje, String opis) {
        this.id = id;
        Oznaka = oznaka;
        Naziv = naziv;
        Espb = espb;
        Nivo = nivo;
        Zvanje = zvanje;
        Opis = opis;
    }


    // Automatski generisani dohvatacki i postavljacki metodi:
    // 1. Desni klik na prazan deo koda
    // 2. Source > Generate Getters and Setters...
    // 3. Select All
    // 4. OK

    public int getId() {
        return id;
    }

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

    public String getOznaka() {
        return Oznaka;
    }

    public void setOznaka(String oznaka) {
        Oznaka = oznaka;
    }

    public String getNaziv() {
        return Naziv;
    }

    public void setNaziv(String naziv) {
        Naziv = naziv;
    }

    public Integer getEspb() {
        return Espb;
    }

    public void setEspb(Integer espb) {
        Espb = espb;
    }

    public Integer getNivo() {
        return Nivo;
    }

    public void setNivo(Integer nivo) {
        Nivo = nivo;
    }

    public String getZvanje() {
        return Zvanje;
    }

    public void setZvanje(String zvanje) {
        Zvanje = zvanje;
    }

    public String getOpis() {
        return Opis;
    }

    public void setOpis(String opis) {
        Opis = opis;
    }

    @Override
    public String toString() {
        return "Studijski program [id=" + id + ", Oznaka=" + Oznaka + ", Naziv=" + Naziv
                + ", Espb=" + Espb + ", Nivo=" + Nivo + ", Zvanje=" + Zvanje + ", Opis=" + Opis + "]";
    }

    
}

10.5 Unos jednog sloga

Napišimo sada i klasu Main koja će sadržati statički metod main u kojem ćemo testirati rad naše aplikacije. Trenutno, metod main će uraditi dve stvari: prva je pozivanje statičke funkcije za unos novog studijskog programa, a druga je zatvaranje fabrike sesija:

package zadatak_10_1;

import org.hibernate.Session;
import org.hibernate.Transaction;

class Main {
    
    public static void main(String[] args) {
        System.out.println("Pocetak rada...\n");
        
        insertStudijskiProgram();
        
        System.out.println("Zavrsetak rada.\n");
        
        // Zatvaranje fabrike sesija
        HibernateUtil.getSessionFactory().close();
    }
    
    ...

Sada prelazimo na implementaciju metoda insertStudijskiProgram koji treba da unese novi red u tabelu STUDIJSKIPROGRAM.

Sve akcije nad bazom podataka se izvršavaju u okviru tzv. sesija (engl. session). Da bismo mogli da radimo sa bazom podataka, potrebno je da otvorimo novu sesiju, što nam je omogućeno metodom openSession iz klase SessionFactory.

Sledeći korak jeste kreiranje objekta klase StudijskiProgram i postavljanje odgovarajućih vrednosti. Na ovaj način smo podatke smestili u memoriju računara. Ono što je potrebno uraditi da bi se oni trajno skladištili u bazu podataka jeste pozvati metod save nad objektom sesije (tj. instancom klase Session koju smo dobili pozivom openSession). Na kraju, potrebno je da zatvorimo sesiju. Kod bi mogao da izgleda kao u nastavku:

private static void insertStudijskiProgram() {
    Session session = HibernateUtil.getSessionFactory().openSession();
    StudijskiProgram studijskiProgram = new StudijskiProgram();
    
    studijskiProgram.setId(102);
    studijskiProgram.setOznaka("MATF_2020");
    studijskiProgram.setNaziv("Novi MATF studijski program u 2020. godini");
    studijskiProgram.setESPB(240);
    studijskiProgram.setNivo(1);
    studijskiProgram.setZvanje("Diplomirani informaticar");
    studijskiProgram.setOpis("Novi studijski program na Matematickom fakultetu");
    
    session.save(studijskiProgram);
    System.out.println("Studijski program je sacuvan");
    
    session.close();
}

Iz ovog dela koda vidimo koliko je jednostavno trajno skladištiti podatke u bazu podataka, koji su se nalazili u memoriji računara. Ipak, postoji još jedna stvar kojom treba dopuniti prethodni kod.

10.6 Implementacija transakcija

Napomenuli smo na početku poglavlja da aplikacija koju budemo kreirali mora da radi tako da svaki zahtev predstavlja jednu transakciju. Ono što je dobra vest jeste da je definisanje transakcija u Hibernate radnom okviru veoma jednostavna procedura. Pogledajmo naredni kod:

Session session = HibernateUtil.getSessionFactory().openSession();
StudijskiProgram studijskiProgram = new StudijskiProgram();

// Postavljanje odgovarajucih vrednosti za studijskiProgram ide ovde ...

Transaction TR = null;
try {
    TR = session.beginTransaction();
    
    session.save(studijskiProgram);
    
    TR.commit();
    System.out.println("Studijski program je sacuvan");
} catch (Exception e) {
    System.out.println("Cuvanje studijskog programa nije uspelo! Transakcija se ponistava!");
    
    if (TR != null) {
        TR.rollback();
    }
} finally {
    session.close();
}

Transakcije se u Hibernate radnom okviru predstavljaju klasom Transaction. We initiate a transaction by invoking the session.beginTrasaction() method, which creates a new Transaction object and returns the reference to us. It gets associated with the session and is open until that transaction is committed or rolled back.

We perform the required work in a transaction, then issue a commit on this transaction. At this stage, the entities are persisted to the database. While persisting, if for whatever reason there are any errors, the Hibernate runtime will catch and throw a HibernateException (which is an unchecked RuntimeException). We then have to catch the exception and roll back the transaction.

10.7 Dohvatanje jednog sloga

S obzirom da smo u prethodnom primeru postavili sva preslikavanja i pripremne korake, ispostaviće se da se dohvatanje jednog sloga iz baze podataka sastoji u jednostavnom pozivanju odgovarajućih metoda. Nakon što dohvatimo jedan slog, možemo ga menjati ili obrisati, o čemu će biti reči u narednoj sekciji.

Ukoliko je potrebno da pronađemo (dohvatimo) red iz tabele sa odgovarajućim primarnim ključem, na raspolaganju nam je metod load() definisan nad objektima klase Session. Postoji više preopterećenja ovog metoda, a ono koje ćemo mi koristiti jeste potpisa:

public void load(Object object, Serializable id)

Prvi argument ovog metoda bi trebalo da bude prazna instanca klase reda koji želimo da učitamo, a drugi argument je instanca primarnog ključa. Nakon izvršavanja ovog metoda, objekat object će biti popunjen vrednostima iz kolona reda čiji je primarni ključ zadat sa id.

Ukoliko nismo sigurni da slog sa zadatim primarnim ključem postoji u tabeli, nije dobro koristiti metod load(). U tom slučaju, bolje je koristiti metod get(). Razlika je u tome što, ukoliko primarni ključ nije pronađen u bazi podataka, metod load() će izbaciti izuzetak, dok će metod get() vratiti null referencu, što je lakše za korišćenje. Ipak, njegova upotreba je nešto drugačija jer su nam na raspolaganju naredna dva preopterećenja:

public <T> T get(Class<T> clazz, Serializable id)
public Object get(String entityName, Serializable id)

Prvi od njih je šablonskog tipa i koristi klasu da odredi iz koje tabele dohvata slog, a drugi koristi naziv entiteta, koji je moguće dobiti poziv metoda getEntityName() nad objektom klase Session:

public String getEntityName(Object object)

Slede primeri kodova koji koriste metod get():

// Get an id from some other Java class,
// for instance, through a web application
Supplier supplier = session.get(Supplier.class, id);
if (supplier == null) {
    System.out.println("Supplier not found for id " + id);
    return;
}

// ili...

String entityName = session.getEntityName(supplier);
Supplier secondarySupplier =
        (Supplier) session.load(entityName, id);

10.8 Brisanje jednog sloga

Brisanje slogova iz baze podataka se jednostavno vrši pozivanjem metoda delete() nad objektom klase Session:

public void delete(Object object)

Argument ovog metoda je trajni objekat. Naravno, postoje i složeniji metodi brisanja podataka.

Cela implementacija klase Main data je u nastavku.

Datoteka: vezbe/primeri/poglavlje_10/src/zadatak_10_1/Main.java:

package zadatak_10_1;

import org.hibernate.Session;
import org.hibernate.Transaction;

class Main {
    
    public static void main(String[] args) {
        System.out.println("Pocetak rada...\n");
        
        insertStudijskiProgram();
        readStudijskiProgram();
        updateStudijskiProgram();
        deleteStudijskiProgram();
        readStudijskiProgram();
        
        System.out.println("Zavrsetak rada.\n");
        
        // Zatvaranje fabrike sesija
        HibernateUtil.getSessionFactory().close();
    }
    
    private static void insertStudijskiProgram() {
        // Otvaranje sesije
        Session session = HibernateUtil.getSessionFactory().openSession();
        // Kreiranje objekta klase StudijskiProgram.
        // U ovom objektu ce biti zapisane sve informacije o novom studijskom programu,
        // koje ce zatim biti skladistene u bazi podataka.
        StudijskiProgram studijskiProgram = new StudijskiProgram();
        
        // Postavljanje odgovarajucih vrednosti za studijski program
        studijskiProgram.setId(102);
        studijskiProgram.setOznaka("MATF_2020");
        studijskiProgram.setNaziv("Novi MATF studijski program u 2020. godini");
        studijskiProgram.setEspb(240);
        studijskiProgram.setNivo(1);
        studijskiProgram.setZvanje("Diplomirani informaticar");
        studijskiProgram.setOpis("Novi studijski program na Matematickom fakultetu");
        
        // Alternativno, mozemo iskoristiti konstruktor koji prima vrednosti za sva polja
        // StudijskiProgram studijskiProgram = new StudijskiProgram(102, "MATF_2020", "Novi MATF studijski program u 2020. godini", 240, 1, "Diplomirani informaticar", "Novi studijski program na Matematickom fakultetu");
        
        Transaction TR = null;
        try {
            // Zapocinjemo novu transakciju
            TR = session.beginTransaction();
            
            // Skladistimo kreirani studijski program u tabelu STUDIJSKIPROGRAM u bazi podataka
            session.save(studijskiProgram);
            // Pohranjivanje izmena i zavrsavanje transakcije
            TR.commit();
            
            System.out.println("Studijski program je sacuvan");
        } catch (Exception e) {
            // Doslo je do greske: ponistavamo izmene u transakciji
            System.out.println("Cuvanje studijskog programa nije uspelo! Transakcija se ponistava!");
            
            if (TR != null) {
                TR.rollback();
            }
        } finally {
            // Bilo da je doslo do uspeha ili do neuspeha,
            // duzni smo da zatvorimo sesiju
            session.close();
        }
    }

    private static void readStudijskiProgram() {
        Session session = HibernateUtil.getSessionFactory().openSession();
        
        // Ucitavanje (dohvatanje) studijskog programa na osnovu primarnog kljuca
        StudijskiProgram s = session.get(StudijskiProgram.class, 102);

        // Provera da li postoji odgovarajuci slog u tabeli
        if (s != null) {
            System.out.println(s);
        }
        else {
            System.out.println("Studijski program ne postoji!");
        }

        // Zatvaramo sesiju
        session.close();
    }
    
    private static void updateStudijskiProgram() {
        Session session = HibernateUtil.getSessionFactory().openSession();

        // Ucitavanje (dohvatanje) studijskog programa na osnovu primarnog kljuca
        StudijskiProgram s = session.get(StudijskiProgram.class, 102);

        Transaction TR = null;

        try {
            TR = session.beginTransaction();
            
            if (s != null) {
                // Azuriranje odgovarajucih polja
                s.setEspb(180);
            
                // Potvrdjivanje izmena i zavrsavanje transakcije
                TR.commit();
                System.out.println("Studijski program azuriran!");
            } else {
                System.out.println("Studijski program ne postoji!");
            }
        } catch (Exception e) {
            System.out.println("Azuriranje studijskog programa nije uspelo! Ponistavanje transakcije!");
            
            if (TR != null) {
                TR.rollback();
            }
        } finally {
            session.close();
        }
    }

    private static void deleteStudijskiProgram() {
        Session session = HibernateUtil.getSessionFactory().openSession();
        StudijskiProgram studijskiProgram = new StudijskiProgram();

        Transaction TR = null;
        try {
            TR = session.beginTransaction();

            // Ucitavanje (dohvatanje) studijskog programa na osnovu primarnog kljuca
            session.load(studijskiProgram, 102);
            // Brisanje ucitanog studijskog programa iz baze
            session.delete(studijskiProgram);

            System.out.println("Studijski program obrisan!");

            // Potvrdjivanje i zavrsavanje transakcije
            TR.commit();
        } catch (Exception e) {
            System.err.println("Brisanje studijskog programa nije uspelo! Ponistavanje transakcije!");

            if (TR != null) {
                TR.rollback();
            }
        } finally {
            session.close();
        }
    }
}

10.9 Složeni ključ

Upravljanje tabelama koje imaju jednostavne primarne ključeve, kao što je to tabela STUDIJSKIPROGRAM, vrlo je jednostavno. Nešto složenije je rukovati tabelom čiji se primarni ključ sastoji od nekoliko kolona. Takvi primarni ključevi se nazivaju složeni ključevi (engl. compound primary key).

You must create a class to represent this primary key. It will not require a primary key of its own, of course, but it must be visible to entity class, must have a default constructor, must be serializable, and must implement hashCode() and equals() methods to allow the Hibernate code to test for primary key collisions (i.e., they must be implemented with the appropriate database semantics for the primary key values).

Your three strategies for using this primary key class once it has been created are as follows:

  1. Mark it as @Embeddable and add to your entity class a normal property for it, marked with @Id.

  2. Add to your entity class a normal property for it, marked with @EmbeddableId.

  3. Add properties to your entity class for all of its fields, mark them with @Id, and mark your entity class with @IdClass, supplying the class of your primary key class.

Hajde da prođemo kroz svaku od opisanih strategija i prikažemo njihove primere.

10.9.1 Prva strategija - @Embeddable i @Id

The use of @Id with a class marked as @Embeddable, as shown in the following example, is the most natural approach. The @Embeddable annotation allows you to treat the compound primary key as a single property, and it permits the reuse of the @Embeddable class in other tables.

Iz CPKBook.java datoteke:

// Ovo je klasa koja ima slozeni kljuc ISBN
@Entity
public class CPKBook {
    @Id
    ISBN id;
    
    ...
    
}

Iz ISBN.java datoteke:

// Ovo je klasa koja predstavlja slozeni kljuc:
// 1. Anotiramo je anotacijom @Embeddable
@Embeddable
// 2. Implementira interfejs java.io.Serializable
public class ISBN implements Serializable {
    // Naziv "group" je nevalidan naziv kolone u SQL-u
    @Column(name="group_number") 
    int group;
    int publisher;
    int title;
    int checkdigit;
    
    // 3. Ima podrazumevani konstruktor
    public ISBN() {
    }
    
    // Get i set metodi
    ...
    
    // 4. Prevazilazi metode equals i hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ISBN)) return false;
        
        ISBN isbn = (ISBN) o;
    
        if (checkdigit != isbn.checkdigit) return false;
        if (group != isbn.group) return false;
        if (publisher != isbn.publisher) return false;
        if (title != isbn.title) return false;
    
        return true;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(this.group, this.publisher, this.title, this.checkdigit);
    }
}

10.9.2 Druga strategija - @EmbeddedId

The next most natural approach is the use of the @EmbeddedId annotation. Here, the primary key class cannot be used in other tables since it is not an @Embeddable entity, but it does allow us to treat the key as a single attribute of the “table class”.

Često se ovakva klasa implementira kao deo klase koja preslikava tabelu, upravo iz razloga što predstavlja njen deo, tj. neće se koristiti kao primarni ključ neke druge klase.

@Entity
public class EmbeddedPKBook {
    @EmbeddedId
    EmbeddedISBN id;

    @Column
    String name;
    
    // Get/set metodi

    static class EmbeddedISBN implements Serializable {
        @Column(name="group_number")
        int group;
        int publisher;
        int title;
        int checkdigit;
        
        public ISBN() {
        }
        
        // Get/set metodi, equals, hashCode...
    }
}

10.9.3 Treća strategija - @IdClass i @Id

Finally, the use of the @IdClass and @Id annotations allows us to map the compound primary key class using properties of the entity itself corresponding to the names of the properties in the primary key class. The names must correspond (there is no mechanism for overriding this), and the primary key class must honor the same obligations as with the other two techniques. The only advantage to this approach is its ability to “hide” the use of the primary key class from the interface of the enclosing entity.

The @IdClass annotation takes a value parameter of Class type, which must be the class to be used as the compound primary key. The fields that correspond to the properties of the primary key class to be used must all be annotated with @Id — note in the following code example that the class properties group, publisher, title and checkdigit are so annotated, and the EmbeddedISBN class is not mapped as @Embeddable, but it is supplied as the value of the @IdClass annotation.

@Entity
@IdClass(IdClassBook.EmbeddedISBN.class)
public class IdClassBook {
    @Id
    int group;
    @Id
    int publisher;
    @Id
    int title;
    @Id
    int checkdigit;
    String name;
    
    public IdClassBook() {
    }
    
    // Get/set metodi
    
    static class EmbeddedISBN implements Serializable {
        @Column(name="group_number")
        int group;
        int publisher;
        int title;
        int checkdigit;
        
        public ISBN() {
        }
        
        // Get/set metodi, equals, hashCode...
    }
}

Zadatak 10.2: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate implementira unos podataka o novom ispitnom roku (jun 2020. godine) u tabelu ISPITNIROK, a zatim briše podatke o unetom ispitnom roku iz tabele ISPITNIROK.

Rešenje: Potrebno je da prvo napravimo klasu koja će predstavljati složeni ključ. Nazovimo je IspitniRokId. U nastavku je data njena implementacija. Obratiti pažnju na metode equals() i hashCode(). Definicije ovih metoda će biti gotovo identične za sve klase koje ih prevazilaze iz klase Object:

Cela implementacija je data u nastavku:

Datoteka: vezbe/primeri/poglavlje_10/src/zadatak_10_2/IspitniRokId.java:

package zadatak_10_2;

import java.io.Serializable;
import java.util.Objects;

import javax.persistence.Embeddable;

// Tabela ISPITNI_ROK ima primarni kljuc koji se sastoji od dve kolone. 
// Ovakav primarni kljuc se naziva slozeni kljuc.
// Za slozeni kljuc je potrebno da se kreira posebna klasa 
// koja mora da implementira interfejs java.io.Serializable. 
// Stoga moraju biti definisana i naredna dva metoda: equals() i hashCode(). 
// Takodje, neophodno je da ima definisan i podrazumevani konstruktor.

// S obzirom da se ova klasa koristi kao primarni kljuc za drugu klasu,
// onda je ne anotiramo pomocu @Entity,
// vec koristimo anotaciju @Embeddable
@Embeddable
class IspitniRokId implements Serializable {

    // Podrazumevani serijski ID verzije
    private static final long serialVersionUID = 1L;

    // Kolone koje ulaze u primarni kljuc
    private Integer skGodina;
    private String oznakaRoka;

    // Podrazumevani konstruktor
    public IspitniRokId() {
    }

    public IspitniRokId(Integer godina, String oznaka) {
        this.skGodina = godina;
        this.oznakaRoka = oznaka;
    }

    // Autogenerisani get/set metodi
    public Integer getSkGodina() {
        return skGodina;
    }

    public void setSkGodina(Integer godina) {
        this.skGodina = godina;
    }

    public String getOznakaRoka() {
        return oznakaRoka;
    }

    public void setOznakaRoka(String oznaka) {
        this.oznakaRoka = oznaka;
    }

    // Prevazilazenje metoda radi testiranja kolizije primarnih kljuceva

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof IspitniRokId)) {
            return false;
        }

        IspitniRokId irOther = (IspitniRokId) o;

        return Objects.equals(this.skGodina, irOther.getSkGodina()) && Objects.equals(this.oznakaRoka, irOther.getOznakaRoka());
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.skGodina, this.oznakaRoka);
    }
}

Zatim je potrebno specifikovati instancu ove klase kao primarni ključ klase IspitniRok.java, koju takođe kreiramo:

Datoteka: vezbe/primeri/poglavlje_10/src/zadatak_10_2/IspitniRok.java:

package zadatak_10_2;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
// Anotacija @Table je neophodna jer se ime klase i ime tabele razlikuju.
@Table(name = "DA.ISPITNIROK")
class IspitniRok {
    // Za primarni kljuc koristimo instancu klase IspitniRokId,
    // s obzirom da ova tabela ima slozeni kljuc.
    // Pogledati klasu IspitniRokId za jos informacija.

    @Id
    private IspitniRokId id = null;

    // Ostale kolone

    @Column(name = "NAZIV", nullable = false)
    private String Naziv;

    @Column(name = "DATPOCETKA", nullable = false)
    private String Pocetak;

    @Column(name = "DATKRAJA", nullable = false)
    private String Kraj;

    // Autogenerisani Get/Set metodi

    public IspitniRokId getId() {
        return id;
    }

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

    public String getNaziv() {
        return Naziv;
    }

    public void setNaziv(String naziv) {
        Naziv = naziv;
    }

    public String getPocetak() {
        return Pocetak;
    }

    public void setPocetak(String pocetak) {
        Pocetak = pocetak;
    }

    public String getKraj() {
        return Kraj;
    }

    public void setKraj(String kraj) {
        Kraj = kraj;
    }

}

Ne zaboravimo da dodamo IspitniRok kao anotiranu klasu u konfiguraciju:

Datoteka: vezbe/primeri/poglavlje_10/src/zadatak_10_2/HibernateUtil.java:

package zadatak_10_2;

import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

class HibernateUtil {
    private static SessionFactory sessionFactory = null;

    static {
        try {
            StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
            sessionFactory = new MetadataSources(registry).addAnnotatedClass(StudijskiProgram.class)
                    .addAnnotatedClass(IspitniRok.class).buildMetadata().buildSessionFactory();
        } catch (Throwable e) {
            System.err.println("Session factory error");
            e.printStackTrace();

            System.exit(1);
        }
    }

    static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

}

Definisane klase se sada jednostavno koriste u metodima insertIspitniRok() i deleteIspitniRok() proširene klase Main.java iz prethodnog primera:

Datoteka: vezbe/primeri/poglavlje_10/src/zadatak_10_2/Main.java:

package zadatak_10_2;

import org.hibernate.Session;
import org.hibernate.Transaction;

class Main {
    
    public static void main(String[] args) {
        System.out.println("Pocetak rada...\n");

        insertIspitniRok();
        deleteIspitniRok();
        
        System.out.println("Zavrsetak rada.\n");
        HibernateUtil.getSessionFactory().close();
    }

    private static void insertIspitniRok() {
        Session session = HibernateUtil.getSessionFactory().openSession();
        
        // Kreiramo praznu instancu objekta ispitnog roka 
        // koju cemo popuniti vrednostima koje treba sacuvati
        IspitniRok ir = new IspitniRok();
        
        // Kreiramo prvo identifikator, tj. slozeni kljuc,
        // a zatim i ostale podatke
        IspitniRokId id = new IspitniRokId(2020, "jun");
        ir.setId(id);
        ir.setNaziv("Jun 2021");
        ir.setPocetak("6/1/2021");
        ir.setKraj("6/22/2021");
        
        // Procedura za cuvanje je ista kao i do sada
        Transaction TR = null;
        try {
            TR = session.beginTransaction();
            
            session.save(ir);
            
            System.out.println("Ispitni rok je sacuvan!");
            TR.commit();
        } catch (Exception e) {
            System.err.println("Cuvanje ispitnog roka nije uspelo! Ponistavanje transakcije!");
            
            if (TR != null) {
                TR.rollback();
            }
        } finally {
            session.close();
        }
    }
    
    private static void deleteIspitniRok() {
        Session session = HibernateUtil.getSessionFactory().openSession();
        
        IspitniRok ir = new IspitniRok();
        IspitniRokId id = new IspitniRokId(2020, "jun");
        
        Transaction TR = null;
        try {
            TR = session.beginTransaction();
            
            session.load(ir, id);
            session.delete(ir);
            
            System.out.println("Ispitni rok je obrisan!");
            TR.commit();
        } catch (Exception e) {
            System.err.println("Brisanje ispitnog roka nije uspelo! Ponistavanje transakcije!");
        
            if (TR != null) {
                TR.rollback();
            }
        } finally {
            session.close();
        }        
    }

}

10.10 Zadaci za vežbu

Zadatak 10.3: Napisati Java aplikaciju koja koriš’cenjem biblioteke Hibernate redom:

  1. Unosi podatak o novom novou kvalifikacije u tabelu NIVOKVALIFIKACIJE sa podacima iz naredne tabele.
  2. Ispisuje podatake o novou kvalifikacije sa identifikatorom 42 iz tabele NIVOKVALIFIKACIJE.
  3. Ažurira stepen za nivo kvalifikacije sa identifikatorom 42 iz tabele NIVOKVALIFIKACIJE. Naziv postaviti na vrednost Novi nivo kvalifikacije.
  4. Ispisuje podatake o novou kvalifikacije sa identifikatorom 42 iz tabele NIVOKVALIFIKACIJE.
  5. Briše podatake o novou kvalifikacije sa identifikatorom 42 iz tabele NIVOKVALIFIKACIJE.
  6. Ispisuje podatake o novou kvalifikacije sa identifikatorom 42 iz tabele NIVOKVALIFIKACIJE.

Svaki zahtev implementirati kao posebnu transakciju.

Kolona Vrednost
Identifikator 42
Naziv Novi nivo

Zadatak 10.4: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate redom:

  1. Unosi podatak o novom predmetu u tabelu PREDMET sa identifikatorom predmeta id i ostalim podacima koji se unose sa standardnog ulaza.
  2. Ispisuje podatake o predmetu sa identifikatorom id iz tabele PREDMET.
  3. Proverava da li korisnik želi da ažurira broj ESPB bodova za predmet sa identifikatorom id u tabeli PREDMET. Ukoliko korisnik odgovori potvrdno, izvršava odgovarajuće ažuriranje. Novi broj bodova unosi se sa standardnog ulaza.
  4. Ispisuje podatake o predmetu sa identifikatorom id iz tabele PREDMET.
  5. Briše podatake o predmetu sa identifikatorom id iz tabele PREDMET.
  6. Ispisuje podatake o predmetu sa identifikatorom id iz tabele PREDMET.

Svaki zahtev implementirati kao posebnu transakciju.