Σε αυτό το παράδειγμα ας υποθέσουμε ότι έχουμε Projects που το κάθε ένα έχει πολλούς μηχανικούς. Πρόκειται για μία σχέση One to Many μεταξύ της οντότητας Project και της οντότητας Engineer. Παρακάτω θα δούμε πως μπορούμε εύκολα με Annotations να εκφράσουμε αυτή την σχέση σε Hibernate και Java.
Η οντότητα Project
[code language=”java” highlight=”29,30″]
package com.gr.zenika.hibernateexamples.domain;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name="projects")
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Integer id;
@Column(name="name")
String name;
@Column(name="version")
String version;
@OneToMany(mappedBy="project", cascade = { CascadeType.ALL })
private List<Engineer> engineers;
public Project() {
}
public Project(Integer id, String name, String version,
List<Engineer> engineers) {
super();
this.id = id;
this.name = name;
this.version = version;
this.engineers = engineers;
}
//Getter and Setter methods
}
[/code]
Χρησιμοποιούμε το Annotation @OneToMany για να δημιουργήσουμε την σχέση μεταξύ των οντοτήτων στο Hibernate. Το mappedBy υποδηλώνει τον ιδιοκτήτη της σχέσης και κάνει αναφορά στο πεδίο Project project που θα δείτε στην οντότητα Engineer. To cascade το χρησιμοποιούμε έτσι ώστε όταν αποθηκεύσουμε μία οντότητα Project στην βάση, η οποία θα έχει γεμάτη την λίστα με τους μηχανικούς, να αποθηκευτούν αυτόματα και όλοι οι μηχανικοί χωρίς να χρειάζεται να τους αποθηκεύσουμε χειροκίνητα. df
Η οντότητα Engineer
[code language=”java” highlight=”30,31,32″]
package com.gr.zenika.hibernateexamples.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name="engineers")
public class Engineer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Integer id;
@Column(name="name")
private String name;
@Column(name="surname")
private String surname;
@Column(name="favorite_language")
private String favoriteLanguage;
@ManyToOne
@JoinColumn(name="project_id")
private Project project;
public Engineer() {
}
public Engineer(Integer id, String name, String surname,
String favoriteLanguage, Project project) {
super();
this.id = id;
this.name = name;
this.surname = surname;
this.favoriteLanguage = favoriteLanguage;
this.project = project;
}
//Getter and Setter methods
}
[/code]
Στην οντότητα Engineer χρησιμοποιούμε το αντίστροφο annotation @ManyToOne σε ένα πεδίο project τύπου Project για να υποδηλώσουμε ότι πολλοί Engineer ανήκουν σε ένα Project. Το σημαντικό εδώ είναι το annotation @JoinColumn με το οποίο το hibernate θα καταλάβει αυτόματα με ποια κολώνα του πίνακα θα κάνει το Join. Ουσιαστικά χρησιμοποιώντας αυτά τα Annotation στις οντότητές μας, θα δούμε ότι αν επιτρέψουμε στο Hibernate να δημιουργήσει την βάση αυτόματα, τότε θα δημιουργήσει ένα Foreign Key με όνομα project_id στον πίνακα engineers που θα κάνει αναφορά στο id που έχει ο πίνακας project, όπως θα περιμέναμε. Αυτό που πρέπει να προσέξουμε εδώ είναι ότι δεν μπορούμε να έχουμε κολώνα @Column(name=”project_id”) private Integer projectId; στην οντότητα Engineer καθώς το Hibernate θα φτιάξει το Foreign key με αυτό το όνομα χάρη στο annotation @JoinColumn(name=”project_id”).
ProjectService
Πρόκειται για το Service που έχει τις απαραίτητες μεθόδους για αποθήκευση και αναζήτηση στην βάση.
[code language=”java” highlight=”32,36″]
import java.util.List;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.gr.zenika.hibernateexamples.domain.Project;
@Service
public class ProjectService {
private static final Logger logger = LoggerFactory.getLogger(ProjectService.class);
@Autowired
private SessionFactory sessionFactory;
public void save(Project project) {
logger.info("{}", project);
Session s = sessionFactory.openSession();
s.save(project);
s.flush();
s.close();
}
@SuppressWarnings("unchecked")
public List<Project> findByName(String name) {
Session s = sessionFactory.openSession();
String hql = "select distinct p from Project p left join fetch p.engineers e where p.name=:name order by p.version";
Query query = s.createQuery(hql);
query.setParameter("name", name);
List<Project> list = (List<Project>) query.list();
s.close();
return list;
}
}
[/code]
Στην γραμμή 32 βλέπουμε το HQL query. Σημειώνουμε τα εξής:
- Στην HQL to left join είναι συντόμευση για το left outer join.
- Στην HQL χρησιμοποιούμε οντότητες δηλαδή κλάσεις της Java και όχι πίνακες. To left join p.engineers ουσιαστικά λέει στο hibernate να κοιτάξει στο πεδίο engineers της κλάσης Project και να κάνει αυτόματα το join. Το με ποια κολώνα θα γίνει το join αυτό το καταλαβαίνει από το annotation @JoinColumn(name=”project_id”).
- Σε SQL αυτό θα μπορούσε να μεταφραστεί σε:[code language=”sql”] SELECT p.* FROM projects p LEFT OUTER JOIN engineers e ON p.id=e.project_id WHERE p.name=’Project name’ order by p.version[/code]
- Χρησιμοποιούμε το select distinct p καθώς όπως μπορούμε να μαντέψουμε από το SQL query, αν υποθέσουμε ότι έχουμε ένα Project A με δύο μηχανικούς και ένα project Β με έναν μηχανικό, το παραπάνω SQL query θα επέστρεφε 3 γραμμές. Αντίστοιχα και στο HQL query θα μας γυρίσει 3 γραμμές οι οποίες θα γίνουν map πάνω στις οντότητες Project και Engineer. Αυτό θα μας δημιουργήσει πρόβλημα εφόσον θα πάρουμε 3 οντότητες Project ενώ ουσιαστικά τα Project είναι δύο (Α και Β). Με select distinct θα πάρουμε σωστά μονάχα τα δύο Project Α και Β.
- Το τελευταίο και σημαντικό είναι το left join fetch. Χρησιμοποιούμε το fetch καθώς από προεπιλογή στο Hibernate οι σχέσεις OneToMany και ManyToMany φορτώνονται με Lazy τρόπο. Αυτό σημαίνει ότι όταν ζητήσουμε όλα τα Project, η λίστα με τους engineers δεν θα είναι γεμάτη εφόσον αυτό θα μπορούσε να δημιουργήσει πρόβλημα αν ο αριθμός των μηχανικών για κάθε Project ήταν εξαιρετικά μεγάλος. Χρησιμοποιώντας το fetch αναγκάζουμε το hibernate να τραβήξει την πληροφορία των Engineer και να γεμίσει την λίστα που έχει η οντότητα Project.
- Αν δεν χρησιμοποιούσαμε το fetch και προσπαθούσαμε να καλέσουμε την getEngineers() τότε το Hibernate θα έτρεχε ένα απλό select query στην βάση εκείνη την στιγμή για να φορτώσει τα δεδομένα των μηχανικών. Το πρόβλημα εδώ είναι ότι στην γραμμή 36 κλείνουμε το Session. Αν καλούσαμε την getEngineers() ενώ το Session είναι κλειστό θα είχαμε εξαίρεση LazyIntializationException. Με το fetch γλιτώνουμε αυτό το πρόβλημα αλλά θα πρέπει να προσέξουμε πότε χρησιμοποιούμε το παραπάνω HQL query καθώς προφανώς σε περίπτωση μεγάλου αριθμού μηχανικών δεν θα θέλαμε να τους φορτώνουμε όλους κάθε φορά που ζητάμε τα projects. Σε μία τέτοια περίπτωση θα έπρεπε να χρησιμοποιήσουμε διαφορετικό query για την αναζήτηση των project και το παραπάνω HQL query για την εύρεση των Project μαζί με τους μηχανικούς.