How to map the PostgreSQL inet type with JPA and Hibernate

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, we are going to see how to map the PostgreSQL inet type with JPA and Hibernate. Traditionally, PostgreSQL has been offering more column types than other relational database systems.

And you don’t even have to implement these types I’m presenting here since they are available via the Hypersistence Utils project.

Inet column type

The PostgreSQL inet type allows you to store Network addresses with both the IP address (IPv4 or IPv6) and the subnet as well.

While you could store a network address as VARCHAR or as a series of bytes or as a numeric type, the inet is more compact and allows you to use various network functions.

While the inet column type is used to store the network address on the database side, in the Domain Model, we are going to use the Inet class type instead:

public class Inet 
        implements Serializable {

    private final String address;

    public Inet(String address) {
        this.address = address;
    }

    public String getAddress() {
        return address;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) 
            return true;
            
        if (o == null || getClass() != o.getClass()) 
            return false;

        Inet inet = (Inet) o;

        return address != null ? 
                address.equals(inet.address) : 
                inet.address == null;
    }

    @Override
    public int hashCode() {
        return address != null ? 
                address.hashCode() : 
                0;
    }

    public InetAddress toInetAddress() {
        try {
            String host = address.replaceAll(
                "\\/.*$", ""
            );
            
            return Inet4Address.getByName(host);
        } catch (UnknownHostException e) {
            throw new IllegalStateException(e);
        }
    }
}

You don’t have to create the Inet class in your application as long as you are using the Hypersistence Utils project.

Inet Hibernate Type

When mapping a Hibernate custom Type, you have two options:

Using the former strategy, the PostgreSQLInetType looks as follows:

public class PostgreSQLInetType extends ImmutableType<Inet> {

    public PostgreSQLInetType() {
        super(Inet.class);
    }

    @Override
    public int[] sqlTypes() {
        return new int[]{
            Types.OTHER
        };
    }

    @Override
    public Inet get(
            ResultSet rs, 
            String[] names, 
            SessionImplementor session, 
            Object owner
        ) throws SQLException {
        String ip = rs.getString(names[0]);
        return (ip != null) ? new Inet(ip) : null;
    }

    @Override
    public void set(
            PreparedStatement st, 
            Inet value, 
            int index, 
            SessionImplementor session
        ) throws SQLException {
        if (value == null) {
            st.setNull(index, Types.OTHER);
        } else {
            Object holder = ReflectionUtils.newInstance(
                "org.postgresql.util.PGobject"
            );
            ReflectionUtils.invokeSetter(
                holder, 
                "type", 
                "inet"
            );
            ReflectionUtils.invokeSetter(
                holder, 
                "value", 
                value.getAddress()
            );
            st.setObject(index, holder);
        }
    }
}

The best way to understand why it’s worth extending the ImmutableType offered by the Hypersistence Utils project is to take a look at the following class diagram:

PostgreSQL Inet Type

Notice that the vast majority of the UserType methods are handled by the ImmutableType abstract base class while the PostgreSQLInetType just has to implement three methods only.

Maven dependency

As already mentioned, you don’t need to create the aforementioned classes. You can get them via the Hypersistence Utils Maven dependency:

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-55</artifactId>
    <version>${hypersistence-utils.version}</version>
</dependency>

If you’re using older versions of Hibernate, check out the Hypersistence Utils GitHub repository for more info about the matching dependency for your current Hibernate version.

Domain Model

Let’s assume our application needs to track the IP addresses of the clients connecting to our production systems. The Event entity will encapsulate the IP address as in the following example.

For Hibernate 6, the mapping will look as follows:

@Entity(name = "Event")
@Table(name = "event")
public class Event {

    @Id
    @GeneratedValue
    private Long id;

    @Type(PostgreSQLInetType.class)
    @Column(
        name = "ip", 
        columnDefinition = "inet"
    )
    private Inet ip;
}

And for Hibernate 5, like this:

@Entity(name = "Event")
@Table(name = "event")
@TypeDef(typeClass = PostgreSQLInetType.class, defaultForType = Inet.class)
public class Event {

    @Id
    @GeneratedValue
    private Long id;

    @Column(
        name = "ip", 
        columnDefinition = "inet"
    )
    private Inet ip;
}

Notice the use of the @TypeDef annotation, which tells Hibernate to use the PostgreSQLInetType Hibernate Type for handling the Inet entity properties.

Testing time

Now, when persisting the following two Event entities:

entityManager.persist(new Event());

Event event = new Event();
event.setIp("192.168.0.123/24");

entityManager.persist(event);

Hibernate generates the following SQL INSERT statements:

INSERT INTO event (ip, id) VALUES (NULL(OTHER), 1)

INSERT INTO event (ip, id) VALUES ('192.168.0.123/24', 2)

Notice that the first INSERT statement sets the ip column to NULL just like its associated entity property while the second INSERT statement sets the ip column accordingly. Even if the parameter is logged as a String, on the database site, the column type is inet and the value is stored in a parsed binary format.

When fetching the second Event entity, we can see that the ip attribute is properly retrieved from the underlying inet database column:

Event event = entityManager.find(Event.class, 2L);

assertEquals(
    "192.168.0.123/24", 
    event.getIp().getAddress()
);

assertEquals(
    "192.168.0.123", 
    event.getIp().toInetAddress().getHostAddress()
);

What’s nice about the inet column type is that we can use network address-specific operators like the && one, which verifies if the address on the left-hand side belongs to the subnet address on the right-hand side:

Event event = (Event) entityManager.createNativeQuery("""
    SELECT e.*
    FROM event e
    WHERE e.ip && CAST(:network AS inet) = true
    """, Event.class)
.setParameter("network", "192.168.0.1/24")
.getSingleResult();

assertEquals(
    "192.168.0.123/24", 
    event.getIp().getAddress()
);

Cool, right?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

Mapping non-standard database column types is fairly easy with Hibernate. However, with the help of the Hypersistence Utils project, you don’t even have to write all these types.

Just add the Maven dependency to your project pom.xml configuration file and add the @Type annotation to the entity attribute in question.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.