Custom Types in Hibernate and the @Type Annotation

 

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

 

Leave a Reply

Your email address will not be published.