Types personnalisés dans Hibernate et l'annotation @Type

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 tutoriel, nous verrons comment Hibernate nous permet d'étendre le mappage de type de base aux classes Java personnalisées. En plus de cela, nous verrons également quelques exemples courants de types personnalisés et les implémenterons à l'aide du mécanisme de mappage de type d'Hibernate.

2. Types de mappage Hibernate

Hibernate utilise des types de mappage pour convertir des objets Java en requêtes SQL pour 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 en types de valeur . Plus précisément, les types d'entités sont utilisés pour mapper des entités Java spécifiques à un domaine et, par conséquent, existent indépendamment des autres types dans l'application. En revanche, les types de valeur sont utilisés pour mapper des objets de données à la place et appartiennent presque toujours aux entités.

Dans ce didacticiel, nous nous concentrerons sur le mappage des types de valeur qui sont ensuite classés en:

  • Types de base - Mappage pour les types Java de base
  • Embeddable - Mappage pour les types Java composites / POJO
  • Collections - Mappage pour une collection de type Java basique et composite

3. Dépendances de Maven

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

 org.hibernate hibernate-core 5.3.6.Final 

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 la mise en œuvre de types personnalisés. Il existe trois approches pour implémenter un type personnalisé dans Hibernate. Discutons chacun d'eux en détail.

4.1. Implémentation de BasicType

Nous pouvons créer un type de base personnalisé en implémentant le 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 courant pour l'implémentation d'un type de base. Supposons que nous devions travailler avec une base de données héritée, qui stocke les dates sous forme de VARCHAR. Normalement, Hibernate mappera ceci au type Java String . Cela rend la validation de la date plus difficile pour les développeurs d'applications.

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

public class LocalDateStringType extends AbstractSingleColumnStandardBasicType { 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 de type SQL d'Hibernate, qui est VARCHAR pour notre exemple. Et, le deuxième 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 { public static final LocalDateStringJavaDescriptor INSTANCE = new LocalDateStringJavaDescriptor(); public LocalDateStringJavaDescriptor() { super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE); } // other methods }

Ensuite, nous avons besoin de passer outre une pellicule et Déballer méthodes pour convertir le type Java en SQL. Commençons par le dépliage:

@Override public  X unwrap(LocalDate value, Class type, WrapperOptions options) { if (value == null) return null; if (String.class.isAssignableFrom(type)) return (X) LocalDateType.FORMATTER.format(value); throw unknownUnwrap(type); }

Ensuite, la méthode wrap :

@Override public  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()); }

Unrap () est appelé pendant la liaison PreparedStatement pour convertir LocalDate en un type String, qui est mappé à VARCHAR. De même, wrap () est appelé lors de la récupération de ResultSet 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 }

Plus tard, nous verrons comment enregistrer ce type dans Hibernate. Et par conséquent, faites 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 courant consiste à mapper un objet de domaine Java complexe à la base de données. Ces 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, la méthode sqlTypes surchargée retourne les types de champs SQL, dans le même ordre qu'ils sont déclarés dans notre classe PhoneNumber . De même, returnedClass méthode renvoie notre PhoneNumber de type Java.

Il ne reste plus qu'à implémenter les méthodes de conversion entre le type Java et le type SQL, comme nous l'avons fait pour notre BasicType .

Tout d'abord, 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, la méthode nullSafeSet :

@Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { if (Objects.isNull(value)) { st.setNull(index, Types.INTEGER); st.setNull(index + 1, Types.INTEGER); st.setNull(index + 2, 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 personnalisé dans notre classe d'entité OfficeEmployee :

@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

Implementing UserType works well for straightforward types. However, mapping complex Java types (with Collections and Cascaded composite types) need more sophistication. Hibernate allows us to map such types by implementing the CompositeUserType interface.

So, let's see this in action by implementing an AddressType for the OfficeEmployee entity we used earlier:

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 }

Contrary to UserTypes, which maps the index of the type properties, CompositeType maps property names of our Address class. More importantly, the getPropertyType method returns the mapping types for each property.

Additionally, we also need to implement getPropertyValue and setPropertyValue methods for mapping PreparedStatement and ResultSet indexes to type property. As an example, consider getPropertyValue for our 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()); }

Finally, we would need to implement nullSafeGet and nullSafeSet methods for conversion between Java and SQL types. This is similar to what we did earlier in our PhoneNumberType.

Please note that CompositeType‘s are generally implemented as an alternative mapping mechanism to Embeddable types.

4.4. Type Parameterization

Besides creating custom types, Hibernate also allows us to alter the behavior of types based on parameters.

For instance, suppose that we need to store the Salary for our OfficeEmployee. More importantly, the application must convert the salary amountinto geographical local currency amount.

So, let's implement our parameterized SalaryType which accepts currency as a parameter:

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 }

Please note that we have skipped the CompositeUserType methods from our example to focus on parameterization. Here, we simply implemented Hibernate's DynamicParameterizedType, and override the setParameterValues() method. Now, the SalaryType accept a currency parameter and will convert any amount before storing it.

We'll pass the currency as a parameter while declaring the 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. Basic Type Registry

Hibernate maintains the mapping of all in-built basic types in the BasicTypeRegistry. Thus, eliminating the need to annotate mapping information for such types.

Additionally, Hibernate allows us to register custom types, just like basic types, in the BasicTypeRegistry. Normally, applications would register custom type while bootstrapping the SessionFactory. Let's understand this by registering the LocalDateString type we implemented earlier:

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 }

Thus, it takes away the limitation of using the fully qualified class name in Type mapping:

@Entity @Table(name = "OfficeEmployee") public class OfficeEmployee { @Column @Type(type = "LocalDateString") private LocalDate dateOfJoining; // other methods }

Here, LocalDateString is the key to which the LocalDateStringType is mapped.

Alternatively, we can skip Type registration by defining 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 discuté de 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é sur la base de certains cas d'utilisation courants où un nouveau type personnalisé peut être utile.

Comme toujours, les exemples de code sont disponibles sur GitHub.