Principe de mapping automatique des énumérations dans MyBatis Plus

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 :

  1. Vérifie s’il existe un TypeHandler spécifique enregistré pour ce type.
  2. Si le type est une énumération, il cherche un handler correspondant à une interface implémentée.
  3. 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 :

  1. Enitialisation des métadonnées nécessaires dans le constructeur.
  2. Conversion de l’énumération vers sa valeur stockée via setNonNullParameter.
  3. 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.

Étiquettes: MyBatis MyBatis-Plus énumération TypeHandler mapping automatique

Publié le 23 août à 17h32