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
):
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 |
STUDIJSKIPROGRAM
.STUDIJSKIPROGRAM
.STUDIJSKIPROGRAM
.ISPITNIROK
.ISPITNIROK
.Takođe, omogućiti da obrada svakog zahteva predstavlja zasebnu transakciju.
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:
Iz glavnog menija odabrati File -> New -> Java Project
poglavlje_10
.Desni klik na projekat koji je napravljen u Package Explorer -> Properties
Podesiti podršku za JDBC:
Podesiti podršku za Hibernate:
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.
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
.
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.
Zadatak 10.1: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate redom:
STUDIJSKIPROGRAM
sa podacima iz naredne tabele,102
iz tabele STUDIJSKIPROGRAM
,102
iz tabele STUDIJSKIPROGRAM
,102
iz tabele STUDIJSKIPROGRAM
,102
iz tabele STUDIJSKIPROGRAM
,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:
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.
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 + "]";
}
}
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.
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.
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);
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();
}
}
}
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:
Mark it as @Embeddable
and add to your entity class a normal property for it, marked
with @Id
.
Add to your entity class a normal property for it, marked with @EmbeddableId
.
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.
@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);
}
}
@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...
}
}
@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
:
Metod equals()
prvo ispituje da li je reference objekata this
i o
(koji se prosleđuje
kao argument metoda) poklapaju. Zatim proverava da li drugi objekat (o
) predstavlja referencu
iste klase kao objekat sa kojim se poredi (this
). Zatim proverava da
li se svi atributi objekata poklapaju pozivajući metod Objects.equals
za svaki par
atributa.
Metod hashCode()
jednostavno poziva metod Objects.hash
koja prima proizvoljni
broj argumenata i vraća heš vrednost na osnovu njihovih vrednosti. Obično je bolje
koristiti ovaj metod nego implementirati svoju heš funkciju.
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();
}
}
}
Zadatak 10.3: Napisati Java aplikaciju koja koriš’cenjem biblioteke Hibernate redom:
NIVOKVALIFIKACIJE
sa podacima iz naredne tabele.42
iz tabele NIVOKVALIFIKACIJE
.42
iz tabele NIVOKVALIFIKACIJE
. Naziv postaviti na vrednost Novi nivo kvalifikacije
.42
iz tabele NIVOKVALIFIKACIJE
.42
iz tabele NIVOKVALIFIKACIJE
.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:
PREDMET
sa identifikatorom predmeta id
i ostalim podacima koji se unose sa standardnog ulaza.id
iz tabele PREDMET
.id
u tabeli PREDMET
. Ukoliko korisnik odgovori potvrdno, izvršava odgovarajuće ažuriranje. Novi broj bodova unosi se sa standardnog ulaza.id
iz tabele PREDMET
.id
iz tabele PREDMET
.id
iz tabele PREDMET
.Svaki zahtev implementirati kao posebnu transakciju.