java.time Classes In JPA

Before Java 8, Java had been notoriously known for poor time API. This has been luckily fixed in Java 8 by introducting Date and Time API. It consists of classes in the java.time package. The most used ones are LocalDate, LocalTime and LocalDateTime for representing date, time, and timestamp without timezone, respectively.

In this article, we will talk about how to use java.time classes in JPA. Before we do so, let's talk about how JPA handles data types in general.

How JPA Handles Data Types

When JPA stores an object in a database, it converts Java data types to SQL datatypes, for instance:

  • Java int becomes SQL integer
  • java.util.String becomes SQL varchar,
  • java.math.BigDecimal becomes SQL numeric, etc.

For each data type, JPA uses a dedicated converter to carry out the conversion. But what if there is no converter for a given data type? For instance, imagine you have created ComplexNumber data type for storing complex numbers

class ComplexNumber {
  double realPart;
  double imaginaryPart;
}
Furthermore imagine you use it in the entity Solution
@Entity
class Solution {
  @Id
  Integer id;
  ComplexNumber solution;
}

By default JPA has no converter for ComplexNumber. The best option it has is to convert it to the most generic data type possible. For instance, if you use PostgreSQL, JPA chooses bytea data type (byte array) and maps the Solution class to the following table

CREATE TABLE solution (
  id int primary key,
  solution bytea
)

What does it mean? You still have the ability to store ComplexNumers in a database. You, however, loose the ability to work with the solution column outside of JPA as only JPA knows how to decode ComplexNumer from the bytea. Therefore you are not able to use plain SQL to accesses the column.

If you are not safisfied with this kind of behaviour, you can provide JPA with a custom data converter. A custom data converter converts a custom data type to an SQL data type. Since we are in Java, a converter cannot use SQL data types, but must use corresponding Java types. The following example shows a converter that converts ComplexNumber to java.util.String, which is in turn converted to SQL varchar.

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class ComplexNumberAttributeConverter implements AttributeConverter<ComplexNumber, String> {

  @Override
  public String convertToDatabaseColumn(ComplexNumber cn) {
    return cn == null ? null : cn.realParn + ":" + cn.imaginaryPart;
  }

  @Override
  public ComplexNumber convertToEntityAttribute(String string) {
    if (string == null) {
	  return null;
	}

	String[] parts = string.split(":");
	if (part.length != 2) {
	  throw new IllegalArgumentException();
	}

	ComplexNumber cn = new ComplexNumber();
	cn.realPart = Double.parseDouble(part[0]);
	cn.imaginaryPart = Double.parseDouble(part[1]);
	
    return cn;
  }
}

ComplexNumberAtrributeConverter uses convertToDatabaseColumn method to convert a custom data type to SQL data type. On the other hand, convertToEntityAttribute is used to convert SQL data type to a custom data type. @Converter annotation is used to tell JPA to automatically apply the converter to each ComplexNumber field.

java.time Prior To JPA 2.2

JPA 2.1 and earlier do not recognize any of the java.time classes. Therefore the situation is the same as in the case of the ComplexNumber data type. By default LocalDate, LocalTime, LocalDateTime and other java.time classes are converted to bytea.

It makes, however, sense to store LocalDate, LocalTime, and LocalDateTime to a databse as SQL date, time, and timestamp, respectively. You can achieve this behaviour by writing the following converters

import java.sql.Date;
import java.time.LocalDate;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class LocalDateAttributeConverter implements AttributeConverter<LocalDate, Date> {

  @Override
  public Date convertToDatabaseColumn(LocalDate value) {
    return (value == null ? null : Date.valueOf(value));
  }

  @Override
  public LocalDate convertToEntityAttribute(Date value) {
    return (value == null ? null : value.toLocalDate());
  }
}
import java.sql.Timestamp;
import java.time.LocalDateTime;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class LocalDateTimeAttributeConverter implements AttributeConverter<LocalDateTime, Timestamp> {

  @Override
  public Timestamp convertToDatabaseColumn(LocalDateTime value) {
    return (value == null ? null : Timestamp.valueOf(value));
  }

  @Override
  public LocalDateTime convertToEntityAttribute(Timestamp value) {
    return (value == null ? null : value.toLocalDateTime());
  }
}
import java.sql.Time;
import java.time.LocalTime;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class LocalTimeAttributeConverter implements AttributeConverter<LocalTime, Time> {

  @Override
  public Time convertToDatabaseColumn(LocalTime value) {
    return (value == null ? null : Time.valueOf(value));
  }

  @Override
  public LocalTime convertToEntityAttribute(Time value) {
    return (value == null ? null : value.toLocalTime());
  }
}
import java.sql.Timestamp;
import java.time.Instant;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class InstantAttributeConverter implements AttributeConverter<Instant, Timestamp> {

  @Override
  public Timestamp convertToDatabaseColumn(Instant value) {
    return value == null ? null : Timestamp.from(value);
  }

  @Override
  public Instant convertToEntityAttribute(Timestamp value) {
    return value == null ? null : value.toInstant();
  }
}

java.time Since JPA 2.2

If you are using JPA 2.2 and later, you are lucky. You need not do anything as JPA 2.2 already includes converters for java.time classes.