How to map the Java YearMonth type with JPA and Hibernate

(Last Updated On: October 23, 2018)

Introduction

In this article, we are going to see how you can map a java.time.YearMonth with both JPA and Hibernate.

As I explained in this article, JPA 2.2 supports the following Date/Time types introduced in Java 8:

  • java.time.LocalDate
  • java.time.LocalTime
  • java.time.LocalDateTime
  • java.time.OffsetTime
  • java.time.OffsetDateTime

Apart from supporting those, Hibernate supports also:

  • java.time.Duration
  • java.time.Instant
  • java.time.ZonedDateTime

However, neither JPA nor Hibernate support the java.time.YearMonth out-of-the-box. As you will see, adding support for java.time.YearMonth is really straightforward for both standard JPA or Hibernate.

Domain Model

Let’s consider we have the following Book entity:

Mapping this entity to a database table requires choosing a column type for the YearMonth property. For this purpose we have the following options:

  • We could save it in a String column type (e.g., CHAR(6)), but that will require 6-byte storage.
  • We could save it as a Date column type, which requires 4 bytes.
  • We could save it in a 3 or 4-byte Integer column type.

Because the String alternative is the least efficient, we are going to choose the Date and the Integer alternatives instead.

Persisting YearMonth as a Date using JPA

When using JPA, we can use the AttributeConverter interface to define the conversion logic between the Java YearMonth type and the java.sql.Date one.

public class YearMonthDateAttributeConverter
        implements AttributeConverter<YearMonth, java.sql.Date> {

    @Override
    public java.sql.Date convertToDatabaseColumn(
            YearMonth attribute) {
        return java.sql.Date.valueOf(attribute.atDay(1));
    }

    @Override
    public YearMonth convertToEntityAttribute(
            java.sql.Date dbData) {
        return YearMonth
                .from(Instant.ofEpochMilli(dbData.getTime())
                .atZone(ZoneId.systemDefault())
                .toLocalDate());
    }
}

Now, we can map the entity as follows:

@Entity(name = "Book")
@Table(name = "book")
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String isbn;

    private String title;

    @Column(
        name = "published_on", 
        columnDefinition = "date"
    )
    @Convert(
        converter = YearMonthDateAttributeConverter.class
    )
    private YearMonth publishedOn;

    //Getters and setters omitted for brevity
}

When persisting a the following Book entity on PostgreSQL:

Book book = new Book();
book.setIsbn("978-9730228236");
book.setTitle("High-Performance Java Persistence");
book.setPublishedOn(YearMonth.of(2016, 10));

entityManager.persist(book);

Hibernate generates the following SQL INSERT statement:

INSERT INTO book (
    isbn, 
    published_on, 
    title, 
    id
)
VALUES (
    '978-9730228236', 
    '2016-10-01', 
    'High-Performance Java Persistence', 
    1
)

Notice the ‘2016-10-01’ value used for the published_on column.

And we can fetch the entity:

Book book = entityManager
.unwrap(Session.class)
.bySimpleNaturalId(Book.class)
.load("978-9730228236");

assertEquals(
    YearMonth.of(2016, 10), 
    book.getPublishedOn()
);

And the publishedOn property is going to be properly set by Hibernate.

We can also reference the publishedOn property in an entity query, like this one:

Book book = entityManager
.createQuery(
    "select b " +
    "from Book b " +
    "where " +
    "   b.title = :title and " +
    "   b.publishedOn = :publishedOn", Book.class)
.setParameter("title", "High-Performance Java Persistence")
.setParameter("publishedOn", YearMonth.of(2016, 10))
.getSingleResult();

Persisting YearMonth as a Date using Hibernate

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

public class YearMonthDateType
        extends AbstractSingleColumnStandardBasicType<YearMonth> {

    public static final YearMonthDateType INSTANCE = 
        new YearMonthDateType();

    public YearMonthDateType() {
        super(
            DateTypeDescriptor.INSTANCE,
            YearMonthTypeDescriptor.INSTANCE
        );
    }

    public String getName() {
        return "yearmonth-date";
    }

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

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

public class YearMonthTypeDescriptor
        extends AbstractTypeDescriptor<YearMonth> {

    public static final YearMonthTypeDescriptor INSTANCE = 
        new YearMonthTypeDescriptor();

    public YearMonthTypeDescriptor() {
        super(YearMonth.class);
    }

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

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

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

    @SuppressWarnings({"unchecked"})
    @Override
    public <X> X unwrap(
            YearMonth 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)) {
            Integer numericValue = 
                (value.getYear() * 100) + 
                value.getMonth().getValue();
                
            return (X) (numericValue);
        }
        if (Date.class.isAssignableFrom(type)) {
            return (X) java.sql.Date.valueOf(value.atDay(1));
        }
        throw unknownUnwrap(type);
    }

    @Override
    public <X> YearMonth wrap(
            X value, 
            WrapperOptions options) {
        if (value == null) {
            return null;
        }
        if (value instanceof String) {
            return fromString((String) value);
        }
        if (value instanceof Number) {
            int numericValue = ((Number) (value)).intValue();
            int year = numericValue / 100;
            int month = numericValue % 100;
            return YearMonth.of(year, month);
        }
        if (value instanceof Date) {
            Date date = (Date) value;
            return YearMonth
                .from(Instant.ofEpochMilli(date.getTime())
                .atZone(ZoneId.systemDefault())
                .toLocalDate());
        }
        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 YearMonthDateType in place, we only need to add the type to the entity mapping like this:

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(
    typeClass = YearMonthDateType.class, 
    defaultForType = YearMonth.class
)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String isbn;

    private String title;

    @Column(
        name = "published_on", 
        columnDefinition = "date"
    )
    @Convert(
        converter = YearMonthDateAttributeConverter.class
    )
    private YearMonth publishedOn;

    //Getters and setters omitted for brevity
}

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

Persisting YearMonth as an Integer using JPA

If you don’t want to store the YearMonth property as a DATE, you can use an Integer-based storage. In this case, you need an AttributeConverted that looks as follows:

public class YearMonthIntegerAttributeConverter
    implements AttributeConverter<YearMonth, Integer> {

    @Override
    public Integer convertToDatabaseColumn(
            YearMonth attribute) {
        return (attribute.getYear() * 100) + 
                attribute.getMonth().getValue();
    }

    @Override
    public YearMonth convertToEntityAttribute(
            Integer dbData) {
        int year = dbData / 100;
        int month = dbData % 100;
        return YearMonth.of(year, month);
    }
}

Now, we need to add the YearMonthIntegerAttributeConverter to the YearMonth property like this:

@Column(
    name = "published_on", 
    columnDefinition = "mediumint"
)
@Convert(
    converter = YearMonthIntegerAttributeConverter.class
)
private YearMonth publishedOn;

Notice that we used the MySQL MEDIUMINT column type which only requires 3 bytes, instead of 4 as it’s typical for the INTEGER column type.

Now, when saving the same Book entity on MySQL, Hibernate will generate the following SQL INSERT statement:

INSERT INTO book (
    isbn, 
    published_on, 
    title, 
    id
)
VALUES (
    '978-9730228236', 
    201610, 
    'High-Performance Java Persistence', 
    1
)

Notice that the 201610 integer value was stored for the associated YearMonth value.

Persisting YearMonth as an Integer using Hibernate

The same can be done using a Hibernate custom type which looks as follows:

public class YearMonthIntegerType
        extends AbstractSingleColumnStandardBasicType<YearMonth> {

    public static final YearMonthIntegerType INSTANCE = 
        new YearMonthIntegerType();

    public YearMonthIntegerType() {
        super(
            IntegerTypeDescriptor.INSTANCE,
            YearMonthTypeDescriptor.INSTANCE
        );
    }

    public String getName() {
        return "yearmonth-int";
    }

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

The IntegerTypeDescriptor SQL descriptor is provided by Hibernate while the YearMonthTypeDescriptor is the same Java class that we introduced when discussing the YearMonthDateType.

That’s it!

Now, just add the @TypeDef annotation to your entity, and you are done:

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(
    typeClass = YearMonthIntegerType.class, 
    defaultForType = YearMonth.class
)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String isbn;

    private String title;

    @Column(
        name = "published_on", 
        columnDefinition = "date"
    )
    @Convert(
        converter = YearMonthDateAttributeConverter.class
    )
    private YearMonth publishedOn;

    //Getters and setters omitted for brevity
}

What’s nice about the Hibernate-specific types is that you can easily provide them at the bootstrap time using the hibernate.type_contributors configuration property:

properties.put("hibernate.type_contributors",
    (TypeContributorList) () -> Collections.singletonList(
        (typeContributions, serviceRegistry) ->
            typeContributions.contributeType(
                YearMonthIntegerType.INSTANCE
            )
));

This way, you no longer need to provide the @TypeDef annotation, and the YearMonth attributes will automatically be handled by the YearMonthIntegerType custom Hibernate type.

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

4 thoughts on “How to map the Java YearMonth type with JPA and Hibernate

  1. Thanks for adding these classes to the hibernate-types project. I already used similar custom types, but now with the new release of hibernate-types I can finally remove a couple of my own custom types 🙂

  2. Please change:
    java.time.ZonedDateTimeType
    to
    java.time.ZonedDateTime

    As you know that class doesn’t exist 🙂

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.