lundi 19 novembre 2012

Java Serveur App 10 : écriture et transaction JPA


Écrire en base de données

Écrire en base de données est crucial pour n'importe quelle application puisque c'est à ce moment que votre application se souvient, stocke de l'information et c'est cette information qui donne sa valeur à votre application. 

Pour écrire en base de données, l'outil principal fourni par la spécification JPA est l'EntityManager qui fournit deux éléments très importants:

  • Les méthodes de base que sont persist() et merge().
  • La possibilité d'ouvrir et de soumettre une transaction.



Transaction avec erreur

Une transaction permet d'encapsuler plusieurs petites opérations en un bloc indivisible. Le but est de pouvoir défaire l'ensemble si une opération échoue et ainsi éviter d'avoir des objets persistés dans un état incohérent. 

Notre exemple est le suivant: pour chaque plat du système, on veut créer une variante "XL" qui coutera 50 cents de plus et aura les mêmes ingrédients.

public static void transactionAvecErreur(){
 // recuperer l'EntityManager
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 EntityTransaction transaction  = em.getTransaction();
 // transaction avec attrapage des erreurs
 try{
  transaction.begin();
  // aller chercher tous les plats
  String query = "SELECT f FROM FoodItem f";
  Query q = em.createQuery(query);
  List<FoodItem> dishes = q.getResultList();
  // pour chaque plat
  for (FoodItem dish : dishes){
   // dupliquer chaque plat
   FoodItem xl = new FoodItem();
   // ajouter 50 cents au prix du duplicata
   xl.setPrice(dish.getPrice()+0.5);
   // ajouter " XL " à la description du plat dupliqué
   xl.setName(dish.getName()+" XL");
   // stocker en BD
   em.persist(xl);
  }
  transaction.commit();
 }catch(Throwable thrown){// si exception, le rollback est implicite
  thrown.printStackTrace();
 }
}

Le problème vient ici du champ "description" de l'objet FoodItem qui ne peut pas être null en base de données. En effet, une des causes d'échec d'une écriture en BD est le plus souvent le non respect des contraintes. (ici le champ description a reçu l'annotation @Column(nullable = false) qui indique que la valeur null n'est pas acceptée)

Transaction sans erreur

Nous montrons ici la même transaction avec la correction de l'erreur, ici la description est bien là. Nous voyons aussi comment ajouter le plat à la liste de plats du restaurant.

public static void transactionSansErreur(){
 // recuperer l'EntityManager
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 EntityTransaction transaction  = em.getTransaction();
 // transaction avec attrapage des erreurs
 try{
  transaction.begin();
  // aller chercher tous les plats
  String query = "SELECT f FROM FoodItem f";
  Query q = em.createQuery(query);
  List<FoodItem> dishes = q.getResultList();
  // pour chaque plat
  for (FoodItem dish : dishes){
   // dupliquer chaque plat
   FoodItem xl = new FoodItem();
   // ajouter 50 cents au prix du duplication
   xl.setPrice(dish.getPrice()+0.5);
   // ajouter " XL " à la description du plat dupliqué
   xl.setName(dish.getName()+" XL");
   // ATTENTION NE PAS OUBLIER LA DESCRIPTION !!!!!!!!!!!
   xl.setDescription(dish.getDescription());
   // mettre à jour les relations
   Restaurant r = dish.getRestaurant();
   xl.setRestaurant(r);
   r.getFoodItems().add(xl);
  }
  transaction.commit();
 }catch(Throwable thrown){
  thrown.printStackTrace();
 }
}

Écriture d'une commande

Pour notre application, une des transactions nécessaires est l'inscription d'une commande. Cette inscription nécessite d'écrire la commande mais également de rajouter cette commande au champ "baskets" de chaque plat. Nous devons également inscrire la date de création de la commande au moment de son inscription en base de données.


public static void inscrireUneCommande(Basket b){
 // recuperer l'EntityManager
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 EntityTransaction transaction  = em.getTransaction();
 try{
  transaction.begin();
  b.setCreationDate(new Date());
  em.persist(b);
  em.flush();
  // mise à jour de la relation pour chaque plat
  for (FoodItem item : b.getFoodItems()){
   // ajout de l'élément dans la liste des paniers auxquels appartient le plat
   item.getBaskets().add(b);
   // prise en compte des changements dans l'objet Java.
   em.merge(item);
  }
  transaction.commit();
 }catch(Throwable thrown){
  thrown.printStackTrace();
 }
}

EntityManager, flush et clear

Les objets géré par un EntityManager et la BD ne sont pas toujours exactement synchronisés suite à une écriture en BD. Nous allons voir les principales causes et les solutions:

  • Exécution retardée: quand on demande un ensemble d'opérations de persistence, elles ne sont pas toujours exécutées immédiatement ou de manière synchrone. Autrement dit, quand on crée un objet, notre EntityManager va souvent continuer à effectuer d'autres opérations pour ne pas attendre la BD. Parfois, vous aurez besoin de forcer votre EntityManager à effectuer une opération pour que la suite puisse se faire. Dans ce cas, la méthode flush() de l'EntityManager permet de forcer l'exécution des opérations en attente.
  • Objet non relâché: un EntityManager gère des objets en lien avec la BD. Il arrive qu'un EntityManager X rapporte un objet A en mémoire. Si un autre EntityManager Y effectue des modifications sur l'objet A, il se peut que X ne les voient pas même si vous effectuez un nouveau find. La raison est que X maintient un cache et ne recherche pas en BD. Ici deux solutions: i) utiliser la méthode clear() sur X afin de vider tous les objets gérés ce qui forcera X à accéder à la BD à nouveau ii) utiliser un EntityManager neuf pour chaque opération.

Une des questions importante est le nombre d'EntityManager et leur possible réutilisation. La réutilisation d'un EntityManager est possible mais elle peut mener à des situations difficiles à démêler pour un programmeur débutant.

Mon conseil pour apprendre est d'acquérir un nouvel EntityManager pour chaque transaction/opération que vous effectuez en BD. (Vous trouverez dans le projet une nouvelle facade qui illustre cette approche)


Gestion des exceptions commit et rollback implicite

JPA nous fournit une solution simple pour gérer les rollbacks. Si une exception est levée pendant l'exécution d'une transaction, il va effectuer un rollback implicite. Il reste possible d'intercepter l'exception avec un bloc try catch Java pour réagir en conséquence.


Fichiers du projet

Vous trouverez le projet avec les deux exemples de transaction et la transaction permettant d'écrire une commande ici. Plusieurs changements ont été faits dans plusieurs fichiers non décrits dans le blog. Les transactions se trouvent dans une classe appelée Transactions.


Lectures d'approfondissement

Il est intéressant de regarder plus en profondeur les problèmes de désynchronisation entre les objets en mémoire et les objets qui persistent, pour cela deux références ici et ici.

Aucun commentaire:

Enregistrer un commentaire