L'intégration de JPA en Spring se fait grâce au module Spring Data.
Par rapport à travailler avec Hibernate en JPA, on va obtenir plusieurs simplifications dans notre code:
Pas vraiment de nouveaux concepts par rapport à ce qu'on a vu précédemment.
Il faut inclure la dépendance Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Il faut un bean DataSource dans une classe de configuration qui décrit les paramètres d'accès à la BD pour JDBC.
@Bean
DataSource getDataSource(){
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("com.p6spy.engine.spy.P6SpyDriver");
dataSourceBuilder.url("jdbc:p6spy:h2:tcp://localhost/~/h2DB");
dataSourceBuilder.username("sa");
dataSourceBuilder.password("");
return dataSourceBuilder.build();
}
Les paramètres additionnels de configuration de Hibernate iront dans le fichier application.properties.
Les Repository sont codés automatiquement. Il suffit de déclarer leur interface.
@Entity
public class Pokemon {
@Id
@GeneratedValue
Long id;
String name;
int score;
}
@Repository
public interface PokemonRepository extends CrudRepository<Pokemon,Long> {
}
Spring va créer un bean du type PokemonRepository en codant toutes les méthodes de l'interface CrudRepository<Pokemon,Long> pour vous.
Spring construit les méthodes à partir de leur nom!
@Repository
public interface PokemonRepository extends CrudRepository<Pokemon,Long> {
Pokemon findByName(String name);
Set<Pokemon> findAllByNameOrderByScoreAsc(String name);
}
On peut aussi definir une méthode d'une requête JPQL.
@Query("SELECT p FROM Pokemon p WHERE p.score > :score")
Set<Pokemon> findPokemonsWithHighScores(int score);
La pratique consiste à écrire les méthodes complexes qui manipule des Pokemon
dans un classe PokemonService qui
va utiliser PokemonRepository.
@Service
public class PokemonService {
private final PokemonRepository pokemonRepository;
public PokemonService(PokemonRepository pokemonRepository){
this.pokemonRepository = pokemonRepository;
}
public void incrementWrong(Long id){
var pokemon = pokemonRepository.findById(id).orElseThrow();
pokemon.setScore(pokemon.getScore()+1);
pokemonRepository.update(pokemon);
}
}
Attention, cela crée deux transactions !
@Service
public class PokemonService {
private final PokemonRepository pokemonRepository;
...
@Transactional
public void incrementWrong(Long id){
var pokemon = pokemonRepository.findById(id).orElseThrow();
pokemon.setScore(pokemon.getScore()+1);
pokemonRepository.update(pokemon);
}
}
L'annotation @Transactional créer un transaction avant l'appel à incrementWrong
et faire un commit après l'appel.
Il y a bien une seule transaction!
@Transactional que des méthodes publiques.@Transactional, l'annotation de la seconde méthode est ignorée.
En effet, l'ajout des transactions se fait par un proxy.
@Service
public class PokemonService {
private final PokemonRepository pokemonRepository;
@PersistenceUnit
private final EntityManagerFactory emf;
@PersistenceContext
private final EntityManager em;
public PokemonService(PokemonRepository pokemonRepository,
@PersistenceUnit EntityManagerFactory emf,
@PersistenceContext EntityManager em){
this.pokemonRepository = pokemonRepository;
this.emf = emf;
this em = emf;
}
...
}
EntityManager est en fait un SharedEntityManager qui est récupérer à partir de la transaction courante.Si deux threads (i.e. deux visiteurs sur votre site) appellent au même moment la méthode incrementWrong, on peut obtenir un seul incrément en BD.
Il y a 3 leviers pour résoudre les problèmes de concurrence.
Dans notre cas particulier, on peut faire un UPDATE atomique.
Il existe plusieurs niveaux d'isolation dans le standard SQL.
@Transactional(isolation = Isolation.SERIALIZABLE)
public void incrementScoreWithSerialization(String name){
var pokemon = pokemonRepository.findByName(name);
pokemon.incrementScore();
pokemonRepository.save(pokemon);
}
Si la transaction n'a pas pu s'exécutée avec la sémantique demandée, une exception:
org.springframework.dao.CannotAcquireLockException
est levée.
Malheureusement H2 que nous utilisons pour les TPs n'implémente pas l'isolation au niveau des transactions. Pour tester, il vous faudra installer une autre BD.
@Transactional
public void incrementScoreWithPessimisticLock(String name) {
var pokemonToUpdate=em.find(Pokemon.class,name, LockModeType.PESSIMISTIC_WRITE);
pokemonToUpdate.setScore(pokemonToUpdate.getScore()+1);
}
Un lock est pris sur la BD qui empèche les modifications, suppressions, et lectures sur cette ligne.
Il existe d'autre locks dans la JPA.
On stocke en BD un numéro de version pour chaque enregistrement.
Quand l'ORM récupère un enregistrement, il récupère son numéro de version. Au moment de mettre à jour l'enregistrement en DB, il vérifie que le numéro de version est le même qu'au moment de la lecture.
Si ce n'est pas le cas, une exception:
org.springframework.orm.ObjectOptimisticLockingFailureException
est levée et la transaction et la transaction est rollback.
Si c'est le cas, il suffit de retenter la transaction.
@Entity
public class Pokemon {
@Id
@GeneratedValue
Long id;
String name;
int score;
@Version
Long version;
}
Il n'y a rien d'autre à faire à part gérer l'exception.
public void incrementScoreWithOptimisticLock(String name){
var retry=true;
while(retry) {
retry=false;
try {
incrementScore(name);
} catch (org.springframework.orm.ObjectOptimisticLockingFailureException e){
retry=true;
}
}
}
@Transactional
public void incrementScore(String name){
var pokemon = pokemonRepository.findByName(name);
pokemon.incrementScore();
pokemonRepository.save(pokemon);
}
Ce code ne marche pas, pourquoi ?
@Transactional
public void incrementScoreWithOptimisticLock(String name){
var retry=true;
while(retry) {
retry=false;
try {
incrementScore(name);
} catch (org.springframework.orm.ObjectOptimisticLockingFailureException e){
retry=true;
}
}
}
@Transactional
public void incrementScore(String name){
var pokemon = pokemonRepository.findByName(name);
pokemon.incrementScore();
pokemonRepository.save(pokemon);
}
Ce code ne marche pas, non plus, pourquoi ?
@Service
public class PokemonService{
@Autowired
PokemonServiceWithFailure pokemonServiceWithFailure;
...
public void incrementScoreWithOptimisticLock(String name){
var retry=true;
while(retry) {
retry=false;
try {
pokemonServiceWithFailure.incrementScoreWrong(name);
} catch (org.springframework.orm.ObjectOptimisticLockingFailureException e){
retry=true;
}
}
}
}
@Service
public class PokemonServiceWithFailure{
...
@Transactional
public void incrementScoreWrong(String name){
var pokemon = pokemonRepository.findByName(name);
pokemon.incrementScore();
pokemonRepository.save(pokemon);
}
}
Solution : mettre les deux méthodes dans des classes différentes.
On peut utiliser aussi Spring Retry mais c'est moins drôle!