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

(Last Updated On: October 23, 2018)

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:

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 static class YearAttributeConverter
        implements AttributeConverter<Year, Short> {

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

    @Override
    public Year convertToEntityAttribute(
            Short dbData) {
        return Year.of(dbData);
    }
}

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 hibernate-types 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 the type to the entity mapping 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;

    //Getters and setters omitted for brevity
}

Notice the @TypeDef annotation added at the entity level which instructs Hibernate to use the YearType to handle the Year entity attributes.

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

Conclusion

As you could 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 hibernate-types dependency to your project.

Subscribe to our Newsletter

* indicates required
10 000 readers have found this blog worth following!

If you subscribe to my newsletter, you'll get:
  • A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
  • 3 chapters from my book, High-Performance Java Persistence, 
  • a 10% discount coupon for my book. 
Get the most out of your persistence layer!

Advertisements

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.