ORM (Object Relational Mapping) és una tècnica per compartir dades entre bases de dades i objectes programmatics.
JPA (Java Persistence API) són unes llibreries de Java EE per gestionar dades allotjades en bases de dades relacionals desde aplicacions JAVA
Hibernate és una eina de mapeig ORM per JAVA que utilitza JPA
Hibernate “facilita” la feina de treballar amb bases de dades des de Java, fent transparent la relació entre taules de la base de dades i objectes Java
Un cop definits els objectes i com es relacionen amb la base de dades, Hibernate pot crear les taules automàticament i gestionar l'actualització de les dades a la base de dades sense que s'hagi d'escriure codi SQL.
Hibernate es configura a través de l'arxiu «hibernate.properties», aquest arxiu conté configuracions de hibernate que no estan relacionades amb el mapeig de dades, tals com:
- Tipus de connexió amb la base de dades.
- Usuari/Contrasenya si la base de dades ho requereix
- URI de la base de dades
(Cal canviar DB_USER i DB_PASSWORD pels valors corresponents a la connexió …)
hibernate.dialect=org.hibernate.dialect.MySQLDialect hibernate.connection.driver_class=com.mysql.jdbc.Driver hibernate.connection.url=jdbc:mysql://localhost:3306/hibernate hibernate.connection.username=DB_USER hibernate.connection.password=DB_PASSWORD hibernate.show_sql=false hibernate.hbm2ddl.auto=update
(Amb SQLite s’especifica l’arxiu a utilitzar, en aquest cas «database.db»)
hibernate.dialect=com.enigmabridge.hibernate.dialect.SQLiteDialect hibernate.connection.driver_class=org.sqlite.JDBC hibernate.connection.url=jdbc:sqlite:database.db hibernate.show_sql=false hibernate.hbm2ddl.auto=update
L'arxiu «hibernate.cfg.xml» conté configuracións relacionades amb el mapeig de dades.
Important: En aquest arxiu s’ha de posar un element <mapping> per cada classe que s’ha de mapejar, segons si són amb format .hbm.xml (format antic) o JPA (format nou)
Exemple:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE hibernate-configuration SYSTEM "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <mapping resource="Employee.hbm.xml"/> <mapping class="Contact"/> </session-factory> </hibernate-configuration>
En aquest exemple s'indica:
- Cal mapejar les classes «Employee» i «Contact»
- La classe «Employee» fa servir el format antic (hbm.xml) i configura les relacions amb la base de dades a través de l'arxiu «Employee.hbm.xml»
- La classe «Contact» fa servir el format JPA modern i configura les relacions directament a través de «Contact.java»
Antigament el mapeig entre Hibernate i la base de dades es feia a través d'arxius «.hbm.xml»
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="Employee" table="employee"> <meta attribute="class-description"> This class contains the employee detail. </meta> <id name="id" type="int" column="id"> <generator class="native"/> </id> <property name="firstName" column="first_name" type="string"/> <property name="lastName" column="last_name" type="string"/> <property name="salary" column="salary" type="int"/> </class> </hibernate-mapping>
En aquest exemple es veu com:
- Relaciona l'objecte «Employee» amb la taula «employee»
- El camp 'id' és de tipus enter i es genera automàticament
- Els camps 'firstName« i «lastName» corresponen a les columnes de la base de dades «first_name» i «last_name» i són de tipus String
En aquest exemple es fan servir notacions JPA per definir amb quina taula es relaciona l'objecte i les propietats de les columnes a la base de dades
import javax.persistence.*; @Entity @Table(name="contact") public class Contact { @Id @Column(name="id") @GeneratedValue(strategy=GenerationType.AUTO) // L’id es genera automàticament private long id; @Column(name="name") private String name;
És una bona pràctica tenir un objecte «Manager» que s'encarregui de definir totes les operacions que es poden fer amb la base de dades, habitualment són operacions basades en CRUD:
- Create
- Read
- Update
- Delete
Exemple d'arxiu Manajer.java
import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.Set; import org.hibernate.HibernateException; import org.hibernate.SQLQuery; import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; public class Manager { private static SessionFactory factory; public static void createSessionFactory() { try { Configuration configuration = new Configuration(); configuration.configure(); StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings( configuration.getProperties()).build(); factory = configuration.buildSessionFactory(serviceRegistry); } catch (Throwable ex) { System.err.println("Failed to create sessionFactory object." + ex); throw new ExceptionInInitializerError(ex); } } public static void close () { factory.close(); } public static Employee addEmployee(String firstName, String lastName, int salary){ Session session = factory.openSession(); Transaction tx = null; Employee result = null; try { tx = session.beginTransaction(); result = new Employee(firstName, lastName, salary); session.save(result); tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); result = null; } finally { session.close(); } return result; } public static Contact addContact(String lname, String lmail){ Session session = factory.openSession(); Transaction tx = null; Contact result = null; try { tx = session.beginTransaction(); result = new Contact(lname, lmail); session.save(result); tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); result = null; } finally { session.close(); } return result; } public static <T> T getById(Class<? extends T> clazz, long id){ Session session = factory.openSession(); Transaction tx = null; T obj = null; try { tx = session.beginTransaction(); obj = clazz.cast(session.get(clazz, id)); tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); } finally { session.close(); } return obj; } public static void updateContact(long contactId, String name, String email, Set<Employee> employees){ Session session = factory.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); Contact obj = (Contact) session.get(Contact.class, contactId); obj.setName(name); obj.setEmail(email); obj.setEmployees(employees); session.update(obj); tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); } finally { session.close(); } } public static void updateEmployee(long employeeId, String firstName, String lastName, int salary){ Session session = factory.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); Employee obj = (Employee) session.get(Employee.class, employeeId); obj.setFirstName(firstName); obj.setLastName(lastName); obj.setSalary(salary); session.update(obj); tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); } finally { session.close(); } } public static <T> void delete(Class<? extends T> clazz, Serializable id){ Session session = factory.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); T obj = clazz.cast(session.get(clazz, id)); session.delete(obj); tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); } finally { session.close(); } } public static <T> Collection<?> listCollection(Class<? extends T> clazz) { return listCollection(clazz, ""); } public static <T> Collection<?> listCollection(Class<? extends T> clazz, String where){ Session session = factory.openSession(); Transaction tx = null; Collection<?> result = null; try { tx = session.beginTransaction(); if (where.length() == 0) { result = session.createQuery("FROM " + clazz.getName()).list(); } else { result = session.createQuery("FROM " + clazz.getName() + " WHERE " + where).list(); } tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); } finally { session.close(); } return result; } public static <T> String collectionToString(Class<? extends T> clazz, Collection<?> collection){ String txt = ""; for(Object obj: collection) { T cObj = clazz.cast(obj); txt += "\n" + cObj.toString(); } if (txt.substring(0, 1).compareTo("\n") == 0) { txt = txt.substring(1); } return txt; } public static void queryUpdate (String queryString) { Session session = factory.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); SQLQuery query = session.createSQLQuery(queryString); query.executeUpdate(); tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); } finally { session.close(); } } public static List<Object[]> queryTable (String queryString) { Session session = factory.openSession(); Transaction tx = null; List<Object[]> result = null; try { tx = session.beginTransaction(); SQLQuery query = session.createSQLQuery(queryString); @SuppressWarnings("unchecked") List<Object[]> rows = query.list(); result = rows; tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); } finally { session.close(); } return result; } public static String tableToString (List<Object[]> rows) { String txt = ""; for (Object[] row : rows) { for (Object cell : row) { txt += cell.toString() + ", "; } if (txt.length() >= 2 && txt.substring(txt.length() - 2).compareTo(", ") == 0) { txt = txt.substring(0, txt.length() - 2); } txt += "\n"; } if (txt.length() >= 2) { txt = txt.substring(0, txt.length() - 2); } return txt; } }
Les transaccions són aquelles operacions que fem amb la base de dades, es diuen així perquè és important que cada transacció obri i tanqui una sessió per mantenir la integritat de les dades. A més:
- Les transaccions poden resultar fallides, i no modificar dades a la base de dades
- Algunes transaccions s'han de personalitzar per cada objecte (operacions d'afegir, actualitzar, …)
- Altres transaccions són genèriques per tots els objectes (esborrar, llistar, consutar, …)
Exemple de la transacció «addEmployee», afegir un «Employee» a la taula «employee»:
public static Employee addEmployee(String firstName, String lastName, int salary){ Session session=factory.openSession(); Transaction tx=null; Employee result=null; try { tx=session.beginTransaction(); result=new Employee(firstName, lastName, salary); session.save(result); tx.commit(); } catch (HibernateException e) { if (tx!=null) tx.rollback(); e.printStackTrace(); result=null; } finally { session.close(); } return result; }
Important: Les dades no es guarden fins que s'ha fet el «tx.commit()», i fins i tot en aquest moment la transacció pot resultar fallida
Hibernate fa ús dels getters i setters dels objectes, així que són obligatoris per tots aquells valors que estan mapejats cap a la base de dades
Igualment, és una bona pràctica sobreescriure la funció «toString()» per obtenir les dades de l'objecte en qüestió.
Exemple complert amb getters i setters:
import java.io.Serializable; import java.util.List; import java.util.Set; import javax.persistence.*; @Entity @Table(name="Contact", uniqueConstraints={@UniqueConstraint(columnNames="id")}) public class Contact implements Serializable { @Id @Column(name="id", unique=true, nullable=false) @GeneratedValue(strategy=GenerationType.IDENTITY) // L'id es genera automàticament private long contactId; @Column(name="name") private String name; @Column(name="email") private String email; @ManyToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE}) @JoinTable(name="Employee_Contact", joinColumns={@JoinColumn(referencedColumnName="id")}, inverseJoinColumns={@JoinColumn(referencedColumnName="id")}) private Set<Employee> employees; // Ha de tenir getter i setter perquè s'encarrega de la taula relacional N:N public Contact() { } public Contact(String name, String email) { this.name=name; this.email=email; } public long getContactId() { return this.contactId; } public void setContactId(long id) { this.contactId=id; } public String getName() { return this.name; } public void setName(String name) { this.name=name; } public String getEmail() { return email; } public void setEmail(String email) { this.email=email; } public Set<Employee> getEmployees () { return this.employees; } public void setEmployees (Set<Employee> employees) { this.employees=employees; } public List<Object[]> queryEmployees () { long id=this.getContactId(); return Manager.queryTable("SELECT DISTINCT e.* FROM Employee_Contact ec, Employee e WHERE e.id=ec.employees_id AND ec.contacts_id=" + id); } @Override public String toString () { String str=Manager.tableToString(queryEmployees()); return this.getContactId() + ": " + this.getName() + ", " + this.getEmail() + ", Employees: [" + str + "]"; } }
Per mapejar les relacions entre taules, Hibernate ofereix:
- 1:1 one-to-one
- 1:N one-to-many
- N:M many-to-many
Hi ha dues maneres:
- Amb arxius .hbm.xml (la manera antiga)
- Amb anotacions JPA directament al codi Java (la manera actual)
Són aquelles que creen una relació entre un element d’una taula i múltiples d’altres taules. Per exemple:
- Un carret de la compra té diversos productes (però cada producte està en un carret)
- Una escola té diversos alumnes (però cada alumne va a una escola)
NOTA: Per simplicitat no veurem les relacions ‘one-to-one’
Cart.hbm.xml
<hibernate-mapping> <class name="Cart" table="Cart"> <meta attribute="class-description">CD</meta> <id name="cartId" type="long" column="cartId"> <generator class="native"/> </id> <property name="type" column="type" type="string"/> <set name="items" cascade="all"> <key column="cartId"/> <one-to-many class="Item"/> </set> </class> </hibernate-mapping>
Cart.java
public class Cart implements Serializable { private long cartId; private String type; private Set<Item> items; public Cart() {} public Cart(String type) { this.type=type; } // Getters i Setters }
Item.hbm.xml
<hibernate-mapping> <class name="Item" table="Item"> <meta attribute="class-description">AB</meta> <id name="itemId" type="long" column="id"> <generator class="native"/> </id> <property name="name" column="name" type="string"/> <many-to-one name="cart" class="Cart" fetch="select"> <column name="cartId" not-null="false" /> </many-to-one> </class> </hibernate-mapping>
Item.java
public class Item implements Serializable { private long itemId; private String name; private Cart cart; public Item() { } public Item(String name) { this.name=name; } // Getters i Setters }
Cart.java
@Entity @Table(name="Cart") public class Cart implements Serializable { @Id @GeneratedValue(strategy=GenerationType.AUTO) @Column(name="cartId", unique=true, nullable=false) private long cartId; @Column(name="type") private String type; @OneToMany @JoinColumn(name="cartId") private Set<Item> items; public Cart() {} public Cart(String type) { this.type=type; } // Getters, Setters i toString }
Item.java
@Entity @Table(name="Item") public class Item implements Serializable { @Id @Column(name="id", unique=true, nullable=false) @GeneratedValue(strategy=GenerationType.AUTO) // L'id es genera automàticament private long itemId; @Column(name="name") private String name; @ManyToOne @JoinColumn(name="cartId", insertable=false, updatable=false) private Cart cart; public Item() { } public Item(String name) { this.name=name; } // Getters, Setters i toString }
Són aquelles que creen una relació entre múltiples elements de múltiples d’altres taules. Necessiten una taula extra per mantenir la relació. Per exemple:
- Una supermercat té N treballadors i alguns d'aquests són N clients
- Múltiples comandes es relacionen amb múltiples clients
Contact.hbm.xml
<hibernate-mapping> <class name="Contact" table="Contact"> <meta attribute="class-description">AB</meta> <id name="contactId" type="long" column="id"> <generator class="identity"/> </id> <property name="name" column="name" type="string"/> <property name="email" column="email" type="string"/> <set name="employees" table="Employee_Contact" cascade="save-update"> <key column="contacts_id"/> <many-to-many column="employees_id" class="Employee"/> </set> </class> </hibernate-mapping>
Contact.java
public class Contact implements Serializable { private long contactId; private String name; private String email; private Set<Employee> employees; public Contact() { } public Contact(String name, String email) { this.name=name; this.email=email; } // Getters i Setters }
Employee.hbm.xml
<hibernate-mapping> <class name="Employee" table="Employee"> <meta attribute="class-description">AB</meta> <id name="employeeId" type="long" column="id"> <generator class="identity"/> </id> <property name="firstName" column="firstName" type="string"/> <property name="lastName" column="lastName" type="string"/> <property name="salary" column="salary" type="int"/> <set name="contacts" table="Employee_Contact" inverse="true"> <key column="employees_id"/> <many-to-many column="contacts_id" class="Contact" /> </set> </class> </hibernate-mapping>
Employee.java
public class Employee implements Serializable { private long employeeId; private String firstName; private String lastName; private int salary; private Set<Contact> contacts; // No té getter i setter public Employee() {} // Getters i Setters }
Contact.java
@Entity @Table(name="Contact", uniqueConstraints={@UniqueConstraint(columnNames="id")}) public class Contact implements Serializable { @Id @Column(name="id", unique=true, nullable=false) @GeneratedValue(strategy=GenerationType.IDENTITY) private long contactId; @Column(name="name") private String name; @Column(name="email") private String email; @ManyToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE}) @JoinTable(name="Employee_Contact", joinColumns={@JoinColumn(referencedColumnName="id")}, inverseJoinColumns={@JoinColumn(referencedColumnName="id")}) private Set<Employee> employees; // Constructors, Getters, Setters i toString }
Employee.java
@Entity @Table(name="Employee", uniqueConstraints={@UniqueConstraint(columnNames="id")}) public class Employee implements Serializable { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(name="id", unique=true, nullable=false) private long employeeId; @Column(name="firstName") private String firstName; @Column(name="lastName") private String lastName; @Column(name="salary") private int salary; @ManyToMany(mappedBy="employees") private Set<Contact> contacts; // Constructors, Getters, Setters i toString }
Els arxius ./run.sh i .\run.bat tenen el codi per fer anar els exemples des de la línia de comandes amb versions modernes de Java (per sistemes Unix o Windows respectivament)
Als exemples, cada vegada s'esborra i crea la base de dades com a nova
Fan servir “@hibernate.argfile” per configurar els paràmetres de .java que permeten compilar i fer anar Hibernate
hibernate-exempleintroduccio.zip
hibernate-exemplejpa_onetomany.zip
hibernate-exemplejpa_manytomany.zip
hibernate-exemplexml_onetomany.zip