I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

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</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.3.6.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, that stores LocalDate Java type as 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";
    }
}

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) LocalDateType.FORMATTER.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(LocalDateType.FORMATTER.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(type = "com.baeldung.hibernate.customtypes.LocalDateStringType")
    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.

So let’s implement a complex PhoneNumber object by implementing 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
}	

Here, the overridden sqlTypes method returns the SQL types of fields, in the same order as they are declared in our PhoneNumber class. Similarly, returnedClass method returns our PhoneNumber 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 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;
}

Next, the 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());
    }
}

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

@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. 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.

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 amount into 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 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 over on GitHub.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

Leave a Reply

avatar
  Subscribe  
Notify of