How to : JPA, Hibernate & Co - french version
Introduction
Pour être directement opérationnel avec les systèmes de persistance, créer sa couche de données et d'accès en suivant les bonnes pratiques du moment, je vous propose dans un seul jar :
- une DAO générique,
- une gestion automatique des données temporelle,
- une instanciation de JPA/Hibernate/Springframework avec gestion des transactions,
- un configuration du model basé sur les annotations
- un exemple complet d'utilisation
Ce Billet fait suite à 'parcourir de gros volume de donnée en deux lignes de code'.
English version of this document
JPA-Utility, what else ?
Dans cette librairie, vous pourrez trouver des utilitaires destinés à nous simplifier l'usage du trio JPA/Hibernate/Spring, comme une DAO générique, des méthodes de parcours de gros volume de données efficace (le système de pagination est intégré), une base pour personnaliser les noms de vos tables en base de données, un mécanisme de datation (pas au carbone 14...) de vos entités, mais surtout un exemple complet et fonctionnel d'une couche de persistence à repomper litérallement pour que vous soyez opérationnel très rapidement.
Une DAO suffit, ... pour commencer
Vous pourrez trouver la classe "org.intelligentsia.utility.jpa.GenericJpaDao" :
C'est une classe paramétrée par le type d'entité gérée 'T' et le type de la clé primaire utilisée 'ID'. Avec ce paramètrage, elle expose :
- les méthodes classique : 'persist', 'delete'
- une méthode de sélection par clé primaire 'findById'.
- un ensemble de méthode 'findByNamedQuery' qui execute une requête, paramétrée ou non, avec un mécanisme de pagination (ou sans) qui vous permet de parcourir de gros volume de donnée avec une boucle 'for' (voir le billet Parcourir de gros volume de données en 2 lignes de code)
Un petit exemple d'utilisation classique :
...
@Autowired
private GenericJpaDao<page, Long> pageDao;
...
// create a new page
Page page = new Page();
page.setName("home page");
pageDao.persist(page);
...
Le parcours d'une collection se résumant à :
@Repository("siteDao")
@Scope(BeanDefinition.SCOPE_SINGLETON)
@Transactional(propagation=Propagation.SUPPORTS)
public class SiteDao extends GenericJpaDao<site, Long> {
public SiteDao() {
super();
}
@Transactional(readOnly = true)
public Iterable<site> findAll() {
return findByNamedQuery("selectAllSite", 10);
}
}
...
// For all site
for (Site site : siteDao.findAllSite()) {
// do something
}
...
Lorsque que vous commencer votre projet, je vous invite à ne pas créer de classe spécifique pour chaque entité et de vous forcer à utiliser une instanciation via Spring.
Vous pourrez par la suite, au fur et à mesure des besoins de vos services, rajouter uniquement les requêtes nécessaire sur vos entités.
A ce moment, vous rajouterez les méthodes ad hoc sur une DAO spécifique.
Easter Eggs, un aperçu
La librairie inclus aussi deux ou trois petites choses:
Entity Stamped
Tout d'abord l'interface "TimeStamped" et son compagnon le bean "TimeStamp" vont vous permettre de gérer sur vos entités les champs classiques "timestamp", "creationDate" et "lastModificationDate" de cette manière :
@Entity
@Table(name = "PAGE")
public class Page implements Serializable, TimeStamped{
...
/**
* Time stamp data embedded in this entity
*/
@Embedded
private TimeStamp timeStamp =" ""new" TimeStamp();
@Override
public TimeStamp getTimeStamp() {
return timeStamp;
}
...
Et c'est tout ! La classe GenericJpaDao va gérer le reste pour vous. Pour les curieux, ce mécanisme repose sur la déclaration d'une interface "TimeStamped", de son bean "TimeStamp" et d'un petit test dans l'implémentation de la méthode "persist" sur "GenericJpaDao".
/**
* if an entity implement this interface, their field 'TimeStamp' will be updated by provider.
* @author <a href="mailto:jguibert@intelligents-ia.com">Jerome Guibert</a>
* @version 1.0.0
*/
public interface TimeStamped {
public TimeStamp getTimeStamp();
}
/**
* TimeStamp offer an implementation for three common fields: timestamp, creationDate and lastModificationDate.
*
* @author <a href="mailto:jguibert@intelligents-ia.com">Jerome Guibert</a>
* @version 1.0.0
*/
public class TimeStamp implements Serializable {
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "TIMESTAMP")
protected Date timestamp;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "LASTMODIFICATION")
protected Date lastModificationDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATION")
protected Date creationDate;
public TimeStamp() {
super();
}
...
}
//
public class GenericJpaDao<t extends Serializable, ID extends Serializable> {
...
public T persist(T entity) {
// here the egg
if (TimeStamped.class.isAssignableFrom(entity.getClass())) {
TimeStamp stamp = ((TimeStamped) entity).getTimeStamp();
Date current = new Date();
if ((stamp.getCreationDate() == null)) {
stamp.setCreationDate(current);
}
stamp.setLastModificationDate(current);
stamp.setTimestamp(current);
}
entityManager.persist(entity);
return entity;
}
...
}
Réalisé à peu de frais, jusqu'à présent ce test n'a pas été remis en cause par ses performances. Si vous voulez rajouter d'autre fonctionnalité de ce type (tracer qui à fait quoi par exemple, ....) je vous invite à étendre la classe GenericJpaDao ou sinon me proposer l'extension (Je serais ravis de l'intégrer au projet).
Le DBA sera (de nouveau) votre meilleur ami (ou presque)
Trop longtemps maltraité, souvent relégué en bout de chaîne, ils peuvent aussi vous aider (si si, mais pas tous...).
Plutôt que faire comme si il n'existait pas, allez le voir en lui disant que :
- Vous allez créér un nouveau schéma (il sera content de l'apprendre au moins un jour avant la mise en production (vécu...) )
- Que vous pouvez normaliser le nom des tables que vous allez créer (préfixe, suffixe)
- Que vous pouvez nommer les tables 'comme y faut' (voir le "@Table(name = 'PAGE')" en début de chaque déclaration d'entité)
Et si, en plus vous lui montrer un ébauche du schéma qui sera créer plus tard (en local sur votre machine vous devriez avoir ce qu'il faut) ...
De cette façon, le jour (qui n'arriveras jamais ...) où vous serez en panne sèche sur une requête, que vos performances en lecture/écriture seront des plus lamentables, vous pourrez lui demander (très humblement) son aide (précieuse) ...
Pour fixer-le-prefixe-du-suffixe, voilà un petit exemple :
- Créez une classe étendant "CustomTableNamingStrategy" et definissez les prefix/suffix de vos tables.
/** * DevTableNamingStrategy add "TEST_" prefix on all TABLE. * * @author <a href="mailto:jguibert@intelligents-ia.com">Jérôme Guibert</a> * @version 1.0.0 */ public class DevTableNamingStrategy extends CustomTableNamingStrategy { private static final long serialVersionUID = -2544171811654641826L; public DevTableNamingStrategy() { super("TEST_", null); } } - Positionnez la valeur de la propriété "model.hibernate.namingStrategy" sur la bonne classe: "model.hibernate.namingStrategy=org.intelligentsia.utility.jpa.DevTableNamingStrategy" (Dans l'example fournit, elle va alimenter le paramètre "hibernate.ejb.naming_strategy").
Le parcours de gros volume de données
Je vous invite à lire le billet Parcourir de gros volume de données en 2 lignes de code. C'est la raison de l'existence des classes "ValueIterator" et "ValueHandler".
L'idée est de rendre transparent la pagination d'une requête retournant un (très) gros volume de données.
Utilisation et Intégration
Configurer votre POM
En deux étapes
- Ajouter le "repository" :
<repositories> <repository> <id>intelligents-ia</id> <name>Intelligents-ia Repository</name> <url>http://intelligents-ia.com/maven2</url> </repository> </repositories>
- Et la dépendance suivante :
<dependencies> <dependency> <groupId>org.intelligentsia.utility</groupId> <artifactId>jpa</artifactId> <version>1.0</version> </dependency> </dependencies>
Pour les curieux: Récupération du code source projet
Cela vous permettra aussi de copier-coller les fichiers ad hoc et de les personnaliser à votre couche de données.
svn checkout https://blog-intelligents-ia.googlecode.com/svn/trunk/jpa-utility jpa-utility --username yoursUserName mvn clean install
Le projet et l'exemple type
Vous pourrez voir que le test unitaire embarque un définission "complète" d'une couche d'accès aux données avec un premier service se basant dessus.
Voici le modèle de données à rendre persistant :
1. Créations des entitées et attributs
Pour l'entité site :
/**
* Our Site entity.
*
* @author <a href="mailto:jguibert@intelligents-ia.com">Jérôme Guibert</a>
* @version 1.0.0
*/
@Entity
@Table(name = "SITE")
@NamedQueries({
@NamedQuery(name = "selectAllSite", query = "SELECT s FROM Site s")
})
public class Site implements Serializable, TimeStamped {
/**
* serialVersionUID
*/
private static final long serialVersionUID = -1823543375371695270L;
/**
* Automatic version number
*/
@Version
protected Long version;
/**
* Identity with an automatic sequence
*/
@Id
@GeneratedValue(generator = "SEQ_SITE_ID")
@SequenceGenerator(name = "SEQ_SITE_ID", sequenceName = "SEQ_SITE_ID")
@Column(name = "SITE_ID")
private Long id;
/**
* Name of site with an index
*/
@Column(name = "NAME", length = 48, nullable = true)
@Index(name = "SITE_NAME_INDEX", columnNames = {
"NAME"
})
private String name;
...
/**
* all pages
*/
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "SITE_ID")
private List<page> pages = new ArrayList<page>();
/**
* Time stamp data embedded in this entity
*/
@Embedded
private TimeStamp timeStamp = new TimeStamp();
...
}
Pour l'entité Page :
/**
* a page included in our site entity
*
* @author <a href="mailto:jguibert@intelligents-ia.com">Jérôme Guibert</a>
* @version 1.0.0
*/
@Entity
@Table(name = "PAGE")
public class Page implements Serializable, TimeStamped{
private static final long serialVersionUID = -773904423928687747L;
/**
* Automatic version number
*/
@Version
protected Long version;
/**
* Identity with an automatic sequence
*/
@Id
@GeneratedValue(generator = "SEQ_PAGE_ID")
@SequenceGenerator(name = "SEQ_PAGE_ID", sequenceName = "SEQ_PAGE_ID")
@Column(name = "PAGE_ID")
private Long id;
/**
* Name of page
*/
@Column(name = "NAME", length = 48, nullable = false)
private String name;
/**
* Time stamp data embedded in this entity
*/
@Embedded
private TimeStamp timeStamp = new TimeStamp();
/**
* An example of many to many association
*/
@ManyToMany(cascade = {
CascadeType.PERSIST, CascadeType.MERGE
}, mappedBy = "pages", targetEntity = org.intelligentsia.utility.jpa.model.Tag.class)
private List<tag> tags = new ArrayList<tag>();
...
}
J'attire votre attention sur la déclaration du liens entre Site et Page en 1-N. Pour un exemple en N-N entre Page et Tag, voici un extrait de la class "Tag" :
...
/**
* a many to many example.
*/
@ManyToMany(targetEntity = org.intelligentsia.utility.jpa.model.Page.class, cascade =" ""{"
CascadeType.PERSIST, CascadeType.MERGE
})
@JoinTable(name = "TAG_PAGE", joinColumns = @JoinColumn(name = "TAG_ID"), inverseJoinColumns = @JoinColumn(name = "PAGE_ID"))
private List<page> pages = new ArrayList<page>();
...
2. Déclaration des entités et le fichier "my-persistence.xml"
<?xml version="1.0" encoding="UTF-8"?> <persistence version="1.0" 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"> <persistence-unit name="my-model" transaction-type="RESOURCE_LOCAL"> <description> Unite du projet </description> <class>org.intelligentsia.utility.jpa.model.Page</class> <class>org.intelligentsia.utility.jpa.model.Site</class> <class>org.intelligentsia.utility.jpa.model.Tag</class> <exclude-unlisted-classes /> </persistence-unit> </persistence>
Un peu fastidieux...
3. Une pincée de Spring
Première déclaration :La localisation du fichier de propriétées
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> // Here we specify where to find our property file <context:property-placeholder location="classpath:META-INF/model.properties" /> <context:annotation-config />
Ici, nous disons à Spring d'aller voir ce que contient le package "org.intelligentsia.utility.jpa" (et ses sous-package), et d'instancier tous ce qui bouge...
// Where are your service and your class model ? <context:component-scan base-package="org.intelligentsia.utility.jpa" />
La partie de plaisir : La déclaration de l'entity manager...
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- ENTITY MANAGER -->
<bean id="modelEntityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="my-model" />
<property name="persistenceXmlLocation" value="classpath:META-INF/my-persistence.xml" />
<property name="dataSource" ref="model-dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="${model.hibernate.dialect}" />
<property name="showSql" value="${model.hibernate.showsql}" />
</bean>
</property>
<property name="jpaPropertyMap">
<map>
<entry key="hibernate.dialect" value="${model.hibernate.dialect}" />
<entry key="hibernate.hbm2ddl.auto" value="${model.hibernate.hbm2ddl.auto}" />
<entry key="hibernate.format_sql" value="${model.hibernate.format_sql}" />
<entry key="hibernate.cache.use_second_level_cache" value="${model.hibernate.cache.use_second_level_cache}" />
<entry key="hibernate.cache.provider_class" value="${model.hibernate.cache.provider_class}" />
<entry key="hibernate.cache.use_query_cache" value="${model.hibernate.cache.use_query_cache}" />
<entry key="hibernate.cache.use_second_level_cache" value="${model.hibernate.cache.use_second_level_cache}" />
<entry key="hibernate.cache.use_structured_cache" value="${model.hibernate.cache.use_structured_cache}" />
<entry key="hibernate.ejb.naming_strategy" value="${model.hibernate.namingStrategy}" />
</map>
</property>
</bean>
<!-- TRANSACTION MANAGEMENT -->
<bean id="modelTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="modelEntityManagerFactory" />
</bean>
<tx:annotation-driven transaction-manager="modelTransactionManager" />
Et maintenant, nous pouvons instancier nos Dao, et definir en une ligne la connection à la base de données.
<!-- DAO DECLARATION --> <bean id="pageDao" class="org.intelligentsia.utility.jpa.GenericJpaDao"> <constructor-arg value="org.intelligentsia.utility.jpa.model.Page" /> </bean> <bean id="tagDao" class="org.intelligentsia.utility.jpa.GenericJpaDao"> <constructor-arg value="org.intelligentsia.utility.jpa.model.Tag" /> </bean> <bean id="model-dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.h2.Driver" /> <property name="url" value="jdbc:h2:~/test" /> </bean> </beans>
4. Le fichier de propriétés à personaliser selon vos goûts et couleur
model.hibernate.dialect=org.hibernate.dialect.H2Dialect model.hibernate.showsql=true # validate | update | create | create-drop model.hibernate.hbm2ddl.auto=update model.hibernate.format_sql=true model.hibernate.cache.use_second_level_cache=true model.hibernate.cache.provider_class=org.hibernate.cache.HashtableCacheProvider model.hibernate.cache.use_query_cache=true model.hibernate.cache.use_second_level_cache=true model.hibernate.cache.use_structured_cache=true #org.hibernate.cfg.DefaultNamingStrategy| org.intelligentsia.utility.jpa.DevTableNamingStrategy model.hibernate.namingStrategy=org.intelligentsia.utility.jpa.DevTableNamingStrategy
J'espère que cela pourra vous être util, si vous avez des questions, remarques, surtout n'hésitez pas!
.




2 commentaires Fil des commentaires de ce billet
Le lundi 23 mai 2011, 15:08
Voici un tutoriel très intéressant qui va notamment vous guider avec une approche "Database => génération de code" très intéressante.
Bien que ce tutoriel date de 2008, il reste un très bon exemple d'utilisation d'hibernate/Jpa/Spring et des wizards ( Hibernate Tools).
Bonne lecture
Le vendredi 29 juillet 2011, 18:34
Le code source de ce projet se trouve sur http://code.google.com/p/blog-intel...