avril282011

How to : JPA, Hibernate & Co - french version

2 commentaires 2 annexes

dao-small.pngIntroduction

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" :

generic-jpa-dao.jpg

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:

class-diagram-all-m

 

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

project-tree.jpg

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 :

sample-model.jpg

 

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!

 

 

.

Par Jérôme Guibert, dans Technique
2 commentaires

Envoyer à un ami 

2 annexes

2 commentaires

Jérôme Guibert

Jérôme Guibert

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

Geronimo

Geronimo

Le vendredi 29 juillet 2011, 18:34

Le code source de ce projet se trouve sur http://code.google.com/p/blog-intel...

>-)

Ajouter un commentaire

Identité

S'abonner pour recevoir les commentaires suivants par email

Texte

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

:) :(( :p X-( :x :-/ 0:) B-) &gt;-)