U prethodnom poglavlju smo se upoznali sa osnovama objektno-relacionih preslikavanja. Ovo poglavlje će biti posvećeno naprednijim konceptima. Upoznaćemo se sa jezikom HQL, koja predstavlja pandan jeziku SQL, samo u radu sa Java klasama i objektima. Dodatno, obratićemo veliku pažnju problemu objektno-relacionih preslikavanja stranih ključeva i načinima za njihovu implementaciju.
Za rad sa potencijalno većim brojem slogova u tabeli, na raspolaganju su nam stoji HQL (Hibernate Query Language). While most ORM tools and object databases offer an object query language, Hibernate’s HQL stands out as complete and easy to use. HQL was inspired by SQL and is a major inspiration for the Java Persistence Query Language (JPQL).
UPDATE
alters the details of existing objects in the database. In-memory entities, managed
or not, will not be updated to reflect changes resulting from issuing UPDATE
statements.
Here’s the syntax of the UPDATE
statement:
UPDATE [VERSIONED]
[FROM] path [[AS] alias] [, ...]
SET property = value [, ...]
[WHERE logicalExpression]
The fully qualified name of the entity or entities is path
. The alias
names may be used
to abbreviate references to specific entities or their properties, and must be used when
property names in the query would otherwise be ambiguous. VERSIONED
means that the
update will update timestamps, if any, that are part of the entity being updated. The
property
names are the names of properties of entities listed in the FROM
path.
An example of the update in action might look like this:
Query query = session.createQuery(
"UPDATE Person SET creditscore = :score WHERE firstname = :name");
query.setInteger("score", 612);
query.setString("name", "John Q. Public");
int modifications = query.executeUpdate();
DELETE
removes the details of existing objects from the database. In-memory entities will
not be updated to reflect changes resulting from DELETE
statements. This also means
that Hibernate’s cascade rules will not be followed for deletions carried out using HQL.
However, if you have specified cascading deletes at the database level (either directly or
through Hibernate, using the @OnDelete
annotation), the database will still remove the
child rows. This approach to deletion is commonly referred to as ”bulk deletion”, since it
is the most efficient way to remove large numbers of entities from the database. Here’s
the syntax of the DELETE
statement:
DELETE
[FROM] path [[AS] alias]
[WHERE logicalExpression]
The fully qualified name of the entity or entities is path
. The alias
names may be used
to abbreviate references to specific entities or their properties, and must be used when
property names in the query would otherwise be ambiguous.
In practice, deletes might look like this:
Query query = session.createQuery(
"DELETE FROM Person WHERE accountstatus = :status");
query.setString("status", "purged");
int rowsDeleted = query.executeUpdate();
A HQL INSERT
cannot be used to directly insert arbitrary entities — it can only be used to
insert entities constructed from information obtained from SELECT
queries (unlike ordinary
SQL, in which an INSERT
command can be used to insert arbitrary data into a table, as
well as insert values selected from other tables). Here’s the syntax of the INSERT
statement:
INSERT
INTO path ( property [, ...])
select
The name of an entity is path
. The property
names are the names of properties of entities
listed in the FROM
path of the incorporated SELECT
query. The select query is a HQL
SELECT
query (as described in the next section). As this HQL statement can only use data
provided by a HQL select, its application can be limited. An example of copying users to
a purged table before actually purging them might look like this:
Query query = session.createQuery(
"INSERT INTO PURGED_USERS(id, name, status) " +
"SELECT id, name, status FROM User WHERE status = :status");
query.setString("status", "purged");
int rowsCopied = query.executeUpdate();
A HQL SELECT
is used to query the database for classes and their properties. As noted
previously, this is very much a summary of the full expressive power of HQL SELECT
queries;
however, for more complex joins and the like, you may find that using the Criteria API is
more appropriate. Here’s the syntax of the SELECT
statement:
[SELECT [DISTINCT] property [, ...]]
FROM path [[AS] alias] [, ...] [FETCH ALL PROPERTIES]
WHERE logicalExpression
GROUP BY property [, ...]
HAVING logicalExpression
ORDER BY property [ASC | DESC] [, ...]
The fully qualified name of the entity or entities is path
. The alias
names may be used
to abbreviate references to specific entities or their properties, and must be used when
property names in the query would otherwise be ambiguous. The property names are the
names of properties of entities listed in the FROM
path.
If FETCH ALL PROPERTIES
is used, then lazy loading semantics will be ignored, and all the
immediate properties of the retrieved object(s) will be actively loaded (this does not apply
recursively).
When the properties listed consist only of the names of aliases in the FROM
clause, the
SELECT
clause can be omitted in HQL.
We have already discussed the basics of the from clause in HQL. The most important feature to note is the alias. Hibernate allows you to assign aliases to the classes in your query with the as clause. Use the aliases to refer back to the class inside the query. For instance, instead of a simple HQL query
FROM Supplier
the example could be the following:
FROM Product AS p
The AS
keyword is optional — you can also specify the alias directly after the class name,
as follows:
FROM Product p
If you need to fully qualify a class name in HQL, just specify the package and class name. Hibernate will take care of most of this behind the scenes, so you really need this only if you have classes with duplicate names in your application. If you have to do this in Hibernate, use syntax such as the following:
FROM businessapp.model.Product
The select clause provides more control over the result set than the from clause. If you want to obtain the properties of objects in the result set, use the select clause. For instance, we could run a projection query on the products in the database that only returned the names, instead of loading the full object into memory, as follows:
SELECT product.name FROM Product product
The result set for this query will contain a List
of Java String
objects. Additionally, we
can retrieve the prices and the names for each product in the database, like so:
SELECT product.name, product.price FROM Product product
This result set contains a List
of Object
arrays (therefore, List<Object[]>
) — each array
represents one tuple of properties (in this case, a pair that represents name and price).
If you’re only interested in a few properties, this approach can allow you to reduce network traffic to the database server and save memory on the application’s machine.
Hibernate supports named parameters in its HQL queries. The simplest example of named parameters uses regular SQL types for the parameters:
String hql = "FROM Product WHERE price > :price";
Query query = session.createQuery(hql);
query.setParameter("price", 25.0);
List results = query.list();
Metod setParameter()
uzima naziv parametarske oznake kao prvi argument i vrednost
kojom ona treba biti zamenjena kao drugi argument. Hibernate će automatski pokušati da dedukuje
tip na osnovu pozicije imenovane parametarske oznake u upitu. Alternativno, postoji preopterećenje
ovog metoda koje prihvata dodatni argument kojim se navodi tip te parametarske oznake.
Naredni kod navodimo tek radi kompletnosti, bez ulaženja u detalje. Jedino na šta ćemo
skrenuti pažnju jeste metod getSingleResult
klase org.hibernate.query.Query
koji dohvata tačno jedan rezultat iz tabele koji zadovoljava HQL upit.
// Dohvatamo model koji sadrzi meta-informacije.
MetamodelImplementor metamodelImplementor =
(MetamodelImplementor) HibernateUtil.getSessionFactory().getMetamodel();
// Izracunavamo tip atributa `caption` u klasi `Photo`.
Type captionType = metamodelImplementor
.entityPersister( Photo.class.getName() )
.getPropertyType( "caption" );
// HQL upit kojim dohvatamo sve slike koje imaju odgovarajuci naziv.
String hql =
"select p " +
"from Photo p " +
"where upper(caption) = upper(:caption) ";
// Koristimo varijantu metoda `setParameter()` sa tri argumenta
// kako bismo nagovestili Hibernate-u koji je tip parametarske oznake sa nazivom "caption".
Photo photo = (Photo) session.createQuery(hql, Photo.class )
.setParameter("caption", new Caption("Moja prva fotografija u novoj godini"), captionType)
.getSingleResult();
Normally, you do not know the values that are to be substituted for the named parameters;
if you did, you would probably encode them directly into the query string. When the value
to be provided will be known only at run time, you can use some of HQL’s object-oriented
features to provide objects as values for named parameters. The Query interface has a
setEntity()
method that takes the name of a parameter and an object.
Using this functionality, we could retrieve all the products that have a supplier whose object we already have:
String supplierHQL = "FROM Supplier WHERE name = 'MegaInc'";
Query supplierQuery = session.createQuery(supplierHQL);
Supplier supplier = (Supplier) supplierQuery.list().get(0);
String hql = "FROM Product AS product WHERE product.supplier = :supplier";
Query query = session.createQuery(hql);
query.setEntity("supplier", supplier);
List results = query.list();
Associations allow you to use more than one class in HQL query, just as SQL allows you
to use joins between tables in a relational database. You add an association to a HQL
query with the JOIN
clause.
Hibernate supports five different types of joins: INNER JOIN
, CROSS JOIN
, LEFT OUTER JOIN
,
RIGHT OUTER JOIN
, and FULL OUTER JOIN
. If you use CROSS JOIN
, just specify both classes
in the FROM
clause (FROM Product p, Supplier s
). For the other joins, use a JOIN
clause
after the FROM
clause. Specify the type of join, the object property to join on, and an alias
for the other class.
For example, you can use INNER JOIN
to obtain the supplier for each product, and then retrieve the
supplier name, product name, and product price, as so:
SELECT s.name, p.name, p.price FROM Product p INNER JOIN p.supplier AS s
You can retrieve the objects using similar syntax:
FROM Product p INNER JOIN p.supplier AS s
Notice that Hibernate does not return Object
objects in the result set; instead, Hibernate
returns Object
arrays in the results. This join results in a projection, thus the use of
the Object
arrays. You will have to access the contents of the Object
arrays to get the
Supplier
and the Product
objects.
Zadatak 11.1: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate ispisuje podatke o svim ispitnim rokovima.
Rešenje: Zahtev je implementiran u metodu readispitniRokovi()
klase Main.java
:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_1/Main.java
:
package zadatak_11_1;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
class Main {
public static void main(String[] args) {
System.out.println("Pocetak rada...\n");
readIspitniRokovi();
System.out.println("Zavrsetak rada.\n");
HibernateUtil.getSessionFactory().close();
}
private static void readIspitniRokovi() {
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction TR = null;
try {
TR = session.beginTransaction();
// HQL upit za izdvajanje svih entiteta tipa IspitniRok
String hql = "FROM IspitniRok";
// Kreiranje objekta koji sadrzi informacije o HQL upitu.
// Obratiti paznju da se klasa Query nalazi u paketu org.hibernate.query!!!
// Takodje, pored samog HQL upita,
// metodu createQuery prosledjujemo klasu koja predstavlja entitet rezultata.
// Drugim recima, kazemo Hibernate-u da zelimo da dohvatimo listu ispitnih rokova.
Query<IspitniRok> upit =
session.createQuery(hql, IspitniRok.class);
// Pozivom metoda list() dohvatamo zeljeni rezultat
List<IspitniRok> ispitniRokovi = upit.list();
// Iteriranje kroz listu
for(IspitniRok ir : ispitniRokovi) {
System.out.println(ir);
}
TR.commit();
} catch (Exception e) {
System.err.println("Postoji problem sa ispisivanjem ispitnih rokova! Ponistavanje transakcije!");
e.printStackTrace();
if (TR != null) {
TR.rollback();
}
} finally {
session.close();
}
}
}
Jedan od najznačajnijih elemenata relacionih baza podataka jesu tzv. asocijativne veze koje se ostvaruju između tabela. Na šta mislimo pod tim pojmom? Neka su nam date dve tabele STUDENT
i KNJIGA
čije su sheme date u nastavku:
CREATE TABLE STUDENT (
ID_STUDENTA INTEGER NOT NULL,
IME_PREZIME VARCHAR(50) NOT NULL,
NAZIV_SMERA VARCHAR(50) NOT NULL,
PRIMARY KEY (ID_STUDENTA)
)
CREATE TABLE KNJIGA (
ID_KNJIGE INTEGER NOT NULL,
NAZIV_KNJIGE VARCHAR(50) NOT NULL,
AUTOR VARCHAR(50) NOT NULL,
PRIMARY KEY (ID_KNJIGE)
)
Ove dve tabele trenutno nisu povezane, odnosno, slogovi iz tabele STUDENT
i slogovi iz tabele KNJIGA
“žive” potpuno odvojeno i bilo kakva izmena jedne tabele nikako ne utiče na drugu tabelu. Međutim, šta ukoliko je potrebno da čuvamo informaciju o tome koji student je pozajmio koju knjigu iz biblioteke? Implementiranje ovog zahteva će kreirati asocijativnu vezu između ovih tabela jer sada postoji zavisnost među podacima iz ovih tabela.
U zavisnosti od toga koja je semantika veze koja se ostvaruje podacima, razlikujemo nekoliko tipova veza. Da bismo odabrali ispravan tip veze, potrebno je da sebi postavimo naredna pitanja:
Odgovori na ova pitanja, koja su postavljena u poslovnom domenu, direktno utiču na tip asocijativne veze koja se formira. Tipovi veza, zajedno sa odgovorima, dati su u narednoj tabeli:
Odgovor na P1 | Odgovor na P2 | Veza između STUDENT i KNJIGA |
---|---|---|
Ne | Ne | Jedan-ka-jedan |
Da | Ne | Više-ka-jedan |
Ne | Da | Jedan-ka-više |
Da | Da | Više-ka-više |
Veza jedan-ka-jedan se može ostvariti na nekoliko načina. U najjednostavnijoj varijanti, svojstva obe klase se čuvaju u istoj tabeli:
CREATE TABLE STUDENTKNJIGA (
SKID INTEGER NOT NULL,
IME_PREZIME ...,
NAZIV_SMERA ...,
NAZIV_KNJIGE ...,
AUTOR ...,
PRIMARY KEY (SKID)
)
U Java domenu:
@Entity
Class StudentKnjiga {
@Id
private Integer skid;
// Ostala polja i get/set metodi
}
Alternativno, trajni objekti se mogu čuvati u različitim tabelama, od kojih svaka ima svoj primarni ključ, ali za svakog studenta i svaku knjigu mora da važi naredno pravilo: student koji je vlasnik neke knjige i ta knjiga moraju imati isti primarni ključ.
Moguće je održavati strani ključ od jednog entiteta ka drugom, ali ne bismo smeli imali dvosmerni strani ključ u ovakvom odnosu jer bismo kreirali kružnu zavisnost. Naravno, možemo da ne kreiramo strani ključ i da ostavimo Hibernate-u da se stara o tome. U tom slučaju bi tabele izgledale:
CREATE TABLE STUDENT (
ID_STUDENTA INTEGER NOT NULL,
IME_PREZIME ...,
NAZIV_SMERA ...,
PRIMARY KEY (ID_STUDENTA)
)
CREATE TABLE KNJIGA (
ID_KNJIGE INTEGER NOT NULL,
NAZIV_KNJIGE ...,
AUTOR ...,
PRIMARY KEY (ID_KNJIGE)
)
Dok bi klase izgledale:
@Entity
class Student {
@Id
private Integer id_studenta;
// ...
}
@Entity
class Knjiga {
@Id
private Integer id_knjige;
// ...
}
Poslednja opcija je da se održava odnos stranim ključem između dve tabele, pri čemu se postavlja opcija unique nad kolonom koja predstavlja strani ključ. Ovaj pristup ima prednost u tome što jednostavnim uklanjanjem opcije unique, od veze jedan-ka-jedan može se ostvariti veza više-ka-jedan. U nastavku dajemo kako bi izgledale tabele u ovom pristupu:
CREATE TABLE STUDENT (
ID_STUDENTA INTEGER NOT NULL,
IME_PREZIME ...,
NAZIV_SMERA ...,
PRIMARY KEY (ID_STUDENTA)
)
CREATE TABLE KNJIGA (
ID_KNJIGE INTEGER NOT NULL,
NAZIV_KNJIGE ...,
AUTOR ...,
ID_STUDENTA INTEGER NOT NULL,
PRIMARY KEY (ID_KNJIGE),
FOREIGN KEY (ID_STUDENTA)
REFERENCES STUDENT,
UNIQUE (ID_STUDENTA)
)
Dok bi klase izgledale:
@Entity
class Student {
@Id
private Integer id_studenta;
@OneToOne(mappedBy="student")
private Knjiga knjiga;
// Get/set metodi
}
@Entity
class Knjiga {
@Id
private Integer id_knjige;
// Ostala polja
@OneToOne
private Student student;
// Get/set metodi
}
Ono što prvo primećujemo u kodu jeste da smo uveli novu anotaciju @OneToOne
. Ova anotacija se koristi ukoliko jedna klasa sadrži referencu na objekat druge klase (na primer, Knjiga
sadrži polje student
koje čuva referencu na klasu Student
) i ukoliko želimo da kažemo Hibernate-u da je veza koja se ostvaruje jedan-ka-jedan.
Da klasa Student
ne čuva referencu ka klasi Knjiga
(kroz polje knjiga
), time bi ovaj odnos bio rešen. Međutim, ukoliko nam je potrebno da obe klase čuvaju referencu jedna ka drugoj, onda dobijamo situaciju kao u prethodnom Java kodu. Ovakav pristup dovodi do toga da je veza koja se ostvaruje bidirekciona, odnosno, za datog studenta možemo dobiti referencu na knjigu čiji je on vlasnik, ali takođe, za datu knjigu, možemo dobiti referencu na studenta koji je njen vlasnik. U tom slučaju, potrebno je da obe reference (polja knjiga
i student
) u obe klase (Student
i Knjiga
, redom) budu dekorisana anotacijom @OneToOne
.
Međutim, ovim i dalje nije rešen problem jer kako Hibernate da zna da li se ova bidirekciona veza održava u tabeli STUDENT
ili u tabeli KNJIGA
? I drugi tipovi veze mogu dovesti do ovakve situacije i rešenje se sastoji u postavljanju opcije mappedBy
u odgovarajućoj anotaciji (u ovom primeru @OneToOne
) kojom se navodi koje polje u drugoj klasi ostvaruje vezu stranog ključa. U ovom slučaju, želimo da postoji strani ključ nad kolonom ID_STUDENTA
u tabeli KNJIGA
, pa zato u klasi Student
navodimo da je
veza ostvarena kroz polje student
u klasi Knjiga
. Vrlo je važno obratiti pažnju na to gde se ova opcija postavlja kako bismo ispravno implementirali ovakvu zavisnost!
Ova dva tipa veze u suštini predstavljaju jedan tip veze, zato što, ako je iz ugla tabele A
ostvarena veza jedan-ka-više ka tabeli B
, onda je posledično iz ugla tabele B
ostvarena veza više-ka-jedan ka tabeli A
. Važi i obratno. Veza jedan-ka-više (ili iz perspektive druge klase, više-ka-jedan) može biti jednostavno reprezentovana stranim ključem ka primarnom ključu tabele koja se nalazi na “jedan” kraju ove veze u tabeli koja se nalazi na “više” kraju ove veze, bez dodatnih ograničenja.
Pretpostavimo da jedan student može imati više knjiga i da jedna knjiga može pripadati tačno jednom studentu. U tom slučaju, ostvaruje se veza jedan-ka-više od tabele STUDENT
ka tabeli KNJIGA
(odnosno, veza više-ka-jedan od tabele KNJIGA
ka tabeli STUDENT
). Shema tabela bi izgledala:
CREATE TABLE STUDENT (
ID_STUDENTA INTEGER NOT NULL,
IME_PREZIME ...,
NAZIV_SMERA ...,
PRIMARY KEY (ID_STUDENTA)
)
CREATE TABLE KNJIGA (
ID_KNJIGE INTEGER NOT NULL,
NAZIV_KNJIGE ...,
AUTOR ...,
ID_STUDENTA INTEGER NOT NULL,
PRIMARY KEY (ID_KNJIGE),
FOREIGN KEY (ID_STUDENTA)
REFERENCES STUDENT
)
Implementacija klasa u Java domenu izgleda:
@Entity
class Student {
@Id
private Integer id_studenta;
@OneToMany(mappedBy="student")
private List<Knjiga> knjige;
// Get/set metodi
}
@Entity
class Knjiga {
@Id
private Integer id_knjige;
// Ostala polja
@ManyToOne
@JoinColumn(name="id_studenta")
private Student student;
// Get/set metodi
}
Ono što bi trebalo da primetimo iz prethodnog koda je sledeće:
@OneToMany
i @ManyToOne
da definišemo odgovarajući tip veze između ovih klasa. U klasi koja predstavlja “jedan” stranu veze (Student
) koristimo @OneToMany
, a u klasi koja predstavlja “više” stranu te iste veze (Knjiga
) koristimo @ManyToOne
.Student
više nema smisla čuvati referencu na jednu knjigu, pošto jedan student može da poseduje više knjiga. Zbog toga, potrebno je da čuvamo kolekciju referenci na knjige. Možemo koristiti razne vrste kolekcija, kao što su java.util.List<T>
, java.util.Set<T>
, java.util.Map<K, V>
, itd.@JoinColumn
da specifikujemo naziv kolone koja učestvuje u stranom ključu koji se kreira u ovoj vezi. Ovo je posebno korisno ukoliko se kolone koje učestvuju u stranom ključu ne zovu isto.Alternativno, veza jedan-ka-više/više-ka-jedan može se održavati postojanjem treće tabele koja se naziva spojna tabela (engl. link table). U našem primeru, ovakva tabela bi održavala strane ključeve ka tabelama STUDENT
i KNJIGA
, a te kolone bi učestvovale u primarnom ključu spojne tabele. Dodatno, mora biti postavljeno ograničenje jedinstvenosti nad jednom stranom odnosa — inače, spojna tabela može predstavljati sve moguće kombinacije, čime bi se ostvarila veza više-ka-više. Dajmo primer sheme spojne tabele:
CREATE TABLE STUDENTKNJIGALINK (
ID_STUDENTA INTEGER NOT NULL,
ID_KNJIGE INTEGER NOT NULL,
PRIMARY KEY (ID_STUDENTA, ID_KNJIGE),
FOREIGN KEY (ID_STUDENTA)
REFERENCES STUDENT,
FOREIGN KEY (ID_KNJIGE)
REFERENCES KNJIGA,
UNIQUE (ID_KNJIGE)
)
Korišćenje spojne tabele se ostvaruje korišćenjem anotacije @JoinTable
, o kojoj će biti reči u nastavku.
Kao što smo napomenuli na kraju prethodne podsekcije, ako se ograničenje jedinstvenosti ne primeni na “jedan” kraju bidirekcione asocijativne veze prilikom korišćenja spojne tabele, ta veza postaje više-ka-više veza. Sve moguće varijante parova (ID_STUDENTA, ID_KNJIGE)
mogu biti reprezentovane, ali nije moguće da jedan student ima više puta istu knjigu, s obzirom da bi u takvoj situaciji došlo do dupliciranja složenog primarnog ključa te spojne tabele.
Ukoliko umesto tog pristupa dodelimo spojnoj tabeli svoj primarni ključ, koji ne sadrži kolone koje ulaze kao deo stranih ključeva ka drugim tabelama, onda time možemo reprezentovati punu više-ka-više vezu, naravno, ukoliko to odgovara domenskom problemu. U tom slučaju, shema spojne tabele bi mogla izgledati kao u narednom kodu:
CREATE TABLE STUDENTKNJIGALINK (
SUROGAT_ID INTEGER NOT NULL,
ID_STUDENTA INTEGER NOT NULL,
ID_KNJIGE INTEGER NOT NULL,
PRIMARY KEY (SUROGAT_ID),
FOREIGN KEY (ID_STUDENTA)
REFERENCES STUDENT,
FOREIGN KEY (ID_KNJIGE)
REFERENCES KNJIGA
)
Bez obzira na pristup u SQL domenu, u Java domenu se koristi anotacija @ManyToMany
i ona se navodi u obe klase. Kao i do sada, ukoliko je veza bidirekciona, potrebno je navesti mappedBy
opciju u odgovarajućoj anotaciji. Ako jedna klasa navede ovu opciju, onda je druga strana vlasnik asocijacije i vrednost opcije mora biti polje te druge klase. Dajemo primer korišćenja kroz klase Student
i Knjiga
:
@Entity
class Student {
@Id
private Integer id_studenta;
@ManyToMany(mappedBy="studenti")
private List<Knjiga> knjige;
// Get/set metodi
}
@Entity
class Knjiga {
@Id
private Integer id_knjige;
@ManyToMany
@JoinTable(
name = "STUDENTKNJIGALINK",
joinColumns = { @JoinColumn(name = "ID_KNJIGE") },
inverseJoinColumns = { @JoinColumn(name = "ID_STUDENTA") }
)
private List<Student> studenti;
// Get/set metodi
}
Anotacija @JoinTable
u prethodnom fragmentu koda ukazuje na to da ce biti kreirana spojna tabela pod nazivom STUDENTKNJIGALINK
. Atributom joinColumns
anotacije @JoinTable
se postavljaju kolone stranog kljuca spojne tabele koje referišu na primarni ključ entiteta koji je odgovoran za odrzavanje asocijacije (ovde je to Knjiga
). Nasuprot tome, atributom inverseJoinColumns
se postavljaju kolone stranog ključa spojne tabele koje referišu na primarni ključ onog drugog entiteta (ovde je to Student
).
Pretpostavimo da imamo tabelu KORISNIK
čiji se složeni primarni ključ sastoji od kolona KORISNICKO_IME
i ID_ODELJENJA
, kao i da imamo tabelu ODELJENJE
koja sadrži primarni ključ ID_ODELJENJE
. Takođe, postoji ograničenje stranog ključa FK1
koji postavlja ograničenje na kolonu ID_ODELJENJA
iz tabele KORISNIK
u odnosu na kolonu ID_ODELJENJE
iz tabele ODELJENJE
i koji predstavlja odnos više-ka-jedan (jedno odeljenje može imati više korisnika, a jedan korisnik može pripadati samo jednom odeljenju). Ova situacija je ilustrovana na narednoj slici.
Problem koji se ovde javlja jeste da postoji strani ključ ka vrednosti iz druge tabele (ODELJENJE
) kao deo složenog primarnog ključa u prvoj tabeli (KORISNIK
). Ovo je očigledno problem dupliciranih podataka i prilikom trajnog čuvanja podataka, Hibernate mora da zna koju će vrednost smatrati za “ispravnu”. Klasu koja sadrži ovakvu specijalnu situaciju potrebno je anotirani anotacijom @MapsId
. Njena vrednost je naziv polja u složenom primarnom ključu te tabele koja čuva informaciju o stranom ključu ka drugoj tabeli. Pogledajmo kako bismo razrešili ovaj problem na prethodno opisanoj situaciji:
public class KorisnikId implements Serializable {
protected String korisnicko_ime;
protected Long id_odeljenja;
// ...
}
@Entity
public class Korisnik {
@EmbeddedId
protected KorisnikId id;
@ManyToOne
@MapsId("id_odeljenja")
protected Odeljenje odeljenje;
public Korisnik(KorisnikId id) {
this.id = id;
}
// ...
}
Anotacija @MapsId
kaže Hibernate razvojnom okruženju da ignoriše vrednost polja KorisnikId.id_odeljenja
prilikom čuvanja instance klase Korisnik
. Hibernate će umesto toga koristiti identifikator iz klase Odeljenje
dodeljen polju Korisnik.odeljenje
prilikom čuvanja sloga u tabeli KORISNIK
.
Alternativni pristup ovom preslikavanju jeste da se umesto anotacije @MapsId
koristi anotacija @JoinColumn
kojom se navodi nazivi kolona koje učestvuju u formiranju stranog ključa. U toj situaciji je neophodno navesti i vrednost false
za opcije insertable
i updatable
ove anotacije sa narednim značenjem:
Svojstvo insertable
uzima podrazumevano vrednost true
, a ukoliko se postavi na false
, onda anotirano polje se neće nalaziti u INSERT
naredbama generisanim od strane Hibernate alata (drugim rečima, neće biti trajno upisano u bazu podataka).
Svojstvo updatable
uzima podrazumevano vrednost true
, a ukoliko se postavi na false
, onda anotirano polje se neće nalaziti u UPDATE
naredbama generisanim od strane Hibernate alata (drugim rečima, neće biti promenjeno jednom kada se trajno upiše u bazu podataka).
Korišćenje ovog pristupa dovodi do narednog koda:
public class KorisnikId implements Serializable {
protected String korisnicko_ime;
protected Long id_odeljenja;
// ...
}
@Entity
public class Korisnik {
@EmbeddedId
protected KorisnikId id;
@ManyToOne
@JoinColumn(name="id_odeljenja",
referencedColumnName = "id_odeljenja",
insertable = false,
updatable = false))
protected Odeljenje odeljenje;
public Korisnik(KorisnikId id) {
this.id = id;
}
// ...
}
Jednostavnije rečeno, ne želimo da menjamo podatke u tabeli ODELJENJE
menjanjem polja id_odeljenja
iz klase KorisnikId
(čime ga obeležavamo da je samo za čitanje (engl. readonly)).
Napomenimo još i da, ukoliko tabela ima više stranih ključeva, onda je korišćenje anotacije @JoinColumn
neophodno da bi se navelo koje kolone učestvuju u formiranju ograničenja stranog ključa. Naravno, ukoliko se @JoinColumn
koristi u kombinaciji sa @MapsId
, onda nije neophodno navesti opcije insertable
i updatable
.
Ova anotacija se još koristi i kada se zbog bidirekcione veze duplicira strani ključ. U tom slučaju se navode sve četiri opcije.
Zadatak 11.2: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate za sve studente, koji su rođeni u mestu koje se unosi sa standardnog ulaza i upisali su studijski program obima epsb koji se unosi sa standardnog ulaza, ispisuje ime, prezime i naziv studijskog programa.
Rešenje: Tabelu DOSIJE
do sad nismo koristili pa je potrebno da kreiramo odgovarajuću klasu i definišemo preslikavanje u tabelu DOSIJE
, a onda i vezu sa klasom StudijskiProgram
. Vezu definišemo koa više-ka-jedan u klasi Student
jer više studenata mogu upisati isti studijski program. Pošto tabela DOSIJE
sadrži polje IDPROGRAMA
, a dodavanjem ove veze dupliciramo podatke (polje StudijskiProgram studijskiProgram
takođe sadrži informaciju o identifikatoru) potrebno je dodati i anotaciju @JoinColumn
za polje studijskiProgram
:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_2/Student.java
:
package zadatak_11_2;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "DA.DOSIJE")
public class Student {
// Primarni kljuc
@Id
private Integer indeks;
// Kolone od znacaja
@Column(name = "IME", nullable = false)
private String ime;
@Column(name = "PREZIME", nullable = false)
private String prezime;
@Column(name = "MESTORODJENJA")
private String mesto;
// Kreiramo dvosmernu asocijativnu vezu izmedju klasa StudijskiProgram i Student.
// Posto tabela Dosije sadrzi strani kljuc idprograma koji referise na StudijskiProgram
// potrebno je da se u klasi StudijskiProgram definise vrednost za opciju mappedBy.
// Dodatno, zbog stranog kljuca moramo dodati anotaciju @JoinColumn kako
// bismo ogranicili koriscenje ove reference na citanje.
@ManyToOne
@JoinColumn(name="IDPROGRAMA", referencedColumnName="ID", insertable=false, updatable=false)
private StudijskiProgram studijskiProgram;
// Get/set metodi
public Integer getIndeks() {
return indeks;
}
public void setIndeks(Integer indeks) {
this.indeks = indeks;
}
public String getIme() {
return ime;
}
public void setIme(String ime) {
this.ime = ime;
}
public String getPrezime() {
return prezime;
}
public void setPrezime(String prezime) {
this.prezime = prezime;
}
public StudijskiProgram getStudijskiProgram() {
return studijskiProgram;
}
public void setStudijskiProgram(StudijskiProgram studijskiProgram) {
this.studijskiProgram = studijskiProgram;
}
}
Ova veza je definisana u klasi StudijskiProgram
kao jedan-ka-više, s obzirom da jedan studijski program može sadržati više studenata:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_2/StudijskiProgram.java
:
package zadatak_11_2;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@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;
// Kreiramo dvosmernu asocijativnu vezu izmedju klasa StudijskiProgram i Student.
// Posto tabela Dosije sadrzi strani kljuc id_smera koji referise na StudijskiProgram
// potrebno je da se u klasi StudijskiProgram postavljamo opciju mappedBy na naziv polja
// tipa StudijskiProgram u klasi Student.
@OneToMany(mappedBy="studijskiProgram")
private List<Student> studenti;
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;
}
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;
}
public List<Student> getStudenti() {
return studenti;
}
public void setStudenti(List<Student> studenti) {
this.studenti = studenti;
}
@Override
public String toString() {
return "Studijski program [id=" + id + ", Oznaka=" + Oznaka + ", Naziv=" + Naziv
+ ", Espb=" + Espb + ", Nivo=" + Nivo + ", Zvanje=" + Zvanje + ", Opis=" + Opis + "]";
}
}
Zahtev je implementiran u metodu readStudenti()
klase Main.java
:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_2/Main.java
:
package zadatak_11_2;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
class Main {
public static void main(String[] args) {
System.out.println("Pocetak rada...\n");
readStudenti();
System.out.println("Zavrsetak rada.\n");
HibernateUtil.getSessionFactory().close();
}
private static void readStudenti() {
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction TR = null;
try (Scanner ulaz = new Scanner(System.in)) {
TR = session.beginTransaction();
System.out.println("Unesite mesto rodjenja:");
String mesto = ulaz.next();
System.out.println("Unesite broj ESPB bodova:");
Integer espb = ulaz.nextInt();
// HQL upit za izdvajanje podataka o studentima
// sa odredjenim mestom rodjenja i brojem bodova.
// Kao sto se u FROM klauzi navodi naziv KLASE, a ne TABELE,
// tako se u WHERE klauzi navode ATRIBUTI, a ne KOLONE.
String hql = "SELECT s.ime, s.prezime, sp.Naziv " +
"FROM Student s INNER JOIN s.studijskiProgram AS sp " +
"WHERE s.mesto = :mesto AND " +
"sp.Espb = :espb";
// Pripremanje upita
// Zbog projekcije rezultat ce biti tipa Object[]
Query<Object[]> upit = session.createQuery(hql, Object[].class);
// Postavljanje vrednosti za imenovane parametarske oznake
upit.setParameter("mesto", mesto);
upit.setParameter("espb", espb);
// Izvrsavanje upita i listanje podataka
List<Object[]> studenti = upit.list();
// Ispis rezultata
if (studenti.size() != 0) {
for (Object[] student : studenti) {
System.out.println(Arrays.toString(student));
}
} else {
System.out.println("Nema rezultata za zadate vrednosti!");
}
TR.commit();
} catch (Exception e) {
System.err.println("Postoji problem sa ispisivanjem podataka o studentima! Ponistavanje transakcije!");
e.printStackTrace();
if (TR != null) {
TR.rollback();
}
} finally {
session.close();
}
}
}
Slična situacija se javlja kada je potrebno mapirati asocijativnu vezu složenog stranog ključa. Pogledajmo DDL za naredne dve tabele:
CREATE TABLE KORISNIK (
KORISNICKO_IME INTEGER NOT NULL,
ID_ODELJENJA INTEGER NOT NULL,
PRIMARY KEY (KORISNICKO_IME, ID_ODELJENJA)
)
CREATE TABLE ARTIKAL (
ID INTEGER NOT NULL,
KORISNICKO_IME_PRODAVCA INTEGER NOT NULL,
ID_ODELJENJA_PRODAVCA INTEGER NOT NULL,
PRIMARY KEY (ID),
FOREIGN KEY (
KORISNICKO_IME_PRODAVCA,
ID_ODELJENJA_PRODAVCA)
REFERENCES KORISNIK
)
Zgodno je ilustrovati ovu situaciju crtežom:
U ovoj shemi, prodavac artikla je predstavljen složenim stranim ključem u tabeli ARTIKAL
. Ova veza predstavlja odnos jedan-ka-više (jedan korisnik je vlasnik više artikala, a jedan artikal pripada samo jednom korisniku). U domenskom modelu, ovo preslikavanje se može rešiti na sledeći način:
///////////////////////////////////
// BEGIN Korisnik.java
@Embeddable
public class KorisnikId implements Serializable {
Integer korisnicko_ime;
Integer id_odeljenja;
// ...
}
@Entity
public class Korisnik {
@Id
KorisnikId id;
// ...
}
// END Korisnik.java
///////////////////////////////////
// BEGIN Artikal.java
@Entity
public class Artikal {
@Id
Integer id_artikla;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "korisnicko_ime_prodavca",
referencedColumnName = "korisnicko_ime"),
@JoinColumn(name = "id_odeljenja_prodavca",
referencedColumnName = "id_odeljenja")
})
Korisnik prodavac;
// ...
}
// END Artikal.java
Anotacija @JoinColumns
predstavlja listu kolona u složenom stranom ključu koje učestvuju u ovoj asocijativnoj vezi. Svaka kolona se ostvaruje navođenjem jedne @JoinColumn
anotacije. Napomenimo da ne bi trebalo zaboraviti postavljanje vrednosti opcije referencedColumnName
u @JoinColumn
anotacijama, čime se ostvaruje veza između izvora i cilja stranog ključa. Hibernate nas, nažalost, neće upozoriti ukoliko ovo zaboravimo, a može dovesti do problema usled pogrešnog redosleda kolona u generisanoj shemi.
Najkomplikovaniji slučaj se može pojaviti ukoliko se izmeni shema tabele ARTIKAL
tako da kolone stranog ključa učestvuju u složenom primarnom ključu, kao u narednoj shemi:
CREATE TABLE ARTIKAL (
ID INTEGER NOT NULL,
KORISNICKO_IME_PRODAVCA INTEGER NOT NULL,
ID_ODELJENJA_PRODAVCA INTEGER NOT NULL,
PRIMARY KEY (ID,
KORISNICKO_IME_PRODAVCA,
ID_ODELJENJA_PRODAVCA),
FOREIGN KEY (
KORISNICKO_IME_PRODAVCA,
ID_ODELJENJA_PRODAVCA)
REFERENCES KORISNIK
)
Naredna slika nam može pomoći u analiziranju ove situacije:
Zapravo, ako malo bolje pogledamo, možemo primetiti da ovaj slučaj nastaje kombinacijom prethodna dva slučaja — sa jedne strane imamo situaciju da strani ključ predstavlja deo složenog primarnog ključa, a takođe imamo situaciju da je strani ključ složen. Dakle, sve što je potrebno da uradimo jeste da kombinujemo rešenja oba problema, tj. da iskoristimo anotacije @MapsId
i @JoinColumns
, zajedno sa odgovarajućom anotacijom za tip veze, kao u narednom kodu:
public class ArtikalId implements Serializable {
Integer id;
KorisnikId id_korisnika;
// ...
}
@Entity
public class Artikal {
@EmbeddedId
ArtikalId id_artikla;
// Naredne anotacije se vezuju za polje `Korisnik prodavac` ispod.
// Odvojene su radi lakseg analiziranja i komentarisanja.
// 1. Vezujemo `ArtikalId.id_korisnika` za identifikator klase `Korisnik`
@MapsId("id_korisnika")
// 2. Definisemo po cemu se vrsi spajanje za slozeni strani kljuc
// koji postoji izmedju klasa `Artikal` i `Korisnik`
@JoinColumns({
@JoinColumn(name = "korisnicko_ime_prodavca",
referencedColumnName = "korisnicko_ime"),
@JoinColumn(name = "id_odeljenja_prodavca",
referencedColumnName = "id_odeljenja")
})
// 3. Definisemo tip veze izmedju `Artikal` i `Korisnik`
@ManyToOne
Korisnik prodavac;
// ...
}
Zadatak 11.3: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate ispisuje nazive svih studijskih programa. Nakon svakog naziva studijskog programa, ispisuju se indeks, ime, prezime i prosek svih studenata na tom studijskom programu.
Ovaj zahtev je do sada najsloženiji jer uvodi dosta novih klasa i preslikavanja. Za početak, s obzirom da su nam potrebni podaci iz tabele ISPIT
koju do sada nismo koristili, potrebno je da kreiramo klasu Ispit
, i definišemo preslikavanja između njih i odgovarajućih tabela, kao i između samih klasa. Za početak, dajmo njenu celu definiciju, pa ćemo obratiti pažnju na neke detalje:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_3/Ispit.java
:
package zadatak_11_3;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import javax.persistence.MapsId;
import javax.persistence.Table;
@Entity
@Table(name = "DA.ISPIT")
public class Ispit {
// Primarni kljuc
// U ovom slucaju cemo koristiti drugi pristup kreiranju primarnog kljuca,
// tj. koriscenjem @EmbeddedId anotacije.
// Pogledati skriptu za vise detalja.
@Id
private IspitId idIspita;
// Ostale kolone
@Column
private Integer ocena;
@Column
private String status;
// Resavanje asocijativnih veza izmedju klasa
// Kreiranje veze izmedju Ispit i Student
// Posto tabela Ispit sadrzi vise od jednog stranog kljuca
// moramo da navedemo anotaciju @JoinColumn i da definisemo
// vrednosti za sve opcije ili kombinaciju @MapsId i @JoinColumn
// s tim da onda mozemo izostaviti opcije insertable i updatable
@ManyToOne
@MapsId("indeks")
@JoinColumn(name="INDEKS", referencedColumnName="INDEKS")
private Student student;
// Problem je sto se u primarnom kljucu javlja "potkljuc" IspitniRokId,
// koji ima kolone "godina" i "oznaka".
// A u primarnom kljucu tabele "ispit",
// te kolone se nazivaju "godina_roka" i "oznaka_roka", redom.
// Tako da je potrebno to resiti.
// IspitniRok se spaja sa Ispit preko IspitniRokId,
// koji je u IspitId stavljen kao polje "id_roka"
// Zato koristimo @MapsId anotaciju, koja prihvata naziv polja
// u @EmbeddedId klasi IspitniRokId
@MapsId("idRoka")
// Sada specifikujemo kako se tacno vrsi "spajanje",
// tj. kako se formira ogranicenje stranog kljuca.
// Za to koristimo JoinColums, posto se spajanje vrsi po dve kolone,
// odnosno koriscenjem dva polja iz klase
@JoinColumns({ @JoinColumn(name = "skgodina", referencedColumnName = "skgodina"),
@JoinColumn(name = "oznakaroka", referencedColumnName = "oznakaroka") })
// Na kraju, specifikujemo tip veze
@ManyToOne
private IspitniRok ispitniRok;
// Get/set metodi
public IspitId getIdIspita() {
return idIspita;
}
public void setId_ispita(IspitId idIspita) {
this.idIspita = idIspita;
}
public Integer getOcena() {
return ocena;
}
public void setOcena(Integer ocena) {
this.ocena = ocena;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
}
Prvo što vidimo jeste da klasa Ispit sadrži složeni ključ IspitId
. Dajmo i definiciju ove klase:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_3/IspitId.java
:
package zadatak_11_3;
import java.io.Serializable;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Embeddable;
// Klasa predstavlja slozeni kljuc za Ispit.
// Anotiralmo je anotacijom @Embeddable
@Embeddable
public class IspitId implements Serializable {
private static final long serialVersionUID = 1L;
// Slozeni kljuc tabele "ispit" sadrzi primarni kljuc tabele "ispitni_rok",
// kao i kolone "indeks" i "id_predmeta"
private IspitniRokId idRoka;
@Column(name = "INDEKS")
private Integer indeks;
@Column(name = "IDPREDMETA")
private Integer idPredmeta;
// Podrazumevani konstruktor za Serializable
public IspitId() {
}
public IspitId(IspitniRokId idRoka, Integer indeks, Integer idPredmeta) {
this.idRoka = idRoka;
this.indeks = indeks;
this.idPredmeta = idPredmeta;
}
// Get/set metodi
public IspitniRokId getIdRoka() {
return idRoka;
}
public void setId_roka(IspitniRokId idRoka) {
this.idRoka = idRoka;
}
public Integer getIndeks() {
return indeks;
}
public void setIndeks(Integer indeks) {
this.indeks = indeks;
}
public Integer getIdPredmeta() {
return idPredmeta;
}
public void setId_predmeta(Integer idPredmeta) {
this.idPredmeta = idPredmeta;
}
// Prevazilazenje metoda za odredjivanje jednakosti kljuceva
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof IspitId)) {
return false;
}
IspitId other = (IspitId) o;
return Objects.equals(this.getIdRoka(), other.getIdRoka())
&& Objects.equals(this.getIndeks(), other.getIndeks())
&& Objects.equals(this.getIdPredmeta(), other.getIdPredmeta());
}
@Override
public int hashCode() {
return Objects.hash(this.getIdRoka(), this.getIndeks(), this.getIdPredmeta());
}
}
Ono što primećujemo jeste da u okviru ovog primarnog ključa se nalazi drugi primarni ključ i to za klasu IspitniRok
, odnosno, klasa IspitniRokId
. Ovde vidimo da možemo klase koje predstavljaju primarne ključeve nekih klasa (kao što je IspitniRokId
) koristiti u klasama koje predstavljaju primarne ključeve drugih klasa (kao što je IspitId
), ali samo ako su oni kreirane prvim pristupom kreiranja stranih ključeva — pomoću anotacija @Id
i @Embeddable
. Sad nam je jasno zašto smo upravo taj pristup koristili za IspitniRokId
. Štaviše, preslikavanje na ovaj način je obavezno ukoliko želimo da sačuvamo sva ograničenja u poslovnom domenu. Alternativni pristup, u kojem bi se kolone SKGODINA
i OZNAKAROKA
iz tabele ISPIT
preslikavala pomoću dva polja u klasi IspitId
, ne bi očuvao organičenje da obe kolone predstavljaju složeni strani ključ ka tabeli ISPITNIROK
. Zbog toga, klasa IspitId
mora imati polje tipa IspitniRokId
kao u kodu iznad.
Vratimo se nazad na klasu Ispit
. Nakon deklarisanja polja ocena
i status
, potrebno je rešiti asocijativne veze ka klasama Student
i IspitniRok
.
Veza ka klasi Student
je dovoljno jednostavna — jedan ispit pripada tačno jednom studentu, dok jedan student može imati više ispita, dakle, veza je više-ka-jedan, odnosno, koristimo anotaciju @ManyToOne
(ovaj zaključak je donesen iz ugla klase Ispit
). Dodatno, potrebno je da postavimo anotaciju @MapsId
kako bismo ignorisali vrednost iz primarnog ključa IspitId
pri čuvanju ovog podatka — umesto njega, biće korišćena vrednost polja indeks
iz klase Student
. Takođe, s obzirom da ova tabela ima više stranih ključeva, potrebno je da koristimo anotaciju @JoinColumn
da specifikujemo precizno koje kolone učestvuju u ograničenju ovog stranog ključa. Alternativno, možemo ga postaviti samo za čitanje, kao što smo diskutovali u podsekciji 11.2.4.
Druga veza je prema klasi IspitniRok
. Ukoliko pogledamo kako je sve do sada postavljeno, videćemo da je ovo instanca problema prikazana u podsekciji 11.2.6. Da se podsetimo, problem je u tome što se u tabeli ISPIT
nalazi složeni strani ključ ka tabeli ISPITNIROK
, pri čemu kolone složenog stranog ključa učestvuju kao deo primarnog ključa. Rešenje je u kreiranju polja klase IspitniRok
i odgovarajućim anotiranjem:
Koristimo anotaciju @MapsId
da ignorišemo kolone zadate poljem idRoka
iz primarnog ključa klase IspitId
. Umesto toga, koristićemo polja iz objekta klase IspitniRok
koji anotiramo.
Koristimo anotaciju @JoinColumns
da definišemo po kojim kolonama se vrši odgovarajuće preslikavanje za složeni strani ključ. Vrednost ove anotacije je lista @JoinColumn
anotacija, za svaku kolonu u složenom stranom ključu.
Koristimo anotaciju @ManyToOne
da definišemo tip preslikavanja. S obzirom da više ispita pripada jednom ispitnom roku, a da jedan ispitni rok sadrži više ispita, veza je više-ka-jedan (opet, posmatrano iz ugla klase Ispit
koju trenutno analiziramo).
Naravno, ovo podrazumeva izmenu i u klasi IspitniRok
, gde sada definišemo vezu između ispitnih rokova i ispita — jedan ispitni rok ima više ispita, te koristimo anotaciju @OneToMany
za polje tipa java.util.List<Ispit>
. S obzirom da je ova veza dvosmerna, moramo reći koja klasa je odgovorna za održavanje takve veze (tj. za održavanje referencijalnog integriteta stranim ključem). S obzirom da klasa Ispit
održava tu vezu, onda u klasi IspitniRok
postavljamo opciju mappedBy
, kao što je i urađeno u klasi IspitniRok
:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_3/IspitniRok.java
:
package zadatak_11_3;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "DA.ISPITNIROK")
class IspitniRok {
// Primarni kljuc
@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;
// Kreiranje veze izmedju IspitniRok i Ispit.
// S obzirom da je veza izmedju ovih klasa dvosmerna,
// onda samo jedna klasa moze biti odgovorna za vezu,
// sto se postize navodjenjem opcije mappedBy
@OneToMany(mappedBy = "ispitniRok")
List<Ispit> ispiti = new ArrayList<>();
// 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;
}
public List<Ispit> getIspiti() {
return ispiti;
}
public void setIspiti(List<Ispit> ispiti) {
this.ispiti = ispiti;
}
@Override
public String toString() {
return "IspitniRok [" + id + ", Naziv=" + Naziv + ", Pocetak=" + Pocetak + ", Kraj=" + Kraj + "]";
}
}
Dopunimo i klasu Student
:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_3/Student.java
:
package zadatak_11_3;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "DA.DOSIJE")
public class Student {
// Primarni kljuc
@Id
private Integer indeks;
// Kolone od znacaja
@Column(name = "IME", nullable = false)
private String ime;
@Column(name = "PREZIME", nullable = false)
private String prezime;
@Column(name = "MESTORODJENJA")
private String mesto;
@ManyToOne
@JoinColumn(name="IDPROGRAMA", referencedColumnName="ID", insertable=false, updatable=false)
private StudijskiProgram studijskiProgram;
// Da bismo izracunali prosek polozenih predmeta za studenta,
// potrebno nam je da dohvatimo informacije o njegovim ispitima.
// Zbog toga definisemo listu ispita,
// i dekorisemo je anotacijom veze izmedju tabela "dosije" i "ispit".
// S obzirom da tabela "ispit" treba da odrzava strani kljuc,
// onda koristimo mappedBy da bismo specifikovali
// da je ta tabela odgovorna za bidirekcionu vezu.
@OneToMany(mappedBy = "student")
private List<Ispit> ispiti = new ArrayList<>();
// Get/set metodi
public Integer getIndeks() {
return indeks;
}
public void setIndeks(Integer indeks) {
this.indeks = indeks;
}
public String getIme() {
return ime;
}
public void setIme(String ime) {
this.ime = ime;
}
public String getPrezime() {
return prezime;
}
public void setPrezime(String prezime) {
this.prezime = prezime;
}
public StudijskiProgram getStudijskiProgram() {
return studijskiProgram;
}
public void setStudijskiProgram(StudijskiProgram studijskiProgram) {
this.studijskiProgram = studijskiProgram;
}
public List<Ispit> getIspiti() {
return ispiti;
}
public void setIspiti(List<Ispit> ispiti) {
this.ispiti = ispiti;
}
// Metod koji racuna prosek studenta
public double prosek() {
double ukupno = 0;
int broj_polozenih = 0;
// Pozivom getIspiti() vrsi se citanje podataka o ispitima
// iz baze i njihovo smestanje u listu.
List<Ispit> ispiti = this.getIspiti();
for (Ispit ispit : ispiti) {
// Izdvajamo informacije samo o polozenim ispitima
if (ispit.getStatus().equalsIgnoreCase("o") && ispit.getOcena() > 5) {
ukupno += ispit.getOcena();
broj_polozenih++;
}
}
if (broj_polozenih == 0)
return 0;
return ukupno / broj_polozenih;
}
}
S obzirom da je potrebno da izračunamo prosek položenih predmeta za datog studenta, definišemo metod prosek()
koji koristi listu svih ispita studenta. Za listu ispita smo morali da napravimo odgovarajuće preslikavanje između klasa Student
i Ispit
. S obzirom da jedan student može imati više ispita, koristimo vezu jedan-ka-više. Dodatno, moramo da navedemo po čemu se vrši “spajanje” između klasa, i to je u ovom slučaju, po koloni indeks
.
Konačno, da bismo završili implementaciju, potrebno je da registrujemo anotirane klase u klasi HibernateUtil
i kreiramo metod u klasi Main
koji testira implementirane klase i metode:
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_3/HibernateUtil.java
:
package zadatak_11_3;
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)
.addAnnotatedClass(Student.class)
.addAnnotatedClass(Ispit.class)
.buildMetadata().buildSessionFactory();
} catch (Throwable e) {
System.err.println("Session factory error");
e.printStackTrace();
System.exit(1);
}
}
static SessionFactory getSessionFactory() {
return sessionFactory;
}
}
Datoteka: vezbe/primeri/poglavlje_11/src/zadatak_11_3/Main.java
:
package zadatak_11_3;
import java.util.Collections;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
public class Main {
public static void main(String[] args) {
System.out.println("Pocetak rada...\n");
readStudijskiProgramoviIStudenti();
System.out.println("Zavrsetak rada.\n");
HibernateUtil.getSessionFactory().close();
}
public static void readStudijskiProgramoviIStudenti() {
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction TR = null;
try {
TR = session.beginTransaction();
// Kreiramo HQL upit koji izdvaja sve studijske programe
String hql = "from StudijskiProgram";
Query<StudijskiProgram> upit =
session.createQuery(hql, StudijskiProgram.class);
// Dohvatamo rezultat u listu studijskiProgrami,
// i proveravamo da li su svi elementi objekti klase StudijskiProgram
List<StudijskiProgram> studijskiProgrami = Collections.checkedList(upit.list(), StudijskiProgram.class);
// Kreiramo HQL upit koji izdvaja sve studente
// na studijskom programu ciji ce identifikator biti postavljen
// na mesto imenovane parametarske oznake "id".
// S obzirom da ovo radimo za svaki studijski program,
// dohvatanje rezultata moramo da ugnezdimo unutar petlje
// koja prolazi studijskim programima.
// Drugim recima, koristimo ugnezdjene kursore.
hql = "from Student where idprograma = :id";
Query<Student> upit2 =
session.createQuery(hql, Student.class);
// Spoljasnja petlja: ispisivanje studijskih programa
for (StudijskiProgram studijskiProgram : studijskiProgrami) {
String naziv = studijskiProgram.getNaziv().trim();
int id = studijskiProgram.getId();
System.out.println("\n\nSTUDISJKI PROGRAM: " + naziv);
// Postavljanje vrednosti parametarske oznake za unutrasnju
// petlju
upit2.setParameter("id", id);
upit2.setMaxResults(5); // Da dohvatimo najvise 5 studenata
// Dohvatanje studenata za tekuci studijski program
List<Student> studenti = Collections.checkedList(upit2.list(), Student.class);
// Unutrasnja petlja: ispisivanje studenata na studijskom programu
for (Student student : studenti) {
int indeks = student.getIndeks();
String ime = student.getIme().trim();
String prezime = student.getPrezime().trim();
double prosek = student.prosek();
System.out.println("Student: " + indeks + ", " + ime + ", " + prezime + ", " + prosek);
}
}
TR.commit();
} catch (Exception e) {
System.err.println("Postoji problem sa izlistavanjem studenata na studijskim programima! Ponistavanje transakcije!");
e.printStackTrace();
if (TR != null) {
TR.rollback();
}
} finally {
session.close();
}
// Za vezbu: razmisliti kako je moguce uraditi ovaj zadatak
// bez koriscenja ugnezdjenih kursora
// (tj. bez koriscenja upita koji pronalazi studente za zadati id studijskog programa)
}
}
Zadatak 11.4: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate za svaki studijski program na osnovnim akademskim studijama izdvaja listu obaveznih predmeta. Prvo ispisati sve podatke o studijskom programu, a zatim oznaku, naziv i broj ESPB bodova svih obaveznih predmeta za taj studijski program.
Zadatak 11.5: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate za svaki predmet ispisuje oznaku i naziv, a zatim i spisak studenata koji su upisali taj predmet u školskoj godini koja se unosi sa standardnog ulaza. Rezultat urediti prema prezimenu i imenu studenta rastuće.
Zadatak 11.6: Napisati Java aplikaciju koja korišćenjem biblioteke Hibernate za svaki studijski program koji je u poslednjih 10 godina upisalo više od 30 studenata izdvajaju podaci o najmlađem studentu koji je upisao taj studijski program. Za najmlađeg studenta po studijskom programu izdvojiti naziv studijskog programa koji studira, indeks, ime i prezime studenta, datum upisa na fakultet, broj položenih predmeta i prosečnu ocenu zaokruženu na 2 decimale. Rezultat urediti prema nazivu studijskog programa.