JEE Back-end

Introduction Hibernate avec JPA

Dans cette séance : ORM

ORM (Object Relational Mapping)

  • Des Objets Java en mémoire (persistants ou pas)
  • Des enregistrements dans une base de données Relationnelle
  • Un Mapping qui gère la correspondance entre les classes Java et les tables de la BD.

L'ORM va gérer la persistance dans la BD des objets Java en générant automatique tout le SQL nécessaire.

Historique

  • JDBC -- Java DataBase Connectivity (java.sql)
    Code indépendant du serveur de BD (MySQL,PostgreSQL,...)
    Relativement bas niveau: SQL
  • ORM (fin des années 90)
    Hibernate, JBoss/Quarkus, ...
  • JPA -- Java Persistence API
    C'est une API (interfaces, annotation, ...) très inspirée par l'API d'Hibernate.
    Hibernate, JBoss/Quarkus, ... sont des frameworks qui implémente l'API JPA.

Hibernate (1/2)

Hibernate (2/2)

Hibernate est sans doute l'ORM implémentant la JPA le plus utilisé.

Historiquement Hibernate a sa propre API et il implémente aussi l'API de JPA.

Les deux API sont assez proches (en effet, l'API d'Hibernate a insipiré la JPA). L'API d'Hibernate est plus riche que la JPA.

Le parti pris de ce cours est de présenter Hibernate avec l'API JPA.

Pour l'instant, on fait Hibernate sans Spring !

Mapping Hibernate (1/2)

POJO

public class Student {
	private int id;
	private String firstName;
	private String lastName;

	//default construct, constructeur, getters et setters
}

Mapping Hibernate: fichier hibernate.cfg.xml

<hibernate-mapping>
<class name = "fr.uge.jee.hibernate.Student" table = "Students">
    <meta attribute = "class-description">
        This class contains the details of the students
    </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"/> 
</class>
</hibernate-mapping>	

Mapping Hibernate (2/2)

Le mapping est la description d'une table pour la classe Student et des relations entre les champs de la classe et les colonnes de la table.

Mapping Hibernate par les annotations

@Entity
@Table(name = "Students")
public class Student {

    @Id
    @GeneratedValue
    private long id;
    @Column(name = "first_name")
    private String firstName;
    @Column(name = "last_name")
    private String lastName;

    ...
}
  • La norme JPA demande le constructeur sans paramètres, les getters et les setters.
  • Sans l'annotation @Table ou @Column, Hibernate utilise le nom de la classe ou du champs.
  • Dans ce cours, nous utiliserons les annotations pour décrire les mappings.

Rôle de la clé primaire

La clé primaire a un rôle à part car elle n'a pas de sens dans le monde Java et qu'elle est fixée par la BD et donc par l'ORM.

public class Student {

    ...

    public Student(){} // for Hibernate

    public Student(String firstName, String lastName) { // for Java 
        this.firstName = firstName;
        this.lastName = lastName;
    }

    ...
 }
  • Le constructeur sans paramètres sera utilisé par Hibernate qui fixera la clé primaire et les autres champs avec les setters.
  • Votre code utilisera l'autre constructeur et laissera la clé primaire à sa valeur par défaut.

EntityManager

EntityManager joue le rôle d'un cache temporaire qui maintient pour certaines lignes de la base de données un unique objet Java correspondant.

Un EntityManager encapsule une une connexion JDBC. Il ne vit donc pas très longtemps. On va en recréer souvent.

L'EntityManager n'est pas thread-safe.

Etat des objets vis-à-vis de l'EntityManager

  • Transient: Objet qui n'a jamais été dans l'EntityManager et dont le champ @Id est à sa valeur par défaut ou qui a été supprimé de l'EntityManager.
  • Persistent: Objet qui est dans l'EntityManager. Les modifications sur cet objet seront répercutées dans la BD (cf. slides suivants).
  • Detached: Les objets qui ne sont plus dans l'EntityManager mais qui y ont été (hors suppression). En particulier, ces objets ont un champ @Id qui n'a plus sa valeur par défaut.

EntityTransaction

Les EntityTransaction correspondent aux transactions au sens BD du terme.

C'est à ce niveau qu'on pourra fixer le niveau d'isolation (cf. le super cours de Mr. Francis).

Les EntityTransaction sont liées à un EntityManager. C'est à l'intérieure d'une EntityTransaction qu'on peut va faire persister des objets, récupérer des objets correspondant à la BD, supprimer des éléments de la BD, ...

En pratique (1/2)

Toutes les interactions avec la BD ont la même forme.

EntityManagerFactory emf = ...
EntityManager em = emf.createEntityManager();
var tx = em.getTransaction();
try{
    tx.begin();

    ...

    tx.commit();
} catch (Exception e){
    tx.rollback();
    throw e;
} finally {
	em.close();
}

En pratique (2/2)

EntityManagerFactory emf = ...
EntityManager em = emf.createEntityManager();
var tx = em.getTransaction();
try{
    tx.begin();
    var harry = new Student("Harry","Potter");
    em.persist(harry);
    tx.commit();
} catch (Exception e){
    tx.rollback();
    throw e;
} finally {
    em.close();
}

EntityManagerFactory

La classe EntityManagerFactory permet de créer des EntityManager.

La création d'une EntityManagerFactory se fait à partir d'un ficher de configuration persistence.xml et des classes annotées par @Entity.

La création de l'EntityManagerFactory est coûteuse. Elle ne doit être construite qu'une seule fois !

Création de l'EntityManagerFactory

<persistence ...>
    <persistence-unit name="main-persistence-unit">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:tcp://localhost/~/h2DB"/>
           

             <property name="jakarta.persistence.jdbc.user"       value="sa"/>
            <property name="jakarta.persistence.jdbc.password"value=""/>
            
            <property name="jakarta.persistence.schema-generation.database.action" value="create"/>
        </properties>
    </persistence-unit>
</persistence>
public class PersistenceUtils {

    static final EntityManagerFactory ENTITY_MANAGER_FACTORY 
                 = Persistence.createEntityManagerFactory("main-persistence-unit");

    static EntityManagerFactory getEntityManagerFactory(){
        return ENTITY_MANAGER_FACTORY;
    }
}

On implémente le pattern Singleton de manière thread-safe.

EntityManager.persist (1/3)

EntityManager.persist(Student student) prend un objet student transient et le fait persister dans la BD.

L'objet student devient persistent.

Toute modification (par les setters) de student sera répercutée au moment du entityTransaction.commit.

Le champ id de student va être positionner. Attention, cela n'est garanti qu'après le entityTransaction.commit

EntityManager.persist (2/3)

EntityManagerFactory emf = PersistenceUtils.getEntityManagerFactory();
EntityManager em = emf.createEntityManager();
var tx = em.getTransaction();
try{
    tx.begin();
    var harry = new Student("Harry","Potter");
    em.persist(harry);
    tx.commit();
} catch (Exception e){
    tx.rollback();
    throw e;
} finally {
    em.close();
}

Ajoute un étudiant Harry Potter dans la BD. Le champs id de l'objet harry sera fixé par Hibernate au moment du commit.

EntityManager.persist (3/3)

EntityManagerFactory emf = PersistenceUtils.getEntityManagerFactory();
EntityManager em = emf.createEntityManager();
var tx = em.getTransaction();
try{
    tx.begin();
    var harry = new Student("Harry","Potter");
    em.persist(harry);
    harry.setFirstName("Giny");
    tx.commit();
} catch (Exception e){
    tx.rollback();
    throw e;
} finally {
    em.close();
}

Ajoute un étudiant Giny Potter dans la BD !

EntityManager.find (1/2)

EntityManager.find(Student.class,1L) renvoie l'objet Student correspondant à la ligne de la table Students ayant la clé primaire 1. S'il n'y a pas de ligne avec cette clé, la méthode renvoie null.

L'objet renvoyé est persistent.

Toute modification (par les setters) de cet objet sera répercutée au moment du entityTransaction.commit.

EntityManager.find (2/2)

EntityManagerFactory emf = PersistenceUtils.getEntityManagerFactory();
EntityManager em = emf.createEntityManager();
var tx = em.getTransaction();
try{
    tx.begin();
    var student = em.find(Student.class,1L);
    if (student!=null){
    	System.out.println(student); // print student with id 1
    }
    tx.commit();
} catch (Exception e){
    tx.rollback();
    throw e;
} finally {
    em.close();
}

EntityManager.merge

La méthode EntityManager.merge(Student student) prend en paramètre un objet student detached.

  • Il récupère un objet persistent newstudent depuis l'EntityManager ou la BD pour la clé primaire student.id
  • elle recopie les champs de student dans newstudent: comme newstudent est persistent, cela modifiera la BD
  • renvoie newstudent

Attention à ne pas continuer à utiliser student.

EntityManager.remove

La méthode EntityManageer.remove(student) prend en paramètre un objet Student persistent et supprime la ligne correspondante dans la BD.

L'objet student n'est plus persistent. Il redevient transient.

JPQL

JPA offre un langage de requête, nommé JPQL, proche de SQL mais présente plusieurs avantages:

  • Les requêtes parlent des classes Java et pas des tables de la BD
    Cela évite les problèmes de noms.
  • Les requêtes JPQL permettent de changer la stratégie de rappatriement des données dans le cas de mappings où une classe utilise d'autres classes.
    C'est un point important, nous reviendrons dessus dans la suite.
var q = "SELECT s from Student s where s.firstName='Harry'";
TypedQuery<Student> query = em.createQuery(q,Student.class);
List<Student> results = query.getResultList();
Student resutl = query.getSingleResult();

La requête JPQL utilise la classe Student et non la table Students.

On peut faire des requêtes en SQL avec createNativeQuery.

Protection contre les injections (1/2)

Code vulnérable aux injections:

var firstName = ... // recupérer d'une form HTML
var q = "SELECT s FROM Student s WHERE s.firstName='"+firstName+"'";
TypedQuery<Student> query = em.createQuery(q,Student.class);
List<Student> res = query.getResultList();

Protection contre les injections (2/2)

var firstName = "Harry' AND s.grade='10";
var q = "SELECT s FROM Student s WHERE s.firstName='"+firstName+"'";
TypedQuery<Student> query = em.createQuery(q,Student.class);
List<Student> res = query.getResultList();

La requête JPQL devient alors:

SELECT s FROM Student s WHERE s.firstName='Harry' AND s.grade='10'

On peut accéder à des informations de la BD alors que ce n'était pas prévu.
On suppose ici qu'on a rajouté un champ grade à la classe Student.

Protection contre les injections (2/2)

var firstName = ... // 
var q = "SELECT s FROM Student s WHERE s.firstName= :firstname";
TypedQuery<Student> query = em.createQuery(q,Student.class);
query.setParameter("firstname",firstName);
List<Student> res = query.getResultList();

Les caractères spéciaux de firstName sont échappés par le framework.
Pas d'injection possible!