Hibernate Anti-Patterns

J’ai eu l’occasion d’assiter à une conférence sur les anti-patterns Hibernate par Patricia Wegrzynowicz à Devoxx hier. Elle a mis en avant certains effets de bords induit par l’utilisation d’Hibernate (les points suivants peuvent souvent s’appliquer à d’autres ORM, rien de personnel avec Hibernate :) . Cet article ne reprends qu’une partie du talk: les points que j’ai pu rencontrer en entreprise.

1. L’hydre

Hydra est une entité qui a la particularité de contenir une liste immuable de tête (heads.)

@Entity
public class Hydra {
  private Long id;
  private List heads = new ArrayList();
  @Id @GeneratedValue
  public Long getId() {...}
  protected void setId() {...}
  @OneToMany(cascade=CascadeType.ALL)
  public List getHeads() {
    return Collections.unmodifiableList(heads);
  }
  protected void setHeads(List heads) {...}
}
// creates and persists the hydra with 3 heads
// new EntityManager and new transaction
Hydra found = em.find(Hydra.class, hydra.getId());

La question est la suivante, combien d’appel sont fait en base de données lors de  la deuxième transaction (créer lors de em.find).

(a) 1 select
(b) 2 selects
(c) 1+3 selects
(d) 2 selects, 1 delete, 3
inserts
(e) None of the above

Pendant la recherche, em.find entraine un unique select en base de donnée sur l’hydre.
Pendant le commit qui est effectué à la fin de la transaction, hibernate vérifie que la collection n’est pas dirty, c’est à dire que les objets devraient être recréés en comparant les références objects des listes. Un deuxième select est alors effectué sur les têtes. Dans notre cas, les références ne correspondant pas, l’ensemble de la liste est alors recréé, ce qui explique le delete et les 3 inserts.

Contrairement à ce que l’on pourrait penser dans un premier temps, la bonne réponse est donc la réponse d.

Il faut donc être bien conscient que si on a un objet qui contient une collection et qui porte la liaison, si on affecte  une nouvelle liste à l’élément, la collection est recrée entièrement : un delete et n insertions d’éléments. On peut rencontrer également ce genre de problème si on utilise des outils qui suppriment les proxies hibernate sur les objets.

En régle générale, il vaut mieux travailler directement avec les collections retournées par hibernate à moins de savoir ce que l’on fait.

@Entity
public class Developer {
  @Id @GeneratedValue
  private Long id;
  private String mainTechnology;
  public boolean likesMainTechnology() {
    return "hibernate".equalsIgnoreCase(mainTechnology);
  }
}

// creates and persists a developer that uses hibernate as mainTechnology
// new EntityManager and new transaction
Developer dev = em.find(Developer.class, id);
boolean foundCoolStuff = false;
for (String tech : new String[]{"HTML5", "Android", "Scala"}) {
  dev.setMainTechnology(tech);
// othersAreUsingIt entraine select count(*) from Developer where mainTechnology = ? and id != ?
  if (othersAreUsingIt(tech, dev) && dev.likesMainTechnology()) {
    foundCoolStuff = true; break;
  }
}
if (!foundCoolStuff) {
// still use hibernate
  dev.setMainTechnology("hibernate");
}

(a) 2 selects
(b) 4 selects
(c) 4 selects, 1 update
(d) 4 selects, 4 inserts
(e) None of the above

La bonne réponse est la réponse d, 4 selects et 4 inserts. En effet, hibernate doit garantir la bonne valeur des requêtes exécutées et parfois doit effectuer une flush pendant une transaction. Si on n’effectue plus l’appel à othersAreUsingIt (qui entraine un select sur la table Developer), il n’y a plus d’update.

List semantics

@Entity
public class Forest {
  @Id @GeneratedValue
  private Long id;
  @OneToMany
  Collection<Tree> trees = new HashSet<Tree>();
  public void plantTree(Tree tree) {
    trees.add(tree);
  }
}
// creates and persists a forest with 10.000 trees

// new EntityManager and new transaction
Tree tree = new Tree(“oak”);
em.persist(tree);
Forest forest = em.find(Forest.class, id);
forest.plantTree(tree);

(a) 1 select, 2 inserts
(b) 2 selects, 2 inserts
(c) 2 selects, 1 delete,
10.000+2 inserts
(d) Even more 😉

La bonne réponse est la réponse c. La combinaison de l’annotation OneToMany et d’une collection entraine un bag semantic. La collection est donc recrée.

Semantic Java Type Annotation Add 1 element Update 1 element Remove 1 element
Bag Semantic java.utill.Collection
java.util.List
@ElementCollection
||
@OneToMany
||
@ManyToMany
1 delete + n insert 1 delete + n insert 1 update
Set Semantic java.utill.Set @ElementCollection
||
@OneToMany
||
@ManyToMany
1 insert 1 update 1 delete
List Semantic java.util.List (@ElementCollection
||
@OneToMany
||
@ManyToMany)&&(@OrderColumn||@IndexColumnn)
1 insert+ m update 1 delete + m insert 1 update

@OneToMany with no cascade options
La première intuition est de remplacer le Set par une List (List<Tree> trees = new ArrayList<Tree>() ). Néanmoins, cela marche exactement de la même manière.
Le seul moyen de ne pas avoir de bag semantic est d’utiliser orderColumn ou indexColumn

Il faut faire attention à choisir une collection appropriée sur la partie qui contient  la liaison. Ainsi dans notre cas, Set<Tree> trees = new HashSet<Tree>() permet d’éviter toutes les insertions parasites.

Utilisation d’un set sur l’object qui ne contient pas la liaison.

@Entity
public class Forest {
  @Id @GeneratedValue
  private Long id;
  @OneToMany (mappedBy = “forest”)
  Collection<Tree> trees = new HashSet<Tree>();
  public void plantTree(Tree tree) {
    trees.add(tree);
  }
}
@Entity
public class Tree {
  @Id @GeneratedValue
  private Long id;
  private String name;
  @ManyToOne
  Forest forest;
  public void setForest(Forest forest) {
    this.forest = forest;
     this.forest.plantTree(this);
  }
}

// creates and persists a forest with 10.000 trees
// new EntityManager and new transaction
em.remove(forest);

L’appel à em.remove entraine java.sql.BatchUpdateException : cannot delete or update a parent row : a foreign key constraint fails.

Si on garde le modèle, la seule solution est de parcourir l’ensemble des arbres de la forêt et de setter leur forêt à null.
Il est ensuite possible de supprimer la foret. Ce qui entraine 10 000 updates et 1 delete …
D’autres types de collections auraient été plus adéquats.

Il existe beaucoup d’autres anti-patterns. Pour les débusquer dans votre code, il est plus que recommander d’observer attentivement les requêtes ! Les slides sont ici : http://www.yonita.com/2011_11_16_PERFORMANCE_ANTIPATTERNS_DEVOXX.pdf

Leave a Reply