Ressource
https://github.com/Adrien-Courses/R605-TD-JPA-synchronize-bidirectional - voir la classe
test/CommandeTest
Dans les cas pratiques suivant, nous souhaitons comprendre l’utilité de la relation bidirectionnelle expliquée dans les sections OneToMany relation et MappedBy. Pour ce faire nous prenons le cas :
- d’une
Commande - qui est composée de plusieurs lignes
LigneDetail(i.e. un bon de commande est composé de plusieurs lignes représentant chacune des articles)
erDiagram COMMANDE { LONG id } LIGNE_DETAIL { LONG id LONG commande_id FK } COMMANDE ||--o{ LIGNE_DETAIL : contains
@Entity
public class Commande {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "commande", cascade = CascadeType.ALL)
private List<LigneDetail> ligneDetails = new ArrayList<LigneDetail>();
}
@Entity
public class LigneDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Commande commande;
}1. Supprimer une ligne d’une commande (NOT WORKING)
Premièrement, regardons ce qu’il se passe si nous ne synchronisons pas les deux côtés de la relation.
@Test
public void testRemoveLigneDetailNotWorking() {
// Récupérer la commande id=1
Commande commande = em.find(Commande.class, 1L);
// Récupérer une ligne associée à la commande (ici la première ligne)
LigneDetail ligneDetail = commande.getLigneDetails().get(0);
// NOT WORKING
em.remove(ligneDetail);
}Quelles requêtes vont être exécutées ?
[Hibernate] select c1_0.id from Commande c1_0 where c1_0.id=? [Hibernate] select ld1_0.commande_id, ld1_0.id from LigneDetail ld1_0 where ld1_0.commande_id=?Aucun delete !
2. Non suffisant
@Test
public void testRemoveLigneDetailInsuffisant() {
Commande commande = em.find(Commande.class, 1L);
// Récupérer une ligne associée à la commande (ici la première ligne)
LigneDetail ligneDetail = commande.getLigneDetails().get(0);
ligneDetail.setCommande(null) // THIS ADDED
// NOT WORKING
em.remove(ligneDetail)
}Quelles requêtes vont être exécutées ?
[Hibernate] select c1_0.id from Commande c1_0 where c1_0.id=? [Hibernate] select ld1_0.commande_id, ld1_0.id from LigneDetail ld1_0 where ld1_0.commande_id=? [Hibernate] update LigneDetail set commande_id=? (à null) where id=?La
ligneDetailn’a pas été supprimée mais seulement update avec la FKcommande_idà NULL. Ainsi, même si nous n’avons plus la relation vers la commande mais nous avons une ligneDetail orpheline …mysql> select * from LigneDetail; +-------------+----+ | commande_id | id | +-------------+----+ | NULL | 1 | -- LigneDetail orpheline +-------------+----+
3. Supprimer une ligne d’une commande (WORKING)
Dans le code precedent, la ligne était toujours associée à la commande. Nous devons donc supprimer également cette association
@Test
public void testRemoveLigneDetailWorking() {
Commande commande = em.find(Commande.class, 1L);
LigneDetail ligneDetail = commande.getLigneDetails().get(0);
commande.getLigneDetails().remove(ligneDetail); // ADD THIS
em.remove(ligneDetail)}Quelles requêtes vont être exécutées ?
[Hibernate] select c1_0.id from Commande c1_0 where c1_0.id=? [Hibernate] select ld1_0.commande_id, ld1_0.id from LigneDetail ld1_0 where ld1_0.commande_id=? [Hibernate] delete from LigneDetail where id=?Nous avons bien un delete !
4. Supprimer une ligne d’une commande BIDIRECTIONAL
Mais, ceci n’est pas parfait
By using the bidirectional add sync methods, we can ensure that the persist entity state transition is going to be propagated properly. Without synchronizing both sides of the JPA association, it’s not guaranteed that the entity state will be properly synchronized with the database. source
@Test
public void testRemoveLigneDetailWorkingConventional() {
Commande commande = em.find(Commande.class, 1L);
LigneDetail ligneDetail = commande.getLigneDetails().get(0);
commande.getLigneDetails().remove(ligneDetail); // ADD THIS
ligneDetail.setCommande(null); // ADD THIS
em.remove(ligneDetail);
}Quelles requêtes vont être exécutées ?
[Hibernate] select c1_0.id from Commande c1_0 where c1_0.id=? [Hibernate] select ld1_0.commande_id, ld1_0.id from LigneDetail ld1_0 where ld1_0.commande_id=? [Hibernate] delete from LigneDetail where id=?Nous avons bien un delete !
On peut donc se demander si l’option 3. avec seulement la ligne
commande.getLigneDetails().remove(ligneDetail);est suffisante.
- En précisant les deux côtés de la relation nous nous assurons de la consistence des données (sans il se pourrait que des problème de synchronization en base de données se produisent)
- Donc par convention nous synchronisons les deux côtés
5. Amélioration
Ressource
Voir également MappedBy et copie défensive
Comment s’assurer que les développeurs n’oublie pas de synchroniser les deux côté de la relation ?
Dans la classe Commande il est recommandé d’ajouter les méthodes suivante qui viennent simplement remplacer dans nos exemple les deux appels
commande.getLigneDetails().remove(ligneDetail); // ADD THIS
ligneDetail.setCommande(null); // ADD THISpublic class Commande {
public void addLigneDetails(LigneDetail ligneDetail) {
ligneDetail.setCommande(this); // this référence la commande
igneDetails.add(ligneDetail);
}
public void removeLigneDetails(LigneDetail ligneDetail) {
ligneDetail.setCommande(null);
ligneDetails.remove(ligneDetail);
}
}Maintenant, plus aucune excuse pour les développeurs ils n’ont qu’à utiliser ces deux méthodes !
@Test
public void improvement() {
Commande commande = em.find(Commande.class, 1L);
LigneDetail ligneDetail = commande.getLigneDetails().get(0);
// commande.getLigneDetails().remove(ligneDetail);
// ligneDetail.setCommande(null);
// DEVIENT
commande.removeLigneDetails(ligneDetail)
em.persist(commande); // optionnelle car commande est un objet MANAGED
}