Tipi personalizzati in Hibernate e annotazione @Type

1. Panoramica

Hibernate semplifica la gestione dei dati tra SQL e JDBC mappando il modello orientato agli oggetti in Java con il modello relazionale nei database. Sebbene la mappatura delle classi Java di base sia incorporata in Hibernate, la mappatura dei tipi personalizzati è spesso complessa.

In questo tutorial, vedremo come Hibernate ci consente di estendere la mappatura dei tipi di base a classi Java personalizzate. In aggiunta a ciò, vedremo anche alcuni esempi comuni di tipi personalizzati e li implementeremo utilizzando il meccanismo di mappatura dei tipi di Hibernate.

2. Tipi di mappatura ibernati

Hibernate utilizza i tipi di mappatura per convertire gli oggetti Java in query SQL per la memorizzazione dei dati. Allo stesso modo, utilizza i tipi di mappatura per convertire il ResultSet SQL in oggetti Java durante il recupero dei dati.

In genere, Hibernate classifica i tipi in tipi di entità e tipi di valore . Nello specifico, i tipi di entità vengono utilizzati per mappare entità Java specifiche del dominio e, quindi, esistono indipendentemente dagli altri tipi nell'applicazione. Al contrario, i tipi di valore vengono utilizzati per mappare gli oggetti di dati e sono quasi sempre di proprietà delle entità.

In questo tutorial, ci concentreremo sulla mappatura dei tipi di valore che sono ulteriormente classificati in:

  • Tipi di base: mappatura per i tipi Java di base
  • Incorporabile - Mappatura per tipi java compositi / POJO
  • Collezioni: mappatura per una raccolta di tipi Java di base e compositi

3. Dipendenze di Maven

Per creare i nostri tipi Hibernate personalizzati, avremo bisogno della dipendenza hibernate-core:

 org.hibernate hibernate-core 5.3.6.Final 

4. Tipi personalizzati in Hibernate

Possiamo utilizzare i tipi di mappatura di base di Hibernate per la maggior parte dei domini utente. Tuttavia, esistono molti casi d'uso, in cui è necessario implementare un tipo personalizzato.

Hibernate rende relativamente più facile implementare tipi personalizzati. Esistono tre approcci per implementare un tipo personalizzato in Hibernate. Discutiamo ciascuno di essi in dettaglio.

4.1. Implementazione di BasicType

Possiamo creare un tipo di base personalizzato implementando BasicType di Hibernate o una delle sue implementazioni specifiche, AbstractSingleColumnStandardBasicType.

Prima di implementare il nostro primo tipo personalizzato, vediamo un caso d'uso comune per l'implementazione di un tipo di base. Supponiamo di dover lavorare con un database legacy, che memorizza le date come VARCHAR. Normalmente, Hibernate lo associa al tipo String Java. In tal modo, la convalida della data è più difficile per gli sviluppatori di applicazioni.

Quindi implementiamo il nostro tipo LocalDateString , che memorizza il tipo Java LocalDate come 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 cosa più importante in questo codice sono i parametri del costruttore. Innanzitutto, è un'istanza di SqlTypeDescriptor , che è la rappresentazione del tipo SQL di Hibernate, che è VARCHAR per il nostro esempio. E il secondo argomento è un'istanza di JavaTypeDescriptor che rappresenta il tipo Java.

Ora possiamo implementare un LocalDateStringJavaDescriptor per l'archiviazione e il recupero di LocalDate come VARCHAR:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor { public static final LocalDateStringJavaDescriptor INSTANCE = new LocalDateStringJavaDescriptor(); public LocalDateStringJavaDescriptor() { super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE); } // other methods }

Successivamente, abbiamo bisogno di sovrascrivere i metodi di wrap e unrap per convertire il tipo Java in SQL. Cominciamo con lo scartare:

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

Successivamente, il metodo 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 () viene chiamato durante l' associazione PreparedStatement per convertire LocalDate in un tipo String, che è mappato a VARCHAR. Allo stesso modo, wrap () viene chiamato durante il richiamo di ResultSet per convertire String in un LocalDate Java .

Infine, possiamo usare il nostro tipo personalizzato nella nostra classe Entity:

@Entity @Table(name = "OfficeEmployee") public class OfficeEmployee { @Column @Type(type = "com.baeldung.hibernate.customtypes.LocalDateStringType") private LocalDate dateOfJoining; // other fields and methods }

Più avanti vedremo come registrare questo tipo in Hibernate. Di conseguenza, fare riferimento a questo tipo utilizzando la chiave di registrazione anziché il nome completo della classe.

4.2. Implementazione di UserType

Con la varietà di tipi di base in Hibernate, è molto raro che sia necessario implementare un tipo di base personalizzato. Al contrario, un caso d'uso più tipico consiste nel mappare un oggetto di dominio Java complesso al database. Tali oggetti di dominio sono generalmente archiviati in più colonne di database.

Quindi implementiamo un oggetto PhoneNumber complesso implementando 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 } 

Qui, il metodo sqlTypes sovrascritto restituisce i tipi di campi SQL, nello stesso ordine in cui sono dichiarati nella nostra classe PhoneNumber . Allo stesso modo, returnedClass metodo restituisce la nostra PhoneNumber tipo Java.

L'unica cosa che resta da fare è implementare i metodi per convertire tra tipo Java e tipo SQL, come abbiamo fatto per il nostro BasicType .

Innanzitutto, il metodo 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; }

Successivamente, il metodo 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()); } }

Infine, possiamo dichiarare il nostro PhoneNumberType personalizzato nella nostra classe di 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. Implementazione di 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

In questo tutorial, abbiamo discusso diversi approcci per la definizione di un tipo personalizzato in Hibernate. Inoltre, abbiamo implementato alcuni tipi personalizzati per la nostra classe di entità in base ad alcuni casi d'uso comuni in cui un nuovo tipo personalizzato può tornare utile.

Come sempre gli esempi di codice sono disponibili su GitHub.