1. Overview

Hibernate simplifies data handling between SQL and JDBC by mapping the Object-Oriented model in Java with the Relational model in Databases. Although mapping of basic Java classes is in-built in Hibernate, mapping of custom types is often complex.

In this tutorial, we’ll see how Hibernate allows us to extend the basic type mapping to custom Java classes. In addition to that, we’ll also see some common examples of custom types and implement them using Hibernate’s type mapping mechanism.

2. Hibernate Mapping Types

Hibernate uses mapping types for converting Java objects into SQL queries for storing data. Similarly, it uses mapping types for converting SQL ResultSet into Java objects while retrieving data.

Generally, Hibernate categorizes the types into Entity Types and Value Types**.** Specifically, Entity types are used to map domain-specific Java entities and hence, exist independently of other types in the application. In contrast, Value Types are used to map data objects instead and are almost always owned by the Entities.

In this tutorial, we will focus on the mapping of Value types which are further classified into:

  • Basic Types – Mapping for basic Java types
  • Embeddable – Mapping for composite java types/POJO’s
  • Collections – Mapping for a collection of basic and composite java type

3. Maven Dependencies

To create our custom Hibernate types, we’ll need the hibernate-core dependency:

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

4. Custom Types in Hibernate

We can use Hibernate basic mapping types for most user domains. However, there are many use cases, where we need to implement a custom type.

Hibernate makes it relatively easier to implement custom types. There are three approaches to implementing a custom type in Hibernate. Let’s discuss each of them in detail.

4.1. Implementing BasicType

We can create a custom basic type by implementing Hibernate’s BasicType or one of its specific implementations, AbstractSingleColumnStandardBasicType.

Before we implement our first custom type, let’s see a common use case for implementing a basic type. Suppose we have to work with a legacy database, that stores dates as VARCHAR. Normally, Hibernate would map this to String Java type. Thereby, making date validation harder for application developers. 

So let’s implement our LocalDateString type, which stores LocalDate Java type as VARCHAR:

public class LocalDateStringType extends AbstractSingleColumnStandardBasicType<LocalDate> {

    public static final LocalDateStringType INSTANCE = new LocalDateStringType();

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

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

    public LocalDate stringToObject(String xml) {
        return fromString(xml);
    }

    public String objectToSQLString(LocalDate value, Dialect dialect) {
        return '\'' + LocalDateStringJavaDescriptor.INSTANCE.toString(value) + '\'';
    }

}

The most important thing in this code is the constructor parameters. First, is an instance of SqlTypeDescriptor, which is Hibernate’s SQL type representation, which is VARCHAR for our example. And, the second argument is an instance of JavaTypeDescriptor which represents Java type.

Now, we can implement a LocalDateStringJavaDescriptor for storing and retrieving LocalDate as VARCHAR:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {

    public static final LocalDateStringJavaDescriptor INSTANCE = new LocalDateStringJavaDescriptor();

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

Next, we need to override wrap and unwrap methods for converting the Java type into SQL. Let’s start with the unwrap:

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

    if (value == null)
        return null;

    if (String.class.isAssignableFrom(type))
        return (X) DateTimeFormatter.ISO_LOCAL_DATE.format(value);

    throw unknownUnwrap(type);
}

Next, the wrap method:

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

    if(String.class.isInstance(value))
        return LocalDate.from(DateTimeFormatter.ISO_LOCAL_DATE.parse((CharSequence) value));

    throw unknownWrap(value.getClass());
}

unwrap() is called during PreparedStatement binding to convert LocalDate to a String type, which is mapped to VARCHAR. Likewise, wrap() is called during ResultSet retrieval to convert String to a Java LocalDate.

Finally, we can use our custom type in our Entity class:

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

    @Column
    @Type(value = UserTypeLegacyBridge.class, 
        parameters = @Parameter(name = UserTypeLegacyBridge.TYPE_NAME_PARAM_KEY, 
        value = "LocalDateString")
    
    private LocalDate dateOfJoining;

    // other fields and methods
}

Later, we’ll see how we can register this type in Hibernate. And as a result, refer to this type using the registration key instead of the fully qualified class name.

4.2. Implementing UserType

With the variety of basic types in Hibernate, it is very rare that we need to implement a custom basic type. In contrast, a more typical use case is to map a complex Java domain object to the database. Such domain objects are generally stored in multiple database columns. UserType interface offers the possibility to map a column from database to a multiple fields in a Java object.

So let’s implement a complex Salary object by implementing UserType:

public class SalaryType implements UserType<Salary> {
    
    @Override
    public int sqlType() {
        return Types.VARCHAR;
    }

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

    // other methods
}

Here, the overridden sqlType method returns the SQL types of field. Similarly, the returnedClass method returns our Salary Java type.

The only thing left to do is to implement the methods to convert between Java type and SQL type, as we did for our BasicType.

First, the nullSafeGet method:

@Override
public Salary nullSafeGet(ResultSet rs, int position, 
    SharedSessionContractImplementor session, Object owner) throws SQLException {
    
    Salary salary = new Salary();

    String salaryValue = rs.getString(position);

    salary.setAmount(Long.parseLong(salaryValue.split(" ")[1]));

    salary.setCurrency(salaryValue.split(" ")[0]);

    return salary;
}

The nullSafeGet method is called when we retrieve information from database and we map that information into the Java object. As we already mentioned the UserType interface offers the possibility to map a single column from database to a multiple fields in Java. In the nullSafeGet method we parse the result and set on the Java object.

Next, the nullSafeSet method:

@Override
public void nullSafeSet(PreparedStatement st, Salary value, int index, 
    SharedSessionContractImplementor session) throws SQLException {
    
    if (Objects.isNull(value))
        st.setNull(index, Types.VARCHAR);
    else {
        Long salaryValue = SalaryCurrencyConvertor.convert(value.getAmount(), value.getCurrency(), 
            localCurrency);
        st.setString(index, value.getCurrency() + " " + salaryValue);
    }
}

In the nullSafeSet method is used when we store the information into database. Doing this we set that information on a single column from database.

Finally, we can declare our custom SalaryType in our OfficeEmployee entity class:

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

     @Type(value = com.baeldung.hibernate.customtypes.SalaryType.class, 
         parameters = {@Parameter(name = "currency", value = "USD")})
     
     private Salary salary;
    
    // other fields and methods
}

4.3. Implementing 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. This interface allow us to map multiple database columns to multiple fields in Java object.
**

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

public class AddressType implements CompositeUserType<Address> {

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

    switch (property) {
        case 0:
            return component.getAddressLine1();
        case 1:
            return component.getAddressLine2();
        case 2:
            return component.getCity();
        case 3:
            return component.getCountry();
        case 4:
            return component.getZipCode();
        default:
            throw new IllegalArgumentException(property + 
                " is an invalid property index for class type " + component.getClass()
                .getName());
        }
    }

    @Override
    public Address instantiate(ValueAccess values, SessionFactoryImplementor sessionFactory) {
        return new Address(values.getValue(0, String.class), 
            values.getValue(1,String.class), 
            values.getValue(2, String.class), 
            values.getValue(3, String.class), 
            values.getValue(4,Integer.class));
    }

    @Override
    public Class<?> embeddable() {
        return Address.class;
    }

    @Override
    public Class<Address> returnedClass() {
        return Address.class;
    }

    //other methods
}

Contrary to UserTypes, which maps to a single database column, CompositeType maps property names of our Address class.

Additionally, we also need to implement getPropertyValue and instantiate 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());
}

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 of our OfficeEmployee. More importantly, the application must convert the salary amount into geographical local currency amount.

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

public class SalaryType implements UserType<Salary>, 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 overridden the setParameterValues() method. Now, the SalaryType accepts 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(value = com.baeldung.hibernate.customtypes.SalaryType, 
        parameters = { @Parameter(name = "currency", value = "USD") })
    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 types while bootstrapping the SessionFactory. Let’s understand this by registering the LocalDateString type we implemented earlier:

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

    MetadataSources metadataSources = new MetadataSources(serviceRegistry);
    Metadata metadata = metadataSources
      .addAnnotatedClass(OfficeEmployee.class)
      .getMetadataBuilder()
      .applyBasicType(LocalDateStringType.INSTANCE)
      .build();
    
    return metadata.buildSessionFactory()
}

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

6. Conclusion

In this tutorial, we discussed multiple approaches for defining a custom type in Hibernate. Additionally, we implemented a few custom types for our entity class based on some common use cases where a new custom type can come in handy.

As always the code samples are available on GitHub.