web-dev-qa-db-fra.com

Est-il possible d'écrire un convertisseur d'énumération générique pour JPA?

Je voulais écrire un convertisseur pour JPA qui stocke toute énumération en tant que MAJUSCULE. Certaines énumérations que nous rencontrons ne respectent pas encore la convention de n'utiliser que des lettres majuscules, jusqu'à ce qu'elles soient refactorisées, je stocke toujours la valeur future.

Ce que j'ai obtenu jusqu'à présent:

package student;

public enum StudentState {

    Started,
    Mentoring,
    Repeating,
    STUPID,
    GENIUS;
}

Je veux que "Démarré" soit enregistré comme "COMMENCÉ" et ainsi de suite.

package student;

import jpa.EnumUppercaseConverter;

import javax.persistence.*;
import Java.io.Serializable;
import Java.util.Date;

@Entity
@Table(name = "STUDENTS")
public class Student implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mId;

    @Column(name = "LAST_NAME", length = 35)
    private String mLastName;

    @Column(name = "FIRST_NAME", nullable = false, length = 35)
    private String mFirstName;

    @Column(name = "BIRTH_DATE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date mBirthDate;

    @Column(name = "STUDENT_STATE")
    @Enumerated(EnumType.STRING)
    @Convert(converter = EnumUppercaseConverter.class)
    private StudentState studentState;

}

le convertisseur ressemble actuellement à ceci:

package jpa;


import javax.persistence.AttributeConverter;
import Java.util.EnumSet;

public class EnumUppercaseConverter<E extends Enum<E>> implements AttributeConverter<E, String> {

    private Class<E> enumClass;

    @Override
    public String convertToDatabaseColumn(E e) {
        return e.name().toUpperCase();
    }

    @Override
    public E convertToEntityAttribute(String s) {
        // which enum is it?
        for (E en : EnumSet.allOf(enumClass)) {
            if (en.name().equalsIgnoreCase(s)) {
                return en;
            }
        }
        return null;
    }

}

ce qui ne fonctionnera pas, c'est que je ne sais pas ce que sera enumClass lors de l'exécution. Et je ne pouvais pas trouver un moyen de transmettre ces informations au convertisseur dans l'annotation @Converter.

Existe-t-il un moyen d'ajouter des paramètres au convertisseur ou de tricher un peu? Ou existe-t-il un autre moyen?

J'utilise EclipseLink 2.4.2

Merci!

24
wemu

Ce que vous devez faire est d'écrire une classe de base générique, puis de l'étendre pour chaque type d'énumération que vous souhaitez conserver. Utilisez ensuite le type étendu dans le @Converter annotation:

public abstract class GenericEnumUppercaseConverter<E extends Enum<E>> implements AttributeConverter<E, String> {
    ...
}

public FooConverter
    extends GenericEnumUppercaseConverter<Foo> 
    implements AttributeConverter<Foo, String> // See Bug HHH-8854
{
    public FooConverter() {
        super(Foo.class);
    }
}

Foo est l'énumération que vous souhaitez gérer.

L'alternative serait de définir une annotation personnalisée, de patcher le fournisseur JPA pour reconnaître cette annotation. De cette façon, vous pouvez examiner le type de champ lorsque vous créez les informations de mappage et introduisez le type d'énumération nécessaire dans un convertisseur purement générique.

En relation:

18
Aaron Digulla

Basé sur la solution @scottb, j'ai fait cela, testé contre hibernate 4.3: (pas de classes hibernate, devrait fonctionner sur JPA très bien)

L'énumération d'interface doit implémenter:

public interface PersistableEnum<T> {
    public T getValue();
}

Convertisseur abstrait de base:

@Converter
public abstract class AbstractEnumConverter<T extends Enum<T> & PersistableEnum<E>, E> implements AttributeConverter<T, E> {
    private final Class<T> clazz;

    public AbstractEnumConverter(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public E convertToDatabaseColumn(T attribute) {
        return attribute != null ? attribute.getValue() : null;
    }

    @Override
    public T convertToEntityAttribute(E dbData) {
        T[] enums = clazz.getEnumConstants();

        for (T e : enums) {
            if (e.getValue().equals(dbData)) {
                return e;
            }
        }

        throw new UnsupportedOperationException();
    }
}

Vous devez créer une classe de conversion pour chaque énumération, je trouve plus facile de créer une classe statique à l'intérieur de l'énumération: (jpa/hibernate pourrait simplement fournir l'interface pour l'énumération, eh bien ...)

public enum IndOrientation implements PersistableEnum<String> {
    LANDSCAPE("L"), PORTRAIT("P");

    private final String value;

    @Override
    public String getValue() {
        return value;
    }

    private IndOrientation(String value) {
        this.value= value;
    }

    public static class Converter extends AbstractEnumConverter<IndOrientation, String> {
        public Converter() {
            super(IndOrientation.class);
        }
    }
}

Et exemple de mappage avec annotation:

...
@Convert(converter = IndOrientation.Converter.class)
private IndOrientation indOrientation;
...

Avec quelques modifications, vous pouvez créer une interface IntegerEnum et générer pour cela.

16
ChRoNoN

Ma solution à ce problème semble similaire et utilise également la fonction de convertisseur JPA 2.1. Hélas, les types génériques dans Java 8 ne sont pas réifiés, et il ne semble donc pas être facile d'éviter d'écrire une classe distincte pour chaque Java enum que vous souhaitez pouvoir convertir vers/depuis un format de base de données.

Vous pouvez cependant réduire le processus d'écriture d'une classe de convertisseur d'énumération à un passe-partout pur. Les composants de cette solution sont:

  1. Encodeable interface; le contrat pour une classe enum qui accorde l'accès à un jeton String pour chaque constante enum (également une fabrique pour obtenir la constante enum pour un jeton correspondant)
  2. AbstractEnumConverter classe; fournit le code commun pour la traduction des jetons vers/depuis les constantes enum
  3. Classes d'énumération Java qui implémentent l'interface Encodeable
  4. Classes de conversion JPA qui étendent la classe AbstractEnumConverter

L'interface Encodeable est simple et contient une méthode d'usine statique, forToken(), pour obtenir des constantes enum:

public interface Encodeable {

    String token();

    public static <E extends Enum<E> & Encodeable> E forToken(Class<E> cls, String tok) {
        final String t = tok.trim().toUpperCase();
        return Stream.of(cls.getEnumConstants())
                .filter(e -> e.token().equals(t))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Unknown token '" +
                        tok + "' for enum " + cls.getName()));
    }
}

La classe AbstractEnumConverter est une classe générique qui est également simple. Il implémente l'interface JPA 2.1 AttributeConverter mais ne fournit aucune implémentation pour ses méthodes (car cette classe ne peut pas connaître les types concrets nécessaires pour obtenir les constantes énum appropriées). Au lieu de cela, il définit des méthodes d'assistance que les classes de convertisseur concret enchaîneront:

public abstract class AbstractEnumConverter<E extends Enum<E> & Encodeable>
            implements AttributeConverter<E, String> {

    public String toDatabaseColumn(E attr) {
        return (attr == null)
                ? null
                : attr.token();
    }

    public E toEntityAttribute(Class<E> cls, String dbCol) {
        return (dbCol == null)
                ? null
                : Encodeable.forToken(cls, dbCol);
    }
}

Un exemple d'une classe d'enum concrète qui pourrait maintenant être conservée dans une base de données avec la fonction de convertisseur JPA 2.1 est illustré ci-dessous (notez qu'il implémente Encodeable, et que le jeton pour chaque constante d'énumération est défini comme un champ privé ):

public enum GenderCode implements Encodeable {

    MALE   ("M"), 
    FEMALE ("F"), 
    OTHER  ("O");

    final String e_token;

    GenderCode(String v) {
        this.e_token = v;
    }

    @Override
    public String token() {
        return this.e_token;
    }
}

Le passe-partout pour chaque classe de convertisseur JPA 2.1 ressemblerait maintenant à ceci (notez que chaque convertisseur devra étendre AbstractEnumConverter et fournir des implémentations pour les méthodes de l'interface JPA AttributeConverter):

@Converter
public class GenderCodeConverter 
            extends AbstractEnumConverter<GenderCode> {

    @Override
    public String convertToDatabaseColumn(GenderCode attribute) {
        return this.toDatabaseColumn(attribute);
    }

    @Override
    public GenderCode convertToEntityAttribute(String dbData) {
        return this.toEntityAttribute(GenderCode.class, dbData);
    }
}
5
scottb