How to map java.time Year and Month 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 you can map the java.time.Year and java.time.Month with both JPA and Hibernate.

As explained in this article, JPA 2.2 supports LocalDate, LocalTime, LocalDateTime, OffsetTime, OffsetDateTime from the java.time package. Hibernate has been supporting the Java 8 Date/Time classes since 5.0 via the hibernate-java8 dependency, but since version 5.2, this dependency was merged with hibernate-core so you get the Duration, Instant and ZonedDateTime types in addition to the ones supported by JPA 2.2.

However, neither JPA nor Hibernate supports the java.time.Year type out-of-the-box. As you will see, adding support for java.time.Year is very easy with both standard JPA or Hibernate.

Domain Model

Let’s assume we have the following Publisher entity which defines an estYear property of type java.time.Year and a salesMonth property of the type java.time.Month:

How to map java.time Year and Month with JPA and Hibernate

Since the java.time.Month is a Java Enum, we can use the @Enumarated annotation to instruct Hibernate to handle this property as an enumeration. If you want to persist the property in an Integer column, you don’t need to specify the javax.persistence.EnumType.ORDINAL since this is the default strategy. However, if you wish to persist the Enum as a String column type, you need to use the @Enumarated(EnumType.STRING) annotation instead.

Because we want to persist the salesMonth in a smallint column, we can map this property like this:

@Column(
    name = "sales_month", 
    columnDefinition = "smallint"
)
@Enumerated
private Month salesMonth;

Since, by default, Hibernate does not support the java.time.Year object type, we will have to instruct Hibernate how to handle the estYear property, and this can be done either via a JPA AttributeConverter or using a Hibernate-specific type.

Persisting java.time.Year using JPA

When using JPA, we can use the AttributeConverter interface to define the conversion logic between the Java Year type and the integer-based column type.

@Converter(autoApply = true)
public class YearAttributeConverter
        implements AttributeConverter<Year, Short> {

    @Override
    public Short convertToDatabaseColumn(
            Year attribute) {
        if (attribute != null) {
            return (short) attribute.getValue();
        }
        return null;
    }

    @Override
    public Year convertToEntityAttribute(
            Short dbData) {
        if (dbData != null) {
            return Year.of(dbData);
        }
        return null;
    }
}

We can use Short instead of Integer since we chose the smallint column type on the database side which takes 2 bytes instead of 4 and takes numeric values from -32768 to 32767 which is sufficient for most applications that need to persist a java.time.Year property.

Now you can instruct Hibernate to use the YearAttributeConverter using the @Convert annotation, and the Publisher entity will look as follows:

@Entity(name = "Publisher")
@Table(name = "publisher")
public class Publisher {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String name;

    @Column(
        name = "est_year", 
        columnDefinition = "smallint"
    )
    @Convert(
        converter = YearAttributeConverter.class
    )
    private Year estYear;

    @Column(
        name = "sales_month", 
        columnDefinition = "smallint"
    )
    @Enumerated
    private Month salesMonth;

    //Getters and setters omitted for brevity
}

When persisting a the following Publisher entity on MySQL:

Publisher publisher = new Publisher();
publisher.setName("vladmihalcea.com");
publisher.setEstYear(Year.of(2013));
publisher.setSalesMonth(Month.NOVEMBER);

entityManager.persist(publisher);

Hibernate generates the following SQL INSERT statement:

INSERT INTO publisher (
    est_year, 
    name, 
    sales_month, 
    id
)
VALUES (
    2013,
    'vladmihalcea.com', 
    10, 
    1
)

Notice the ‘2013’ value used for the est_year column and the 10 value used for Month.NOVEMBER for the sales_month column.

And when fetching the Publisher entity:

Publisher publisher = entityManager
.unwrap(Session.class)
.bySimpleNaturalId(Publisher.class)
.load("vladmihalcea.com");

assertEquals(
	Year.of(2013), 
	publisher.getEstYear()
);
assertEquals(
	Month.NOVEMBER, 
	publisher.getSalesMonth()
);

We can see that the estYear and salesMonth properties are properly set by Hibernate.

Persisting java.time.Year using Hibernate

You can achieve the same goal using a Hibernate custom type:

public class YearType
        extends AbstractSingleColumnStandardBasicType<Year> {

    public static final YearType INSTANCE = new YearType();

    public YearType() {
        super(
            SmallIntTypeDescriptor.INSTANCE,
            YearTypeDescriptor.INSTANCE
        );
    }

    public String getName() {
        return "year";
    }

    @Override
    protected boolean registerUnderJavaType() {
        return true;
    }
}

While the SmallIntTypeDescriptor is a Hibernate SQL descriptor provided via the hibernate-core dependency, the YearTypeDescriptor is a class that I wrote for myself and looks as follows:

public class YearTypeDescriptor
        extends AbstractTypeDescriptor<Year> {

    public static final YearTypeDescriptor INSTANCE = 
        new YearTypeDescriptor();

    public YearTypeDescriptor() {
        super(Year.class);
    }

    @Override
    public boolean areEqual(
            Year one, 
            Year another) {
        return Objects.equals(one, another);
    }

    @Override
    public String toString(
            Year value) {
        return value.toString();
    }

    @Override
    public Year fromString(
            String string) {
        return Year.parse(string);
    }

    @SuppressWarnings({"unchecked"})
    @Override
    public <X> X unwrap(
            Year value, 
            Class<X> type, 
            WrapperOptions options) {
        if (value == null) {
            return null;
        }
        if (String.class.isAssignableFrom(type)) {
            return (X) toString(value);
        }
        if (Number.class.isAssignableFrom(type)) {
            Short numericValue = (short) value.getValue();
            return (X) (numericValue);
        }
        throw unknownUnwrap(type);
    }

    @Override
    public <X> Year wrap(
            X value, 
            WrapperOptions options) {
        if (value == null) {
            return null;
        }
        if (value instanceof String) {
            return fromString((String) value);
        }
        if (value instanceof Number) {
            short numericValue = ((Number) (value)).shortValue();
            return Year.of(numericValue);
        }
        throw unknownWrap(value.getClass());
    }
}

You don’t have to write these Hibernate type yourself since it’s already available via the Hypersistence Utils project, which offers support for JSON, ARRAY, DB-specific Enums, PostgreSQL INET type and more.

With the YearType in place, we only need to add it to the entity mapping.

For Hibernate 6, the mapping will look as follows:

@Entity(name = "Publisher")
@Table(name = "publisher")
public class Publisher {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String name;

    @Type(YearType.class)
    @Column(
        name = "est_year", 
        columnDefinition = "smallint"
    )
    private Year estYear;

    @Column(
        name = "sales_month", 
        columnDefinition = "smallint"
    )
    @Enumerated
    private Month salesMonth;
}

And for Hibernate 5, like this:

@Entity(name = "Publisher")
@Table(name = "publisher")
@TypeDef(typeClass = YearType.class, defaultForType = Year.class)
public class Publisher {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String name;

    @Column(
        name = "est_year", 
        columnDefinition = "smallint"
    )
    private Year estYear;

    @Column(
        name = "sales_month", 
        columnDefinition = "smallint"
    )
    @Enumerated
    private Month salesMonth;
}

The @Type and @TypeDef annotations instruct Hibernate to use the YearType to handle the Year entity attributes.

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

As you can see, even if Hibernate does not provide a Year or a Month type out-of-the-box, mapping one is not very difficult.

The AttributeConverter works with any JPA provider while the Hibernate-specific type is very convenient if you add the Hypersistence Utils dependency to your project.

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.