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!