JPA

Un article de Agora2ia.


Sommaire

Présentation

En utilisant les annotations, les informations de persistance sont directement définies dans le bean lui même !...


Travaux Pratiques

http://today.java.net/pub/a/today/2006/05/23/ejb3-persistence-api-for-client-side-developer.html

  1. Creer une base (Cf. Derby)
  2. Creer un nouveau projet
  3. Ajouter au projet les librairies :
    1. De l'ORM (Ex: Hibernate Core, Hibernate Annotations, and Hibernate Entity Manager .zip files)
    2. De la BD (contenant le driver)
  4. Creer le fichier persistence.xml
  5. Une classe avec les annotations de persistence : Personn
  6. La classe main qui cree une Personn et la sauvegarde

Et les tests unitaires

Unit test JPA Entities with in-memory database : http://eskatos.wordpress.com/2007/10/15/unit-test-jpa-entities-with-in-memory-database/


Modèles de mapping de l'heritage

  • Annotations vs. fichier XML.
  • Note : Ce que j'appelle une hiérarchie c'est par exemple les classes Cocker, Boxer, Chien, Chat et Annimal, toutes appartenant au même arbre d'héritage : à savoir elles héritent toutes (directement ou nom) de Annimal.
  1. Une table par classe (les tables sont jointes pour reconstituer les données (JOINED))
  2. Une seule table par hiérarchie de classe (SINGLE_TABLE)
  3. Une table par classe concrète (optionnelle, TABLE_PER_CLASS)
  4. Héritage de propriétés des classes parentes


Il est à noter que les stratégies de mapping d'un héritage se fait via l'annotation de classe @Inheritance au niveau de LA classe mère. Les classes filles héritent de cette stratégie sans avoir à répéter cette annotation.


MappedSuperclass : Superclasse associée

JPA définie la notion de Mapped Superclass qui correpond à une classe de l'héritage (qui regroupe un ensemble de données et de comportement) mais qui n'est pas persistée en base et sur laquelle on ne pourra pas faire de query : ce n'est donc pas une @Entity.

Pour signifier cela, on utilise l'annotation de classe @MappedSuperclass.

Dans notre exemple avec les annimaux, Chien et Chat sont des candidats potentiels pour être des MappedSuperclass.

@Entity
public class Annimal { /* ... */ }

@MappedSuperclass
public abstract class Chien extends Annimal { /* ... */ }

@Entity
public class Boxer extends Chien { /* ... */ }


Il n'est pas obligatoire que les Mapped Superclass soient abstraites, mais cela reste une bonne pratique : difficile d'envisager une classe concrète (de notre héritage persisté), que l'on puisse instancier mais qui ne soient pas persistée.


SINGLE_TABLE : Une seule table pour toutes les classes d'une hiérarchie

Dans une unique table, chaque ligne représente une instance d'une des classes d'une même hierarchie. Ce modèle, le plus commun et performant (en échange d'une plus grande comsommation d'espace), entraine que pour chaque ligne, certaines colonnes sont nulles etant donné que l'on retrouve les même colonnes/attributs pour toutes les lignes/classes. Cela implique que ces colonnes, spécifiques aux classes filles, doivent être "nullable".

La classe mère, porte l'annotation @Inheritance(strategy=InheritanceType.SINGLE_TABLE)

Nous avons une colonnes qui permet d'identifier le type/classe de la ligne considérée. Cette colonne porte le juste de nom de "colonne descriminante", et est définie ainsi par l'annotation @DiscriminatorColumn(name="C_TYP") (aussi au niveau de la classe). Par défaut le nom utilisé sera DTYPE. Le type par défaut de cette colonne est le STRING, mais on peut utiliser un des deux autres types possibles, à savoir INTEGER ou CHAR. Pour cela on utilisera l'annotation discriminatorType.

Ensuite, chaque ligne aura dans cette colonne une "valeur descriminante" ou "indicateur de classe", qui permet de determiner le type de la classe associée à cette ligne. Chacune de ces valeurs possibles sera associée à une classe concrète via l'annotation de classe @DiscriminatorValue. Cette valeur doit être du même type que spécifié précdemment via le discriminatorType. Si le type descrimant est INTEGER, la @DiscriminatorValue doit être définie. Si le type descrimant est STRING et que la @DiscriminatorValue n'est pas définie l'API utilisera la DiscriminatorValue pour identifier le nom de la classe associée...

La "valeur descriminante" peut être définie de trois façcons :

  1. Via l'annotation de classe @DiscriminatorValue vue ci-dessus.
  2. Via l'annotation de classe traditionnelle @Entity(name="PTEmp") (les lignes ayant "PTEmp" comme DiscriminatorValue seront associées à cette classe).
  3. Via la même annotation de classe mais sans l'attribut 'name' : @Entity (c'est le nom de la classe qui devra être indiqué dans la DiscriminatorColumn)


Dans l'exemple suivant, notre unique table ANNIMAUX contient une colonne descriminante appelée ANN_TYPE qui contient l'une des trois valeurs suivantes : Chien, BOX ou COC.

@Entity
@Table(name="ANNIMAUX")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="ANN_TYPE")
public abstract class Annimal { /* ... */ }

@Entity
public class Chien extends Annimal { /* ... */ }

@MappedSuperclass
public abstract class Mammifere extends Annimal { /* ... */ }

@Entity
@DiscriminatorValue("BOX")
public class Boxer extends Chien { /* ... */ }

@Entity(name="COC")
public class Cocker extends Chien { /* ... */ }


Debug

Si la colonne descriminante contient une valeur pour laquelle il n'y a pas de valeur descriminante associée (dit autrement, il n'y a pas de classe fille associée à la valeur contenue dans la colonne descriminante de la classe mère), vous aurez une erreur dans ce genre :

Exception Description: Missing class for indicator field value [580] of type [class java.lang.String].
Descriptor: RelationalDescriptor(com.company.projet.Item --> [DatabaseTable(ITEM)])

Cette ligne ce lit comme suit :

  • JPA n'a pas trouvé de classe fille correspondant à la valeur descriminante 580
  • Le type de cette colonne descriminante est java.lang.String.
  • Qu'elle se trouve dans la classe mère com.company.projet.Item
  • Associée à la table ITEM


Une autre erreur qu'il est difficile de lier à cette fonctionnalité :

Exception Description: Cannot find value in class indicator mapping in parent descriptor [null]

Cette erreur est apparue sur mon application suite à une modification. Au cours d'un refactoring j'avais volontairement supprimé les classes filles du fichier persistence.xml. En oubliant de les remettre et en relançant l'application aucun problème d'exécution, mais ma sauvegarde/chargement avait un comportement bizarre. Etape par étape, j'en suis arrivé à définir ma classe racine comme abstract et c'est à ce moment que l'erreur précédente est survenue.

Un conseil donc : mettre les classes racines comme abstract permet de mieux lever les erreurs.


LANCER LE SERVEUR DEPUIS LE REPERTOIRE CONTENANT LES DONNEES

call %DERBY_INSTALL%/bin/startNetworkServer.bat


Contenu de la base de données

+-------------------------------------------------------+
|                         ITEMS                         |
+---------+-----+-----------+------------+--------------+
| ITEM_ID | ... | ITEM_TYPE | DATE_VALUE | DOUBLE_VALUE |
+---------+-----+-----------+------------+--------------+
|    33   | ... |   'DATE'  | 27/11/1933 |     null     |
|   123   | ... |  'DOUBLE' |   null     |    300.12    |
|                           .                           |
|                           .                           |
|                           .                           |
+---------+-----+-----------+------------+--------------+


Item.java

@Entity
@Table(name="ITEMS")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="ITEM_TYPE")
public abstract class Item { 
    @Id
    @Column(name = "ITEM_ID")
    private Long id;
}

AbstractCell.java

@MappedSuperclass
public abstract class AbstractCell extends Item { /* ... */ }

DateCell.java

@Entity
@DiscriminatorValue("DATE")
public class DateCell extends Cell {
    @Column(name = "DATE_VALUE")
    @Temporal(TemporalType.DATE)
    protected Date value;
    
    public Date getValue() { return value; }
    
    public void setValue(Date value) { this.value = value; }
}

BigDecimalCell.java

@Entity(name="DOUBLE")
public class BigDecimalCell extends Cell { 
    @Column(name = "DOUBLE_VALUE")
    private BigDecimal value;
    
    public BigDecimal getValue() { return value; }
    
    public void setValue(BigDecimal value) { this.value = value; }
}


META-INF/persistence.xml

NB -> par defaut, user == password == app

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
	version="1.0">
	<persistence-unit name="JpaInheritanceUnit" transaction-type="RESOURCE_LOCAL">
		<provider>oracle.toplink.essentials.PersistenceProvider</provider>
		<mapping-file>META-INF/orm.xml</mapping-file>
		<class>com.bnpparibas.bfi.era.common.deal.dto.Item</class>
		<class>com.bnpparibas.bfi.era.common.deal.dto.DateCell</class>
		<class>com.bnpparibas.bfi.era.common.deal.dto.BigDecimalCell</class>
		<exclude-unlisted-classes>true</exclude-unlisted-classes>
		<properties>
			
			
			
			
			<property name="toplink.jdbc.url" value="jdbc:derby:C:\local\data\derby\JpaSpike" />
			<property name="toplink.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />

			<property name="toplink.jdbc.user" value="app" />
			<property name="toplink.jdbc.password" value="app" />
			<property name="toplink.jdbc.driver" value="org.apache.derby.jdbc.ClientDriver" />
		</properties>
	</persistence-unit>
</persistence>


Main.java

package com.bnpparibas.bfi.era;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.Query;

import com.bnpparibas.bfi.era.common.deal.dto.Item;

public class Main {
	EntityManagerFactory factory;
	EntityManager manager;

	public void init() {
		factory = Persistence.createEntityManagerFactory("JpaInheritanceUnit");
		manager = factory.createEntityManager();
	}

	private void shutdown() {
		manager.close();
		factory.close();
	}

	private void search() {
		EntityTransaction tx = manager.getTransaction();
		tx.begin();
		System.out.println("searching for Item");
		Query query = manager.createQuery("select i from Item i");
		List<Item> items = query.getResultList();
		for (Item i : items) {
			System.out.println("got an Item: " + i.getType());
		}
		System.out.println("done searching for Item");
		tx.commit();
	}

	public static void main(String[] args) {
		Main main = new Main();
		main.init();
		try { main.search(); } 
		catch (RuntimeException ex) { ex.printStackTrace(); } 
		finally { main.shutdown(); }
	}
}

JOINED : Une table par classe

Dans cette stratégie, toutes les classes sont représentées par une table (même les classes abstraites).

Cela nécessite des jointures pour retrouver les propriétés d’une instance d’une classe.

Une colonne discriminatrice est ajoutée dans la table qui correspond à la classe racine de la hiérarchie d’héritage. Cette colonne permet de simplifier certaines requêtes ; par exemple, pour retrouver les noms de tous les employés (classe Personne à la racine de la hiérarchie d’héritage).


@Entity
@Table(name="EMP")
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="EMP_TYPE", discriminatorType=DiscriminatorType.INTEGER)
public abstract class Employee { /*...*/ }

@Entity
@Table(name="CONTRACT_EMP")
@DiscriminatorValue("1")
public class ContractEmployee extends Employee { /*...*/ }

@MappedSuperclass
public abstract class CompanyEmployee extends Employee { /*...*/ }

@Entity
@Table(name="FT_EMP")
@DiscriminatorValue("2")
public class FullTimeEmployee extends CompanyEmployee { /*...*/ }

@Entity
@Table(name="PT_EMP")
@DiscriminatorValue("3")
public class PartTimeEmployee extends CompanyEmployee { /*...*/ }




Modèles d'association

Association One To One

On utilise pour cette relation l'annotation de membre @OneToOne.

On souhaite ici définir une relation entre un Employe et une PlaceDeParking :

@Entity
@Table(name = "EMPLOYEE")
public class Employe {
    @OneToOne
    @JoinColumn(name = "PLACE_ID") // EMPLOYEE.PLACE_ID
    private PlaceDeParking placeDeParking;
    // ...
}

@Entity
@Table(name = "PARKING_PLACE")
public class PlaceDeParking {
    @OneToOne(mappedBy = "placeDeParking")
    private Employe Employe;
    // ...
}


Association One To Many

One To Many bi-directionnelle

On utilise pour cette relation l'annotation de membre @ManyToOne.

On retrouve une structure très proche de l'association précédente, en remplaçant @OneToOne par @ManyToOne.

On souhaite ici définir une relation entre un contenant (un Livre) et ses contenus (des Chapitre) :

// Le CONTENU
@Entity
@Table(name = "CHAPITRE")
public class Chapitre {
    @ManyToOne
    @JoinColumn(name = "ID_LIVRE")
    private Livre livre;
    // ...
}

// Le CONTENANT
@Entity
@Table(name = "LIVRE")
public class Livre {
    @OneToMany(mappedBy = "livre")
    private Collection<Chapitre> chapitres;
    // ...
}

Dans le contenant, si l'on doit utiliser une collection non typée, on doit spécifier le type du contenu via l'attibut targetEntity de l'annotation de membre @OneToMany. Dans notre exemple, cela donnerait pour la classe Livre :

@Entity
@Table(name = "LIVRE")
public class Livre {
    @OneToMany(targetEntity=Chapitre.class, mappedBy = "livre")
    private Collection chapitres;
    // ...
}

One To Many uni-directionnelle

Dans certain cas, on ne souhaite pas que le contenu ait connaissance de son contenant, que les brebis ne connaissent pas leur berger.

Cela se traduit par une table d'association (BERGER_BREBIS) côté base de données.

Pour ce qui est du mapping JPA, aucune information n'apparait dans la classe contenu (la brebis), tout est défini dans la classe contenant (le berger).

public class Berger {

    @OneToMany
    @JoinTable(name="BERGER_BREBIS",
        joinColumns=@JoinColumn(name="BERGER_ID"),
        inverseJoinColumns=@JoinColumn(name="BREBIS_ID"))'
    private Collection<Brebis> brebis;
}


Association class : Association entre deux classes, avec un état

On veut parfois créer une association entre deux entités, en donnant un état ou une pondération à cette association. On parle dans ce cas de classe d'association.

Prenons par exemple deux classes, Employe et Projet. On souhaite ajouter la notion de 'date d'arrivée d'un employé sur un projet. Il s'agit d'une association entre un Employe et un Projet avec un critère, la date d'arrivée.

@Entity
public class Employee {
    @Id private int id;
    
    @OneToMany(mappedBy="employee")
    private Collection<ProjectAssignment> assignments;
    
}

@Entity
public class Project {
    @Id private int id;
    
    @OneToMany(mappedBy="project")
    private Collection<ProjectAssignment> assignments;
    
}

@Entity
@Table(name="EMP_PROJECT")
public class ProjectAssignment {
    @Id private int id;
    
    @ManyToOne
    @JoinColumn(name="EMP_ID")
    private Employee employee;
    
    @ManyToOne
    @JoinColumn(name="PROJECT_ID")
    private Project project;
    
    @Temporal(TemporalType.DATE)
    @Column(name="START_DATE", updatable=false)
    private Date startDate;
    
}