Crédit photo : Groum – Licence Commons Creatives 2.0
Quelles sont les bonnes pratiques concernant les méthodes hashCode() et equals() pour les Entités JPA ?
Lorsque vous stockez des objets mappés avec un ORM comme Hibernate, dans des collections, il est intéressant de regarder comment implémenter correctement les méthodes equals() et hashCode(). C’est aussi une approche à comprendre en général dès lors qu’il devient nécessaire de stocker un objet dans une collection.
The general contract is: if you want to store an object in a List, Map or a Set then it is an requirement that equals and hashCode are implemented so they obey the standard contract as specified in the documentation.
L’approche Domain Driven Design donne un nom à ces objets, qui portent une clé d’identification unique… ce sont des Entities. Nous faisons en fait la différence entre les Values Objets dont l’identité est basée sur les valeurs des attributs des champs et les Entities, dont l’identité est déterminée par un ou plusieurs attributs.
Un Value Object
Un Value Object sera immuable. Ce n’est pas obligatoire, mais logique. S’il était possible de changer la valeur de l’un de ses attributs, ce serait alors un autre Value Object.
Prenez l’adresse postale où vous habitez et imaginez sa modélisation. Constituée d’un numéro, d’un nom de rue, d’un code postal et d’une ville, c’est bien l’ensemble de ces 4 attributs qui fait l’identité de l’objet. Si je change de ville, ce n’est plus la même adresse.
public class Address { private Integer number; private String streetName; private Integer zip; private String city; public Address(Integer number, String streetName, Integer zip, String city) { this.number = number; this.streetName = streetName; this.zip = zip; this.city = city; } public Integer getNumber() { return number; } public String getStreetName() { return streetName; } public Integer getZip() { return zip; } public String getCity() { return city; } }
En conséquence il est logique et normal d’utiliser le générateur de votre IDE pour écrire les méthodes equals et hashCode :
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Address address = (Address) o; if (city != null ? !city.equals(address.city) : address.city != null) return false; if (number != null ? !number.equals(address.number) : address.number != null) return false; if (streetName != null ? !streetName.equals(address.streetName) : address.streetName != null) return false; if (zip != null ? !zip.equals(address.zip) : address.zip != null) return false; return true; } @Override public int hashCode() { int result = number != null ? number.hashCode() : 0; result = 31 * result + (streetName != null ? streetName.hashCode() : 0); result = 31 * result + (zip != null ? zip.hashCode() : 0); result = 31 * result + (city != null ? city.hashCode() : 0); return result; }
Entity
Une entité, et là je me rapproche maintenant de JPA, est un objet dont l’identité est déterminée par une ou plusieurs attributs. Prenons le cas simple où je n’ai qu’un Id de type Long sur une Entité personne. Votre base de donnée a déjà une contrainte et une intégrité, qui fixe l’unicité d’un objet avec la PRIMARY_KEY. C’est ce que l’on retrouve dans votre domaine, sur votre Entité.
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Person { @Id @GeneratedValue private Long id; private String firstName; private String lastName; public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Long getId() { return id; } }
Notez qu’il n’y a pas de méthode setId, puisque nous laissons JPA se charger de l’affectation de l’id technique de notre classe Person.
En conséquence, il est alors possible de définir de manière différente les méthodes equals() et hashCode() afin de n’utiliser que l’ID technique. Qu’y gagne-t-on ? Un code plus léger ? Oui mais ce n’est pas l’intérêt principal de cette approche. C’est aussi le respect du contrat de l’identité, vu sous l’approche Domain Driven Design.
Voici une première implémentation, qui n’est pas complète, afin de vous montrer le principe :
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; if (!id.equals(person.id)) return false; return true; } @Override public int hashCode() { return id.hashCode(); }
La critique de ce premier script : je pars du principe qu’id n’est jamais null. Or c’est faux, à la création de l’objet par exemple. L’id n’est pas encore affecté par JPA. Il y a donc un souci.
Nous pouvons contourner ce problème en utilisant le VMID dont RMI se sert depuis java 1.4.2. D’après la Javadoc, le VMID est unique :
A VMID is a identifier that is unique across all Java virtual machines. VMIDs are used by the distributed garbage collector to identify client VMs.
A priori donc, nous n’aurons pas de soucis avec la JVM.
Il ne reste plus qu’à réécrire le equals/hashCode afin de se baser sur cet ID lorsque l’id technique est null :
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; if (id == null) { if (person.uidInEquals == null) { return false; } if (uidInEquals == null) { uidInEquals = new java.rmi.dgc.VMID(); } return (person.uidInEquals.equals(uidInEquals)); } return id.equals(person.id); } @Override public int hashCode() { if(id==null){ if(uidInEquals==null){ uidInEquals = new java.rmi.dgc.VMID(); } return uidInEquals.hashCode(); } return id.hashCode(); } private java.rmi.dgc.VMID uidInEquals = null; ... ...
Ce code est optimisable, et je l’ai vu la première fois lorsque j’avais testé le logiciel Celerio. Le site d’Hibernate présente avec plus de détails une solution complète et optimisée sur cette page.
L’approche clé métier
Une dernière approche est de ne pas utiliser l’id technique du tout mais de se baser sur la valeur des champs. C’est intéressant, et c’est un mix entre la notion de Value Object et la notion d’Entity. Si par exemple nous pensons que le prénom et le nom sont assez discriminant dans notre application, rien ne nous empêche d’écrire ce code :
@Entity public class Person { @Id @GeneratedValue public Long id; private String firstName; private String lastName; public Person(final String firstName, final String lastName) { if(lastName==null){ throw new IllegalArgumentException("lastname cannot be null"); } if(firstName==null){ throw new IllegalArgumentException("firstname cannot be null"); } this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; if (!firstName.equals(person.firstName)) return false; if (!lastName.equals(person.lastName)) return false; return true; } @Override public int hashCode() { int result = firstName.hashCode(); result = 31 * result + lastName.hashCode(); return result; } }
Si vous regardez attentivement ce code, vous verrez que j’ai retiré les setters, et que j’ai renforcé mon constructeur afin de rendre obligatoire le nom de famille et le prénom.
Pour calculer l’égalité de l’objet ou son hashCode, il est alors possible de se baser sur les valeurs des attributs… la définition même d’un Value Object.
Qu’en pensez-vous ?
Références
A lire absolument : equals and hashCode in Hibernate
On n’oublie surtout pas le constructeur vide qui reste obligatoire pour les entités JPA 🙂
hello,
pour ma part, je me base exclusivement sur l’approche ‘clé métier’, même si parfois, ce n’est pas évident du tout.
Pour rebondir sur ‘Value Object’, il y a peut être mélange de termes.
Dans le monde JPA, un objet est Entité ou valeur (aka Embeddable, Component), ‘Value Object’ a l’image forte de l’utilisation que tu décris qui est trop restrictive.
Pour moi on peut tout à fait changer certaines valeurs d’attributs d’un Embeddable. Les caractéristiques fortes d’un Embeddable sont son lien fort (de composition en UML) entre lui et l’entité à laquelle il appartient, les cycles de vie sont liés ainsi qu’un embeddable ne peut être associé à 2 entités différentes.
« Si vous regardez attentivement ce code, vous verrez que j’ai retiré les setters, et que j’ai renforcé mon constructeur afin de rendre obligatoire le nom de famille et le prénom. »
Tout à fait, c’est ce à quoi servent les constructeurs. Combien de fois on a pu me demander ‘comment je fais pour rendre des attributs obligatoires?’ et après on dit que java est compliqué lol
Je me permet juste de rectifier une petite phrase : « Une dernière approche est de ne pas utiliser l’id technique du tout mais de se baser sur la valeur des champs. C’est intéressant, et c’est un mix entre la notion de Value Object et la notion d’Entity. »
L’entité n’en est pas moins une. Ce n’est pas un «mix», et même plutôt une recommandation de DDD, d’utiliser en priorité un ou plusieurs champs de l’entité quand cela est possible. Un identifiant est généré par le système quand on n’a malheureusement pas ce choix. L’approche est alors exactement la même dans les deux cas à mon avis au delta de l’attribution de l’identifiant par l’ORM et de la nullité que tu soulèves. Ton approche est séduisante mais j’avoue ne jamais avoir eu besoin de comparer deux entités avant que celles-ci soient ajoutées à l’entrepôt.
Je préconise également l’approche « clé métier » dès que possible.
Par contre, je pense déceler une erreur dans ta présentation de l’approche basée sur les VMID. Normalement la valeur d’un hashcode d’un objet ne doit pas changer durant la vie de l’objet or si je comprends bien ton code : dès qu’un objet sera « flushé » alors l’id sera valorisé et donc le hashcode pourra s’appuyer dessus (alors que jusque là il utilisait le VMID). D’ailleurs le code généré par Celerio tient compte de ce phénomène et c’est pourquoi le code des equals et hashcode est aussi compliqué 😉
D’accord sur le principe, en particulier sur la clé métier.
Mais l’implémentation eclipse est illisible !
Et commons-lang ?
http://commons.apache.org/lang/api/org/apache/commons/lang/builder/EqualsBuilder.html
http://commons.apache.org/lang/api/org/apache/commons/lang/builder/HashCodeBuilder.html
http://commonclipse.sourceforge.net/download.html
Et les annotations ?
http://community.jboss.org/wiki/AnnotationdrivenequalsandhashCode
http://pojomatic.sourceforge.net/pojomatic/index.html
+1000 avec la remarque de @mOuLi.
Et j’ajouterais un exemple concret pour illustrer pourquoi un hashCode qui change au cours de la durée de vie peut poser de gros problèmes.
Dans le cas d’une relation Parent-Enfants où les enfants sont stockés dans un Set il y a un risque de ne plus pouvoir retrouver ses « petits » si ils sont placés dans le Set avant d’être persistés :
1-Création d’une nouvelle instance d’un enfant et stockage dans le Set papa.getEnfants()
=>le Set met à jour sa table de hashage avec le hashCode de l’enfant basé sur le VMID
2-Persistence de l’enfant
=>Changement de hashCode : on se retrouve dans le cas de la rupture du contrat (défini dans la doc) et la table de hashage du Set ne correspond plus aux hashCode des objets contenus.
Conséquences : Les méthodes contains(Object) et remove(Object) ne fonctionneront plus et renverront toujours FALSE même si on passe une instance dont on est sûr qu’elle se trouve dans le Set (faux négatif). Je vous dit pas les séances de DEBUG qui s’en suivent…
Mes 2 cts
Il y a aussi deux bonnes pratiques :
– utiliser les getters
– utiliser instanceof au lieu de getClass
Raison : les proxies, les proxies et les proxies
Références :
– Effective Java
– http://blog.xebia.com/2008/03/08/advanced-hibernate-proxy-pitfalls/
– http://cwmaier.blogspot.com/2007/07/liskov-substitution-principle-equals.html
Problématique très intéressante.
Parmi les classes utilitaires pour simplifier les choses (dans le cas des values objects en tout cas), il y avait aussi les propositions pour Java 7 :
cf : Nouvelle classe utilitaire java.util.Objects en java 7
Et je plussoie Thomas Queste sur la référence à Effective Java.
A noter que si l’on parle d’entités et que ces entités ne sont manipulées que dans le cadre d’un seul contexte de persistence, il vaut mieux laisser equals / hashCode non implementé. En effet le contexte de persistence garantie que deux entites sont egales si et seulement si elles sont ==
La ou ce modele casse, c’est quand on détache et reattache/merge les entites. D’où l’intérêt des sessions longues ou conversations, d’où l’interet de Seam 🙂
Arrêtez moi si je dis une connerie (ça m’arrive ces temps si à cause du champagne), mais avec ton hashcode() basé sur VMID, lors de l’affectation de l’ID par un PERSIST, le hascode change. Donc
Entity monEntity = new Entity();
uneHashMap( monEntity ); // le hashcode est utilisé comme clé dans la map
…
em.persist( monEntity );
…
uneHashMap.contains( monEntity ) => false ?
gênant non ?
Ou alors, il faut retourner en priorité le VMID s’il a déjà été affecté