Utilisation
MyBatis Plus propose deux mécanismes pour mapper automatiquement les énumérations :
Méthode 1 : Annotation Utilisez l'annotation @EnumValue sur le champ de l'énumération pour indiquer la valeur stockée en base de données.
@Getter
@AllArgsConstructor
public enum GradeEnum {
PRIMARY(1, "École primaire"),
SECONDARY(2, "Collège"),
HIGH(3, "Lycée");
@EnumValue // Spécifie que la valeur stockée est le code
private final int code;
// Autres champs...
}
Méthode 2 : Implémentation d'une interface Implémentez l'interface IEnum<T>, en redéfinissant la méthode getValue() pour définir la valeur persistée.
@Getter
@AllArgsConstructor
public enum AgeEnum implements IEnum<Integer> {
ONE(1, "Un an"),
TWO(2, "Deux ans"),
THREE(3, "Trois ans");
private final int value;
private final String description;
@Override
public Integer getValue() {
return this.value;
}
}
Si aucune de ces deux approches n’est utilisée, le mapping repose sur le gestionnaire par défaut configuré dans MyBatis : defaultEnumTypeHandler.
Principe interne
TypeHandler associé aux énumérations
Recommandé : Comprendre le mapping des énumérations dans MyBatis
Lorsqu’un TypeHandler est recherché pour un type énuméré, MyBatis suit cette séquence :
- Vérifie s’il existe un TypeHandler spécifique enregistré pour ce type.
- Si le type est une énumération, il cherche un handler correspondant à une interface implémentée.
- En l’absence de solution, utilise le handler par défaut.
Le point clé se situe dans la méthode getJdbcHandlerMap de TypeHandlerRegistry :
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) {
return null;
}
if (jdbcHandlerMap == null && type instanceof Class) {
Class<?> clazz = (Class<?>) type;
if (clazz.isEnum()) {
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(clazz, clazz);
if (jdbcHandlerMap == null) {
register(clazz, getInstance(clazz, defaultEnumTypeHandler));
return TYPE_HANDLER_MAP.get(clazz);
}
} else {
jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
}
}
TYPE_HANDLER_MAP.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
return jdbcHandlerMap;
}
Par défaut, si aucun handler n'est trouvé, MyBatis Plus configure CompositeEnumTypeHandler comme handler par défaut :
public class MybatisConfiguration extends Configuration {
public MybatisConfiguration() {
super();
this.mapUnderscoreToCamelCase = true;
typeHandlerRegistry.setDefaultEnumTypeHandler(CompositeEnumTypeHandler.class);
languageRegistry.setDefaultDriverClass(MybatisXMLLanguageDriver.class);
}
@Override
public void setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler) {
if (typeHandler != null) {
CompositeEnumTypeHandler.setDefaultEnumTypeHandler(typeHandler);
}
}
}
CompositeEnumTypeHandler et MybatisEnumTypeHandler
CompositeEnumTypeHandler agit comme un proxy. Il délègue le traitement au gestionnaire interne delegate :
public class CompositeEnumTypeHandler<E extends Enum<E>> implements TypeHandler<E> {
private static final Map<Class<?>, Boolean> MP_ENUM_CACHE = new ConcurrentHashMap<>();
@Setter
private static Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
private final TypeHandler<E> delegate;
public CompositeEnumTypeHandler(Class<E> enumClassType) {
if (enumClassType == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
if (CollectionUtils.computeIfAbsent(MP_ENUM_CACHE, enumClassType, MybatisEnumTypeHandler::isMpEnums)) {
delegate = new MybatisEnumTypeHandler<>(enumClassType);
} else {
delegate = getInstance(enumClassType, defaultEnumTypeHandler);
}
}
@Override
public void setParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
delegate.setParameter(ps, i, parameter, jdbcType);
}
// ...
}
La décision de choisir entre MybatisEnumTypeHandler ou le handler par défaut dépend de la méthode isMpEnums :
public static boolean isMpEnums(Class<?> clazz) {
return clazz != null && clazz.isEnum() &&
(IEnum.class.isAssignableFrom(clazz) || findEnumValueFieldName(clazz).isPresent());
}
Ainsi, le comportemant réel est géré par MybatisEnumTypeHandler :
public class MybatisEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
private final Class<E> enumClassType;
private final Class<?> propertyType;
private final Invoker getInvoker;
public MybatisEnumTypeHandler(Class<E> enumClassType) {
if (enumClassType == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.enumClassType = enumClassType;
MetaClass metaClass = MetaClass.forClass(enumClassType, REFLECTOR_FACTORY);
String fieldName = "value";
if (!IEnum.class.isAssignableFrom(enumClassType)) {
fieldName = findEnumValueFieldName(this.enumClassType)
.orElseThrow(() -> new IllegalArgumentException(String.format("No @EnumValue found in class: %s.", this.enumClassType.getName())));
}
this.propertyType = ReflectionKit.resolvePrimitiveIfNecessary(metaClass.getGetterType(fieldName));
this.getInvoker = metaClass.getGetInvoker(fieldName);
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
throws SQLException {
if (jdbcType == null) {
ps.setObject(i, this.getValue(parameter));
} else {
ps.setObject(i, this.getValue(parameter), jdbcType.TYPE_CODE);
}
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
Object value = rs.getObject(columnName, this.propertyType);
if (null == value || rs.wasNull()) {
return null;
}
return this.valueOf(value);
}
private E valueOf(Object value) {
E[] constants = this.enumClassType.getEnumConstants();
return Arrays.stream(constants)
.filter(e -> equalsValue(value, getValue(e)))
.findAny()
.orElse(null);
}
private Object getValue(Object object) {
try {
return this.getInvoker.invoke(object, new Object[0]);
} catch (ReflectiveOperationException e) {
throw ExceptionUtils.mpe(e);
}
}
}
Le fonctionnement global est donc :
- Enitialisation des métadonnées nécessaires dans le constructeur.
- Conversion de l’énumération vers sa valeur stockée via
setNonNullParameter. - Conversion inverse depuis la valeur en base vers l’énumération dans
getNullableResult.
Les détails enternes de la comparaison ou du reflet ne sont pas approfondis ici.