Types personnalisés dans Hibernate

1. Vue d’ensemble

Hibernate simplifie la gestion des données entre SQL et JDBC en mappant le modèle orienté objet en Java avec le modèle relationnel dans les bases de données.

Bien que le mappage des classes Java de base soit intégré à Hibernate, le mappage des types personnalisés est souvent complexe.

Dans ce didacticiel, nous verrons comment Hibernate nous permet d’étendre le mappage de types de base aux classes Java personnalisées. En plus de cela, nous allons voir quelques exemples courants de types personnalisés et les implémenter en utilisant le mécanisme de mappage de types de Hibernate.

2. Types de mappage Hibernate

Hibernate utilise des types de mappage pour convertir les objets Java en requêtes SQL afin de stocker des données. De même, il utilise des types de mappage pour convertir SQL ResultSet en objets Java lors de la récupération de données.

En règle générale, Hibernate classe les types en types d’entité et types de valeur . Spécifiquement, les types d’entité sont utilisés pour mapper des entités Java spécifiques à un domaine et existent donc indépendamment des autres types de l’application. En revanche, les types de valeur sont utilisés pour mapper des objets de données et appartiennent presque toujours aux entités.

Dans ce tutoriel, nous allons nous concentrer sur la cartographie des types de valeur qui sont ensuite classés dans:

  • Types de base - Mappage pour les types Java de base

  • Intégrable - Mappage pour les types java composites/POJO

  • Collections - Cartographie pour une collection de java composite et de base

type

3. Dépendances Maven

Pour créer nos types Hibernate personnalisés, nous avons besoin de la dépendance hibernate-core :

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.3.6.Final</version>
</dependency>

4. Types personnalisés dans Hibernate

Nous pouvons utiliser les types de mappage de base Hibernate pour la plupart des domaines d’utilisateurs. Cependant, il existe de nombreux cas d’utilisation dans lesquels nous devons implémenter un type personnalisé.

Hibernate facilite relativement l’implémentation des types personnalisés. Il existe trois approches pour implémenter un type personnalisé dans Hibernate. Discutons de chacune d’elles en détail.

4.1. Implémentation de BasicType

Nous pouvons créer un type de base personnalisé en implémentant _BasicType d’Hibernate ou l’une de ses implémentations spécifiques, AbstractSingleColumnStandardBasicType._

Avant d’implémenter notre premier type personnalisé, voyons un cas d’utilisation commun pour l’implémentation d’un type de base. Supposons que nous devions travailler avec une base de données existante, qui stocke les dates sous la forme VARCHAR. Normalement, Hibernate mapperait cela sur le type String Java. Ainsi, la validation des dates est plus difficile pour les développeurs d’applications.

Implémentons donc notre type __LocalDateString , qui stocke le type LocalDate __Java en tant que VARCHAR:

public class LocalDateStringType
  extends AbstractSingleColumnStandardBasicType<LocalDate> {

    public static final LocalDateStringType INSTANCE = new LocalDateStringType();

    public LocalDateStringType() {
        super(VarcharTypeDescriptor.INSTANCE, LocalDateStringJavaDescriptor.INSTANCE);
    }

    @Override
    public String getName() {
        return "LocalDateString";
    }
}

La chose la plus importante dans ce code est les paramètres du constructeur.

Tout d’abord, une instance de SqlTypeDescriptor , qui est la représentation du type SQL d’Hibernate, qui est VARCHAR pour notre exemple. Et le second argument est une instance de JavaTypeDescriptor qui représente le type Java.

Maintenant, nous pouvons implémenter un _LocalDateStringJavaDescriptor pour stocker et récupérer LocalDate_ en tant que VARCHAR:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {

    public static final LocalDateStringJavaDescriptor INSTANCE =
      new LocalDateStringJavaDescriptor();

    public LocalDateStringJavaDescriptor() {
        super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE);
    }

   //other methods
}

Ensuite, nous devons redéfinir les méthodes _ wrap et unwrap pour convertir le type Java en SQL. Commençons par le décompresser: _

@Override
public <X> X unwrap(LocalDate value, Class<X> type, WrapperOptions options) {

    if (value == null)
        return null;

    if (String.class.isAssignableFrom(type))
        return (X) LocalDateType.FORMATTER.format(value);

    throw unknownUnwrap(type);
}

Ensuite, le __wrap __method:

@Override
public <X> LocalDate wrap(X value, WrapperOptions options) {
    if (value == null)
        return null;

    if(String.class.isInstance(value))
        return LocalDate.from(LocalDateType.FORMATTER.parse((CharSequence) value));

    throw unknownWrap(value.getClass());
}

_unwrap () est appelé pendant PreparedStatement binding pour convertir LocalDate _ en un type String, mappé à VARCHAR.

De même, _wrap () est appelé pendant ResultSet retrieval pour convertir String en Java LocalDate_ .

Enfin, nous pouvons utiliser notre type personnalisé dans notre classe Entity:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Column
    @Type(type = "com.baeldung.hibernate.customtypes.LocalDateStringType")
    private LocalDate dateOfJoining;

   //other fields and methods
}

Nous verrons plus tard comment enregistrer ce type dans Hibernate. Et par conséquent, fait référence à ce type en utilisant la clé d’enregistrement au lieu du nom de classe complet.

4.2. Implémentation de UserType

Avec la variété de types de base dans Hibernate, il est très rare que nous ayons besoin d’implémenter un type de base personnalisé. En revanche, un cas d’utilisation plus typique consiste à mapper un objet de domaine Java complexe sur la base de données. De tels objets de domaine sont généralement stockés dans plusieurs colonnes de base de données.

Implémentons donc un objet PhoneNumber complexe en implémentant UserType:

public class PhoneNumberType implements UserType {
    @Override
    public int[]sqlTypes() {
        return new int[]{Types.INTEGER, Types.INTEGER, Types.INTEGER};
    }

    @Override
    public Class returnedClass() {
        return PhoneNumber.class;
    }

   //other methods
}

Ici, __sqlTypes method surchargé renvoie les types de champs SQL, dans le même ordre que ceux déclarés dans notre PhoneNumber class. De même, returnedClass method renvoie notre type PhoneNumber __Java.

Il ne reste plus qu’à implémenter les méthodes pour convertir le type Java en type SQL, comme nous l’avons fait pour notre BasicType .

Premièrement, la méthode _ nullSafeGet _ :

@Override
public Object nullSafeGet(ResultSet rs, String[]names,
  SharedSessionContractImplementor session, Object owner)
  throws HibernateException, SQLException {
    int countryCode = rs.getInt(names[0]);

    if (rs.wasNull())
        return null;

    int cityCode = rs.getInt(names[1]);
    int number = rs.getInt(names[2]);
    PhoneNumber employeeNumber = new PhoneNumber(countryCode, cityCode, number);

    return employeeNumber;
}

Ensuite, __nullSafeSet __method:

@Override
public void nullSafeSet(PreparedStatement st, Object value,
  int index, SharedSessionContractImplementor session)
  throws HibernateException, SQLException {

    if (Objects.isNull(value)) {
        st.setNull(index, Types.INTEGER);
    } else {
        PhoneNumber employeeNumber = (PhoneNumber) value;
        st.setInt(index,employeeNumber.getCountryCode());
        st.setInt(index+1,employeeNumber.getCityCode());
        st.setInt(index+2,employeeNumber.getNumber());
    }
}

Enfin, nous pouvons déclarer notre __PhoneNumberType dans notre classe OfficeEmployee __entity personnalisée:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Columns(columns = { @Column(name = "country__code"),
      @Column(name = "city__code"), @Column(name = "number") })
    @Type(type = "com.baeldung.hibernate.customtypes.PhoneNumberType")
    private PhoneNumber employeeNumber;

   //other fields and methods
}

4.3. Implémentation de CompositeUserType

L’implémentation de _UserType works convient bien aux types simples. Cependant, le mappage de types Java complexes (avec les types composites Collections et Cascaded) nécessite davantage de sophistication. Hibernate nous permet de mapper de tels types en implémentant l’interface CompositeUserType _ .

Voyons donc cela en action en implémentant un __AddressType pour le OfficeEmployee __entity que nous avons utilisé précédemment:

public class AddressType implements CompositeUserType {

    @Override
    public String[]getPropertyNames() {
        return new String[]{ "addressLine1", "addressLine2",
          "city", "country", "zipcode" };
    }

    @Override
    public Type[]getPropertyTypes() {
        return new Type[]{ StringType.INSTANCE,
          StringType.INSTANCE,
          StringType.INSTANCE,
          StringType.INSTANCE,
          IntegerType.INSTANCE };
    }

   //other methods
}

Contrairement à UserTypes , qui mappe l’index des propriétés de type, les noms de propriété __CompositeType maps de notre Address class. Plus important encore, the getPropertyType __method renvoie les types de mappage pour chaque propriété.

En outre, nous devons également implémenter _getPropertyValue and setPropertyValue methods pour mapper PreparedStatement and ResultSet indexes sur la propriété. Par exemple, considérons getPropertyValue for notre AddressType: _

@Override
public Object getPropertyValue(Object component, int property) throws HibernateException {

    Address empAdd = (Address) component;

    switch (property) {
    case 0:
        return empAdd.getAddressLine1();
    case 1:
        return empAdd.getAddressLine2();
    case 2:
        return empAdd.getCity();
    case 3:
        return empAdd.getCountry();
    case 4:
        return Integer.valueOf(empAdd.getZipCode());
    }

    throw new IllegalArgumentException(property + " is an invalid property index for class type "
      + component.getClass().getName());
}

Enfin, nous aurions besoin de mettre en œuvre _nullSafeGet et nullSafeSet methods pour la conversion entre les types Java et SQL. Ceci est similaire à ce que nous avons fait précédemment dans notre PhoneNumberType._

Veuillez noter que les fichiers ** CompositeType sont généralement implémentés comme un mécanisme de mappage alternatif aux types _Embeddable _ .

4.4. Type Paramétrage

Outre la création de types personnalisés, Hibernate nous permet également de modifier le comportement des types en fonction de paramètres.

Par exemple, supposons qu’il soit nécessaire de stocker le _Salary pour notre OfficeEmployee. Plus important encore, l’application doit convertir le montant du salaire _en montant géographique en devise locale.

Donc, implémentons notre _SalaryType qui accepte currency _ en tant que paramètre:

public class SalaryType implements CompositeUserType, DynamicParameterizedType {

    private String localCurrency;

    @Override
    public void setParameterValues(Properties parameters) {
        this.localCurrency = parameters.getProperty("currency");
    }

   //other method implementations from CompositeUserType
}

Veuillez noter que nous avons ignoré les méthodes CompositeUserType de notre exemple pour nous concentrer sur le paramétrage. Ici, nous avons simplement implémenté DynamicParameterizedType de Hibernate et substitué la méthode _setParameterValues ​​() . Désormais, le SalaryType accepte un paramètre currency _ et convertira tout montant avant de le stocker.

Nous allons passer le currency en tant que paramètre tout en déclarant le Salary:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Type(type = "com.baeldung.hibernate.customtypes.SalaryType",
      parameters = { @Parameter(name = "currency", value = "USD") })
    @Columns(columns = { @Column(name = "amount"), @Column(name = "currency") })
    private Salary salary;

   //other fields and methods
}

5. Registre de types de base

Hibernate maintient le mappage de tous les types de base intégrés dans le BasicTypeRegistry . Ainsi, il n’est plus nécessaire d’annoter les informations de mappage pour ces types.

De plus, Hibernate nous permet d’enregistrer des types personnalisés, tout comme les types de base, dans BasicTypeRegistry . Normalement, les applications enregistrent un type personnalisé lors de l’amorçage de __SessionFactory. Prenons comprendre cela en enregistrant le LocalDateString __type que nous avons implémenté précédemment:

private static SessionFactory makeSessionFactory() {
    ServiceRegistry serviceRegistry = StandardServiceRegistryBuilder()
      .applySettings(getProperties()).build();

    MetadataSources metadataSources = new MetadataSources(serviceRegistry);
    Metadata metadata = metadataSources.getMetadataBuilder()
      .applyBasicType(LocalDateStringType.INSTANCE)
      .build();

    return metadata.getSessionFactoryBuilder().build()
}

private static Properties getProperties() {
   //return hibernate properties
}

Ainsi, cela supprime la limitation d’utilisation du nom de classe complet dans le mappage de types:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Column
    @Type(type = "LocalDateString")
    private LocalDate dateOfJoining;

   //other methods
}

Ici, LocalDateString est la clé à laquelle le type ____LocalDateString est mappé.

Alternativement, nous pouvons ignorer l’enregistrement de type en définissant TypeDefs:

@TypeDef(name = "PhoneNumber", typeClass = PhoneNumberType.class,
  defaultForType = PhoneNumber.class)
@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Columns(columns = {@Column(name = "country__code"),
    @Column(name = "city__code"),
    @Column(name = "number")})
    private PhoneNumber employeeNumber;

   //other methods
}

6. Conclusion

Dans ce didacticiel, nous avons présenté plusieurs approches pour définir un type personnalisé dans Hibernate. De plus, ** nous avons implémenté quelques types personnalisés pour notre classe d’entités en fonction de cas d’utilisation courants dans lesquels un nouveau type personnalisé peut s’avérer utile.

Comme toujours, les exemples de code sont disponibles à l’adresse over sur GitHub .