klee-contrib / topmodel

Outil de modélisation et générateurs pour divers langages
https://klee-contrib.github.io/topmodel/
MIT License
11 stars 12 forks source link

[JAVA] Enumérations représentant totalement l'entité #373

Open kbuntrock opened 3 months ago

kbuntrock commented 3 months ago

En présence d'une liste de références immutables, la génération actuelle java génère une entité dont l'attribut primary key devient une enumération. Entrainant une petite gymnastique ensuite suivant si on manipule l'entity ou la primary key seulement.

Je trouve pertinent de générer directement une énumération pour représenter plus fidèlement l'objet référence et simplifier sa manipulation.

Exemple :

Représentation du modèle :

class:
  name: TypeUtilisateur
  comment: Le type d'un utilisateur
  reference: true
  properties:
    - name: Code
      comment: Code du type
      domain: CODE
      primaryKey: true
    - name: Libelle
      comment: Libellé du type d'utilisateur
      domain: LIBELLE
  values:
    ADMINISTRATEUR: { Code: ADM, Libelle: Administrateur }
    GESTIONNAIRE: { Code: GES, Libelle: Gestionnaire }
    CLIENT: { Code: CLI, Libelle: Client }

Génération actuelle (2 fichiers) :

TypeUtilisateurCode :

/**
 * Enumération des valeurs possibles de la propriété Code de la classe TypeUtilisateur.
 */
public enum TypeUtilisateurCode {
    /**
     * Administrateur.
     */
    ADM,
    /**
     * Client.
     */
    CLI,
    /**
     * Gestionnaire.
     */
    GES;
}

TypeUtilisateur :

/**
 * Le type d'un utilisateur.
 */
@Generated("TopModel : https://github.com/klee-contrib/topmodel")
@Entity
@Table(name = "TYPE_UTILISATEUR")
@Immutable
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class TypeUtilisateur {

    /**
     * Code du type.
     */
    @Id
    @Column(name = "CODE", nullable = false, length = 3, columnDefinition = "varchar")
    @Enumerated(EnumType.STRING)
    private TypeUtilisateurCode code;

    /**
     * Libellé du type d'utilisateur.
     */
    @Column(name = "LIBELLE", nullable = true, length = 15, columnDefinition = "varchar")
    private String libelle;

    /**
     * No arg constructor.
     */
    public TypeUtilisateur() {
        // No arg constructor
    }

    public static final TypeUtilisateur ADM = new TypeUtilisateur(TypeUtilisateurCode.ADM);
    public static final TypeUtilisateur CLI = new TypeUtilisateur(TypeUtilisateurCode.CLI);
    public static final TypeUtilisateur GES = new TypeUtilisateur(TypeUtilisateurCode.GES);

    /**
     * Enum constructor.
     * @param code Code dont on veut obtenir l'instance.
     */
    public TypeUtilisateur(TypeUtilisateurCode code) {
        this.code = code;
        switch(code) {
        case ADM :
            this.libelle = "utilisateur.typeUtilisateur.values.ADMINISTRATEUR";
            break;
        case CLI :
            this.libelle = "utilisateur.typeUtilisateur.values.CLIENT";
            break;
        case GES :
            this.libelle = "utilisateur.typeUtilisateur.values.GESTIONNAIRE";
            break;
        }
    }

    /**
     * Getter for code.
     *
     * @return value of {@link tuto.entities.utilisateur.TypeUtilisateur#code code}.
     */
    public TypeUtilisateurCode getCode() {
        return this.code;
    }

    /**
     * Getter for libelle.
     *
     * @return value of {@link tuto.entities.utilisateur.TypeUtilisateur#libelle libelle}.
     */
    public String getLibelle() {
        return this.libelle;
    }

    /**
     * Set the value of {@link tuto.entities.utilisateur.TypeUtilisateur#code code}.
     * @param code value to set
     */
    public void setCode(TypeUtilisateurCode code) {
        this.code = code;
    }

    /**
     * Set the value of {@link tuto.entities.utilisateur.TypeUtilisateur#libelle libelle}.
     * @param libelle value to set
     */
    public void setLibelle(String libelle) {
        this.libelle = libelle;
    }
}

Génération souhaitée (2 fichier, mais un seul à manipuler) :

./**
* La classe qui sera manipulée par les devs
*/
public enum TypeUtilisateur {
    ADMINISTRATEUR("ADM", "Administrateur"),
    GESTIONNAIRE("GES", "Gestionnaire"),
    CLIENT("CLI", "Client");

    private static final Map<String, TypeUtilisateur> MAP_BY_ID = new HashMap<>();

    static {
        for(final TypeUtilisateur typeUtilisateur : values()) {
            MAP_BY_ID.put(typeUtilisateur.code, typeUtilisateur);
        }
    }

    private final String code;
    private final String libelle;

    TypeUtilisateur(String code, String libelle) {
        this.code = code;
        this.libelle = libelle;
    }

    public static TypeUtilisateur fromId(final String code) {
        TypeUtilisateur typeUtilisateur = MAP_BY_ID.get(code);
        if(typeUtilisateur == null) {
            throw new NoSuchElementException("Enum TypeUtilisateur non trouvée pour code : " + code);
        }
        return typeUtilisateur;
    }

    public String getCode() {
        return code;
    }

    public String getLibelle() {
        return libelle;
    }
}

Et le converter associé

./**
* C'est ce qui va permettre de faire la conversion code <-> TypeUtilisateur
*/
@Converter
public class TypeUtilisateurConverter implements AttributeConverter<TypeUtilisateur, String> {

    @Override
    public String convertToDatabaseColumn(TypeUtilisateur typeUtilisateur) {
        return typeUtilisateur == null ? null : typeUtilisateur.getCode();
    }

    @Override
    public TypeUtilisateur convertToEntityAttribute(String code) {
        return TypeUtilisateur.fromCode(code);
    }
}

Dans les entités associés, la convertion est ensuire indiquée comme telle grace à l'annotation @Convert :

/**
 * Type de l'utilisateur.
 */
@Convert(converter = TypeUtilisateurConverter.class)
@Column(name = "type", nullable = false)
private TypeUtilisateur type;

What do you think?

gideruette commented 2 months ago

Ca peut poser problème de supprimer le fait que ce soit une association. Il faut donc le mettre en option. Mais en dehors de ça, c'est faisable. Par contre je ne vois pas de nécessité de retransformer typeUtilisateurCode en String, même si effectivement il serait beaucoup moins utilisé.

dchallas commented 2 months ago

Quelques remarques :

1- Le converteur ne me parrait pas utile, on peut utiliser directement @Enumerated(EnumType.STRING)

2- J'utilise ma classe EnumLookup qui permet de prendre en charge la recherche d'énumération par Enum::name (return null si pas trouvé :)) Donc pas besoin de surcharger l'énum pour cela.

A lire : https://dzone.com/articles/java-enum-lookup-by-name-or-field-without-throwing

import Collections.associateBy;

import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.function.Function;

import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j;

@Slf4j @UtilityClass public final class EnumLookup { // Attention on ne peut réinitialiser ce cache, mais c'est uniquement sur des enums private static final Map<Class<?>, Map<?, ?>> cache = new HashMap<>();

public static <T, E extends Enum<E>> Map<T, E> lookupNullable(final Class<E> clazz) {
    return (Map<T, E>) lookupNullable(clazz, Enum::name);
}

private static <E extends Enum<E>> Map<String, E> lookupNullable(final Class<E> clazz,
        final Function<E, String> mapper) {
    final var indexCache = (Map<String, E>) cache.get(clazz);
    if (indexCache != null) {
        return indexCache;
    }
    final var index = associateBy(EnumSet.allOf(clazz), mapper);
    cache.putIfAbsent(clazz, index);
    return index;
}

public static <E extends Enum<E>> E lookupNullable(final Class<E> clazz, final String displayName) {
    final E e = lookupNullable(clazz, Enum::name).get(displayName);
    if (e == null) {
        LOGGER.debug("looking up {} not found ", displayName);
    }
    return e;
}

}