14

There's hibernate-java8 JAR providing adapters for a couple of classes like Instant, LocalDate, etc., but some classes from java.time, e.g., Year, Month, YearMonth are missing. These classes get stored like an unknown Serializable, which is needlessly wasteful.

Surely I could use an int year instead of Year year, but I don't think, it's a good idea.

It looks like writing the YearJavaDescriptor should be pretty easy, however, I wonder why it's missing. Especially in case of YearMonth, I'd strongly prefer an existing adapter, ist there any? Or am I doing something stupid?

I'm unsure as googling returns nothing.

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
maaartinus
  • 44,714
  • 32
  • 161
  • 320
  • 1
    I think this answers your problem: http://stackoverflow.com/questions/40825495/java-time-yearmonth-as-a-type-within-entity – LLL Mar 31 '17 at 08:04
  • 1
    One piece of advice when serialising / encoding a type like `YearMonth`. [Think of your database statistics](https://blog.jooq.org/2016/10/05/why-you-should-design-your-database-to-optimise-for-statistics/), and perhaps avoid a human-readable format in favour of a better optimisable one. – Lukas Eder Mar 31 '17 at 09:43
  • @LukasEder Thank you, I've read your blogpost. IIUIC, you'd recommend something like `months since 2000` or `DATE('2017-03-01')` instead of `'2017-03'` or `201703` so that statistics work better. It's pity that databases have no custom types with `toString` / `fromString`, so we could have both readibility and speed. – maaartinus Mar 31 '17 at 09:52
  • 1
    @maaartinus: Yes, that's my recommendation. The `DATE` type (along with a `CHECK` constraint enforcing the day value to be 1) is probably a good compromise in this case. PostgreSQL knows `domain`, a tool to make `CHECK` constraints reusable: https://www.postgresql.org/docs/current/static/sql-createdomain.html. Still no `toString()` / `fromString()` formatting option, but at least it's easier to document... Note, my advice is only important if you query large amounts of data with this type as predicate. If you just store the value, then this optimisation might be premature. – Lukas Eder Mar 31 '17 at 10:12
  • @nazar_art Sort of.... I'm rather confused about what's happening: Before, both `Year` and `Month` were stored as a `Serializable`. I wrote a `YearJavaDescriptor extends AbstractTypeDescriptor` and now *both* are stored as `int`. I don't have time now to investigate further. – maaartinus Apr 06 '17 at 21:24

3 Answers3

8

Try to create a converter - AttributeConverter implementation for this purpose.

I used in the past something like following:

@Entity
public class RealEstateAgency {
    @Column(name = "createdAt")
    @Convert(converter = ZonedDateTimeConverter.class)
    private ZonedDateTime creationDate;
}

@Converter(autoApply = true)
public class ZonedDateTimeConverter implements AttributeConverter<ZonedDateTime, Date> {
 
    public Date convertToDatabaseColumn(ZonedDateTime toConvert) {
        return toConvert == null ? null : Date.from(toConvert.toInstant());
    }
 
    public ZonedDateTime convertToEntityAttribute(Date toConvert) {
        return toConvert == null ? null : ZonedDateTime.from(toConvert.toInstant());
    }
}
catch23
  • 17,519
  • 42
  • 144
  • 217
  • It's important to remember that it has to be `java.sql.Date`. Stuff like `LocalDate` although independently correctly serialized by Hibernate does not seem to work in this case. – Dcortez Jun 08 '18 at 07:34
4

If your JPA provider does not persist a type in a sensible manner (in this case because it is a Java8 class, which was added after JPA 2.1 was approved) then you need to define a JPA 2.1 AttributeConverter to convert it to a standard JPA persistable type (in this case something like java.sql.Date).

Anuj Teotia
  • 1,303
  • 1
  • 15
  • 21
2

Using JPA AttributeConverter

For YearMonth, you can create the following YearMonthDateAttributeConverter like this:

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());
    }
}

And, use @Convert on the YearMonth JPA entity property:

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

Using a Hibernate custom Type

You can create a custom Hibernate Type by extending the AbstractSingleColumnStandardBasicType:

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;
    }
}

And define the YearMonthTypeDescriptor 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 (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 Date) {
            Date date = (Date) value;
            return YearMonth
                .from(Instant.ofEpochMilli(date.getTime())
                .atZone(ZoneId.systemDefault())
                .toLocalDate());
        }
        throw unknownWrap(value.getClass());
    }
}

You don't need to write this type by yourself, you can use the Hibernate Types and just declare the Maven dependency like this:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

That's it!

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911