Custom Hibernate Types with @Type Annotation Guide

Paul Ravvich
Hibernate At the Gates of Mastery
3 min readApr 14, 2024

--

One of the most powerful yet underappreciated of these annotations is @Type. This article delves into how you can create and utilize custom types in Hibernate, using the example of mapping an IPAddress class to a database column through the IPAddressType custom type.

Custom Hibernate Types with @Type Annotation Guide

Hi, this is Paul, and welcome to this article, we explore how to create your Custom Hibernate Type.

Introduction to Custom Types

Custom types in Hibernate allow developers to map fields in an entity to SQL types in a way that doesn’t fit the traditional Java-to-SQL data types. The @Type annotation specifies the custom type class that Hibernate should use to map a field of an entity to a corresponding database column. This is particularly useful for complex types or when you need to add custom behavior during the persistence process.

Case Study: Mapping IP Addresses

Consider a simple scenario where our application needs to store device information, including IP addresses, in a database. Our example uses the following table definition:

CREATE TABLE devices
(
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address VARCHAR(255) NOT NULL
);

The IPAddress Entity

We define an IPAddress class to encapsulate the IP address in our domain model. This class only contains a string representing the IP address:

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@AllArgsConstructor
public class IPAddress {
private String address;
}

The IPAddressType Custom Type

To integrate this class with Hibernate, we use a custom type, IPAddressType, which implements the UserType interface. This type handles SQL interactions and ensures the IP address format is correctly managed:

import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Properties;

public class IPAddressType implements UserType<IPAddress>, ParameterizedType {
private String scheme = "https://";

@Override
public int getSqlType() {
return Types.VARCHAR;
}

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

@Override
public boolean equals(IPAddress x, IPAddress y) {
return x != null && x.equals(y);
}

@Override
public int hashCode(IPAddress x) {
return x.hashCode();
}

@Override
public IPAddress nullSafeGet(ResultSet rs, int column, SharedSessionContractImplementor session, Object owner) throws SQLException {
String address = rs.getString(column);
if (address != null && !address.startsWith(scheme)) {
address = scheme + address;
}
return address != null ? new IPAddress(address) : null;
}

@Override
public void nullSafeSet(PreparedStatement st, IPAddress value, int index, SharedSessionContractImplementor session) throws SQLException {
if (value == null || value.getAddress() == null) {
st.setNull(index, Types.VARCHAR);
} else {
String address = value.getAddress();
if (!address.startsWith("http://") && !address.startsWith("https://")) {
address = scheme + address;
}
st.setString(index, address);
}
}

@Override
public IPAddress deepCopy(IPAddress value) {
return new IPAddress(value.getAddress());
}

@Override
public boolean isMutable() {
return true;
}

@Override
public Serializable disassemble(IPAddress value) {
return value.getAddress();
}

@Override
public IPAddress assemble(Serializable cached, Object owner) {
return new IPAddress((String) cached);
}

@Override
public IPAddress replace(IPAddress detached, IPAddress managed, Object owner) {
return deepCopy(detached);
}

@Override
public void setParameterValues(Properties parameters) {
String paramScheme = parameters.getProperty("scheme");
if (paramScheme != null && (paramScheme.equals("http") || paramScheme.equals("https"))) {
scheme = paramScheme + "://";
}
}
}

Using @Type in the Device Entity

In our Device entity, we then use the @Type annotation to declare that the ipAddress our custom should handle field IPAddressType:

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.Type;

import java.util.Objects;

@Entity
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "devices")
public class Device {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "name")
private String name;

@Column(name = "address")
@Type(value = IPAddressType.class, parameters = {
@Parameter(name = "scheme", value = "http")
})
private IPAddress ipAddress;

// equals and hashCode
}

Benefits of Using @Type

The use of @Type provides several benefits:

  1. Flexibility: Allows custom handling of database operations for specific types.
  2. Reusability: Custom types can be reused across different entities or projects.
  3. Consistency: Ensures that data formatting and behavior are consistent throughout the application.

Conclusion

In this example, we demonstrated how the @Type annotation in Hibernate can be used to create a custom type for managing IP addresses. This approach is not limited to IP addresses but can be extended to other types requiring special handling or having unique behaviors. Utilizing custom types enhances the modularity, clarity, and maintainability of the application code, making Hibernate a more powerful tool in the arsenal of Java developers.

Thank you for reading until the end. Before you go:

Paul Ravvich

--

--

Paul Ravvich
Hibernate At the Gates of Mastery

Software Engineer with over 10 years of XP. Join me for tips on Programming, System Design, and productivity in tech! New articles every Tuesday and Thursday!