Recevez les mises à jour gratuites du blog par Email : »» Garanti sans spam indésirable ««

Hibernate : Une façon générique de persister les Enum

de Mimie le 11 août 2010

Rubrique : Programmation

Enum

Une enum représente un type énuméré, c’est à dire un type qui n’accepte qu’un ensemble fini d’éléments. Ce nouveau type permet donc de créer simplement des énumérations qui, dans sa forme la plus basique, contient simplement la liste des valeurs possibles qu’elle peut prendre, ex :

public enum Color {
     WHITE, RED, BLUE
};

La plupart du temps ce type d’enum est persisté en stockant sa valeur ordinale (0 pour WHITE, 1 pour RED et 2 pour BLUE) ce qui ne me plait pas du tout, c’est un peu comme stocker l’index d’un élément dans une liste au lieu de l’identifiant de l’élément lui-même. La valeur de mon enum en base doit être définie quelque part et non devinée selon un pseudo-placement dans une classe.

Dans notre cas notre enum est un peu plus complexe : constructeur à plusieurs paramètres, méthodes, etc., le voici en détails :

public enum Color {
	WHITE (1, "White", 3), // => 1 : identifiant en base, "White" : son libellé, 3 : ordre des couleurs
	RED (2, "Red", 1),
	BLUE (3, "Blue", 2);
    private final Integer value;
    private final String label;
    private final Integer order;
    private Color(Integer value, String label, Integer order) {
        this.value = value;
        this.label = label;
        this.order = order;
    }
    public Integer value() { return value; }
    public String label() { return label; }
    public Integer order() { return order; }
    /**
	 * Récupère la couleur selon sa valeur.
	 * @param value
	 * @return
	 */
	public static Color fromValue(Integer value) {
		switch (value) {
		case 1: return WHITE;
		case 2: return RED;
		case 3: return BLUE;
		default: return null;
		}
	}
}

Nous souhaitons que l’objet Color soit persisté en base avec son attribut value et qu’il puisse être intégralement récupérer par le biais de cet attribut unique.
Les deux méthodes value() et fromValue() sont justement présentes pour réaliser ce mapping.

UserType

Ces deux méthodes vont nous permettre de définir un UserType Hibernate permettant d’automatiser le mécanisme de sauvegarde/récupération de l’enum et de pouvoir étendre ce principe à d’autres énumérations en un clin d’oeil.

La classe ci-après, qui sert à faire le lien entre nos emum et la base de données, a été récupéré sur le site officiel de la communauté jBoss : http://community.jboss.org/wiki/Java5EnumUserType

Ce nouveau type de valeurs est générique et utilise par défaut les méthodes value et fromValue de l’énumération, nous allons voir plus bas comment l’utiliser dans les fichiers de mapping Hibernate.

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.type.NullableType;
import org.hibernate.type.TypeFactory;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;
public class GenericEnumUserType implements UserType, ParameterizedType {
	private static final String DEFAULT_IDENTIFIER_METHOD_NAME = "value";
	private static final String DEFAULT_VALUE_OF_METHOD_NAME = "fromValue";
	private Class<? extends Enum> enumClass;
	private Class<?> identifierType;
	private Method identifierMethod;
	private Method valueOfMethod;
	private NullableType type;
	private int[] sqlTypes;
	public void setParameterValues(Properties parameters) {
		String enumClassName = parameters.getProperty("enumClass");
		try {
			enumClass = Class.forName(enumClassName).asSubclass(Enum.class);
		} catch (ClassNotFoundException cfne) {
			throw new HibernateException("Enum class not found", cfne);
		}
		String identifierMethodName = parameters.getProperty("identifierMethod", DEFAULT_IDENTIFIER_METHOD_NAME);
		try {
			identifierMethod = enumClass.getMethod(identifierMethodName, new Class[0]);
			identifierType = identifierMethod.getReturnType();
		} catch (Exception e) {
			throw new HibernateException("Failed to obtain identifier method", e);
		}
		type = (NullableType) TypeFactory.basic(identifierType.getName());
		if (type == null)
			throw new HibernateException("Unsupported identifier type " + identifierType.getName());
		sqlTypes = new int[] { type.sqlType() };
		String valueOfMethodName = parameters.getProperty("valueOfMethod", DEFAULT_VALUE_OF_METHOD_NAME);
		try {
			valueOfMethod = enumClass.getMethod(valueOfMethodName, new Class[] { identifierType });
		} catch (Exception e) {
			throw new HibernateException("Failed to obtain valueOf method", e);
		}
	}
	public Class returnedClass() {
		return enumClass;
	}
	public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
		Object identifier = type.get(rs, names[0]);
		if (rs.wasNull()) {
			return null;
		}
		try {
			Object obj = valueOfMethod.invoke(enumClass, new Object[] { identifier });
			return obj;
		} catch (Exception e) {
			throw new HibernateException("Exception while invoking valueOf method '" + valueOfMethod.getName() + "' of " + "enumeration class '"
					+ enumClass + "'", e);
		}
	}
	public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
		try {
			if (value == null) {
				st.setNull(index, type.sqlType());
			} else {
				Object identifier = identifierMethod.invoke(value, new Object[0]);
				type.set(st, identifier, index);
			}
		} catch (Exception e) {
			throw new HibernateException("Exception while invoking identifierMethod '" + identifierMethod.getName() + "' of " + "enumeration class '"
					+ enumClass + "'", e);
		}
	}
	public int[] sqlTypes() {
		return sqlTypes;
	}
	public Object assemble(Serializable cached, Object owner) throws HibernateException {
		return cached;
	}
	public Object deepCopy(Object value) throws HibernateException {
		return value;
	}
	public Serializable disassemble(Object value) throws HibernateException {
		return (Serializable) value;
	}
	public boolean equals(Object x, Object y) throws HibernateException {
		return x == y;
	}
	public int hashCode(Object x) throws HibernateException {
		return x.hashCode();
	}
	public boolean isMutable() {
		return false;
	}
	public Object replace(Object original, Object target, Object owner) throws HibernateException {
		return original;
	}
}

Mapping Hibernate

Le mapping Hibernate est tout simple, nous utilisons un typedef qui permet d’assigner un nom à notre type de valeur générique et nous permet aussi de lui donner une liste de paramètres par défaut.

Voici comment procéder si les méthodes utilisées sont value et fromValue (par défaut) :

<hibernate-mapping>
	<typedef name="colorEnum" class="package.de.la.classe.GenericEnumUserType">
		<param name="enumClass">package.de.l.enum.Color</param>
	</typedef>
	<class ...>
		...
		<property name="color" type="colorEnum">
         		<column name="color" not-null="true" />
		</property>
		...
	</class>
</hibernate-mapping>

Si les méthodes de votre enum sont nommées différemment (toInt et fromInt par exemple), voici comment procéder à la surcharge de ces méthodes :

<hibernate-mapping>
	<typedef name="colorEnum" class="package.de.la.classe.GenericEnumUserType">
		<param name="enumClass">package.de.l.enum.Color</param>
		<param name="identifierMethod">toInt</param>
		<param name="valueOfMethod">fromInt</param>
	</typedef>
	<class ...>
		...
		<property name="color" type="colorEnum">
         		<column name="color" not-null="true" />
		</property>
		...
	</class>
</hibernate-mapping>

Conclusion

Les champs de vos objets du modèle peuvent à présent être de type Enum sans vous soucier de la façon dont il sont sauvegardés et récupérés. Testé et approuvé.

Cet article a été écrit par :

– qui a déjà rédigé 123 posts sur Des Geeks et des lettres.

Passionné d'informatique et développeur JavaEE de métier, je me consacre principalement à écrire des billets sur les sujets du Web et de la programmation Web. Ce blog est un espace qui me permet de partager mes découvertes avec vous et me sert accessoirement de pense bête !

Contacter l'auteur

Jetez aussi un oeil sur :

{ 15 commentaires… à vous de vous exprimer ! }

1 Greg août 11, 2010 à 21 h 04 min

:D Testé et approuvé par Mimie himself

Répondre

2 TL août 13, 2010 à 21 h 07 min

Hello,
Bien vu, simple, efficace et super pratique !
Mais refactoring de mon appli en vue !
A la rentrée, on va dire.
Bravo !

TL.

Répondre

3 Mimie août 13, 2010 à 21 h 14 min

Super :) moi aussi j’ai du refondre mon code avec cette technique ^^

Répondre

4 Rodolphe septembre 17, 2010 à 19 h 02 min

Brillant !
Simple d’utilisation, efficace, LA solution pour un mapping propre de valeurs d’enum en base.

Répondre

5 Mimie septembre 17, 2010 à 19 h 10 min

Merci Rodolphe, c’est clair que c’est pratique et efficace :)

Répondre

6 ed novembre 19, 2010 à 10 h 58 min

Excellent ! Merci !

Répondre

7 Mimie décembre 16, 2010 à 22 h 58 min

Pour récupérer un set d’enums voici la façon de procéder :

[xml]
<typedef name="colorEnum" class="package.de.la.classe.GenericEnumUserType">
<param name="enumClass">package.de.l.enum.Color</param>
</typedef>

<class ….>
<set name="colors" table="MTG_CARDS_COLORS" lazy="false" fetch="join">
<key column="CARD"/>
<element type="colorEnum" column="COLOR"/>
</set>
</class>
[/xml]

Répondre

8 Mimie mars 11, 2011 à 11 h 40 min

La façon de faire avec les annotations est plutôt bien décrit à cette adresse : Java+5+Enums+Persistence+with+Hibernate.

Répondre

9 Paul novembre 15, 2011 à 18 h 45 min

Je ne comprends pas toute cette complexité. Pourquoi ne pas simplement utiliser le code suivant dans l’entité ? (si bien entendu les annotations sont disponibles)

@Enumerated(EnumType.STRING)
@Column(name = « gender », nullable = false, length = 6)
private Gender gender;

La longueur de la colonne correspond à la longueur de la plus longue valeur de l’enum.

Répondre

10 Mimie novembre 15, 2011 à 19 h 57 min

@Paul : peux-tu nous dire quelles sont les valeurs stockées dans ta base avec ce mécanisme ? personnellement je me refuse à stocker des chaînes de caractères pour ce genre d’infos, si c’est le cas ça ne m’irait donc pas.

Répondre

11 Paul novembre 15, 2011 à 22 h 34 min

Effectivement, ce sont des chaînes de caractères.
Toutefois, il est possible d’utiliser la variante @Enumerated(EnumType.ORDINAL) qui stocke sous forme d’entier.

Répondre

12 Mimie novembre 15, 2011 à 22 h 35 min

Oui mais comme je l’ai expliqué au début du billet, je me refuse aussi de stocker en base la valeur ordinale de l’enum, ça n’a pas de sens pour moi, si je change l’ordre des constantes dans ma classe, ma base devient incohérente, un non sens.

Répondre

13 Paul novembre 15, 2011 à 22 h 48 min

Tout à fait d’accord, c’est d’ailleurs pour ça que j’utilise le EnumType.STRING. :-)

En fait, ce que je ne comprends pas, c’est la raison pour laquelle tu veux à tout prix stocker un enum sous forme d’entier et non de chaîne ? Sûrement pour améliorer les performances des requêtes en BD ? Au final, les opérations supplémentaires engendrées par la méthode que tu proposes n’impliquent-ils pas un traitement par Hibernate plus long ?

=> Temps total (requête + traitement par Hibernate) le même pour les deux techniques ?

Ca n’est en aucun cas une question piège, j’essaie juste de savoir quels sont les avantages et inconvénients de chaque technique. ;-)

Répondre

14 Mimie novembre 15, 2011 à 22 h 56 min

Ce qu’il faut bien avoir en tête c’est que les temps de traitements Java (tous mécanismes Hibernate) sont négligeables comparés au temps d’exécution d’une requête SQL en base, requêter sur un entier est beaucoup plus efficace que de requêter sur un varchar (notamment avec des tris), c’est donc bien une question de performance.

Répondre

15 Paul novembre 15, 2011 à 23 h 00 min

Ok, merci pour l’explication. J’essaierai de garder ça en tête pour améliorer les performances de mes requêtes lorsque j’ai besoin de trier en fonction d’une valeur d’enum.

Répondre

Laissez un Commentaire

Article précédent:

Article suivant: