Gestion des transactions Spring JPA
Comment gérer les transactions quand on utilise Spring et JPA ?
Ce n'est pas bien compliqué.... ou presque.
Il nous faut tout d'abord un projet avec des objets persités en base de données, quelques dao et les services d'interrogation ad hoc. Pour cela je vous propose de repartir du projet java de ce billet : "How to : JPA, Hibernate & Co".
Si vous avez déjà lu ce billet, vous pouvez passer directement au chapitre suivant.
Presentation du projet
Voici le modèle de données de ce projet :
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>();
...
}
2. Configuration du 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>
3. Configuration Spring
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 />
Instanciation des objets du package "org.intelligentsia.utility.jpa" (et ses sous-package)
// Where are your service and your class model ? <context:component-scan base-package="org.intelligentsia.utility.jpa" />
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" />
Instanciation de nos Dao, et definission de 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
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
Gestion des transactions
Transaction Manager
Apres avoir déclarée notre "entity manager factory", nous avons besoin d'un gestionnaire de transaction. Cela ce déclare comme suit :
<!-- TRANSACTION MANAGEMENT --> <bean id="modelTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="modelEntityManagerFactory" /> </bean> <tx:annotation-driven transaction-manager="modelTransactionManager" />
Nous spécifions à Spring que nous allons configurer les transaction via des annotations.
Déclaration des services transactionels
La classe "GenericJpaDao" mère de nos autres DAO, ne déclare pas d'attribut de transaction. Ceci vous oblige à spécifier où et comment une transactions doit commencer.
Ma méthode est la suivante :
- Ajout du support des transactions (en lecture seulement) au service
- Ajout du support des transactions en écriture pour les méthodes qui en ont besoin
Concrétement pour impleménter le service :
public interface BlogService {
public void createBlog(final String name);
public Iterable<Site> findAllSite();
}
Cela donne :
@Service("blogService")
@Scope(BeanDefinition.SCOPE_SINGLETON)
@Transactional(readOnly = true, propagation = Propagation.REQUIRED)
public class BlogServiceImpl implements BlogService {
@Autowired
private SiteDao siteDao;
@Autowired
private GenericJpaDao<Page, Long> pageDao;
@Autowired
private GenericJpaDao<Tag, Long> tagDao;
@Override
@Transactional(readOnly = false)
public void createBlog(final String name) {
/**
* Create a new site
*/
Site site = new Site();
site.setName(name);
siteDao.persist(site);
/**
* Add a default page
*/
Page page = new Page();
page.setName("home page");
pageDao.persist(page);
// associate
site.getPages().add(page);
siteDao.persist(site);
}
@Override
public Iterable<Site> findAllSite() {
return siteDao.findAll();
}
}
La méthode "findAllSite" est bien en lecture seule, alors que "createBlog" doit pouvoir supporter l'écriture
Et c'est tout!
Bien évidement, pour aller un peu plus loin dans la configuration et la gestion des transactions au sein de Spring, je vous invite à consulter la documentation à ce sujet.

un commentaire Fil des commentaires de ce billet
Le lundi 5 décembre 2011, 21:05
Un des points clefs sur l'implémentation du service est la déclaration de " Propagation.REQUIRED" qui indique à spring d'utiliser une transaction existante si elle existe OU d'en créer une si il n'y en a pas encore.