0

I am migrating to quarkus 3 and i couldnt find a way to serialize properly (like in hibernate 5) my OffsetDateTime for a field annotated with jsonb in an entity :

This is one of my entity, OffsetDateTime is not a problem here when persisting because i added the parameter hibernate-orm.mapping.timezone.default-storage="NORMALIZE" in configuration so i get my date right with the timezone

package myPackage;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.orange.erable.ohp.common.quarkus.rest.internal.identifier.models.IdentifierCharacteristicNameDTO;
import com.orange.erable.ohp.persistence.entities.enums.EIdentifierCategory;
import com.orange.erable.ohp.persistence.entities.enums.EIdentifierStatus;
import io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import lombok.Data;
import org.hibernate.annotations.Type;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.validation.constraints.NotNull;

import java.lang.reflect.Field;
import java.time.OffsetDateTime;
import java.util.UUID;

@Data
@Entity
@Table(name = "t_identifier")
public class IdentifierEntity extends PanacheEntityBase {

    @Id
    private String idohp;

    private String msisdn;

    private String idAdv;

    private String contractAid;

    private String countryCode;

    @NotNull
    @Enumerated(EnumType.STRING)
    @Type(PostgreSQLEnumType.class)
    @Column(columnDefinition = "status")
    private EIdentifierStatus status;

    @Enumerated(EnumType.STRING)
    @Type(PostgreSQLEnumType.class)
    @Column(columnDefinition = "category")
    private EIdentifierCategory category;

    @Column(updatable = false)
    private OffsetDateTime creationDate = OffsetDateTime.now();

    private OffsetDateTime updateDate = OffsetDateTime.now();

    @JsonInclude(JsonInclude.Include.NON_NULL)
    @Transient
    private boolean updated = false;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    @Transient
    private boolean created = false;
}

But for this entity which includes previous entity, the OffsetDateTime inside identifier are not persisted with the right format, it is persisted with UTC format, im missing my timezone:

package myPackage;

import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.*;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.time.OffsetDateTime;

@Data
@NoArgsConstructor
@Entity
@Table(name = "t_identifier_history")
public class IdentifierHistoryEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String idohp;

    @Type(JsonBinaryType.class)
    @Column(columnDefinition = "jsonb")
    private IdentifierEntity identifier;

    @Column(insertable = false, updatable = false)
    private OffsetDateTime creationDate = OffsetDateTime.now();

    @Column(insertable = false)
    private OffsetDateTime updateDate = OffsetDateTime.now();

    private String historyVersion;

    public IdentifierHistoryEntity(IdentifierEntity identifierEntity, String historyVersion) {
        this.idohp = identifierEntity.getIdohp();
        this.identifier = identifierEntity;
        this.historyVersion = historyVersion;
    }
}

I have tried setting @JdbcTypeCode( SqlTypes.JSON ) on identifier field

I have tried to create my own format mapper and refer to it with quarkus.hibernate-orm.unsupported-properties."hibernate.type.json_format_mapper"="myCustomFormatMapper" Here is the format mapper :

public class RegisterJacksonCustomizer implements FormatMapper {
    private final FormatMapper delegate = new JacksonJsonFormatMapper(createObjectMapper());

    private static ObjectMapper createObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        objectMapper.disable(SerializationFeature.WRAP_ROOT_VALUE);
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        final JavaTimeModule javaTimeModule = new JavaTimeModule();

        javaTimeModule.addSerializer(OffsetDateTime.class, new CustomOffsetDateTimeSerializer(DATE_TIME_FORMATTER));
        javaTimeModule.addDeserializer(OffsetDateTime.class, new CustomOffsetDateTimeDeserializer(DateTimeFormatter.ISO_OFFSET_DATE_TIME));

        objectMapper.registerModule(javaTimeModule);
        return objectMapper;
    }

    @Override
    public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
        return delegate.fromString(charSequence, javaType, wrapperOptions);
    }

    @Override
    public <T> String toString(T t, JavaType<T> javaType, WrapperOptions wrapperOptions) {
        return delegate.toString(t, javaType, wrapperOptions);
    }
}

Combined with this configuration :

quarkus:
  hibernate-orm:
    mapping:
      timezone:
        default-storage: "NORMALIZE"
    jdbc:
      timezone: "Europe/Paris"
    unsupported-properties:
      - "hibernate.type.json_format_mapper": "pathToMyMapper"
AJO
  • 31
  • 5

1 Answers1

0

I see several confusing things in your questions, so I'll address them separately.

Mapping as JSON most likely ignores object/relational mapping!

When you map a property as JSON/XML, you're sidestepping any relational mapping, so it's no surprise that the dates inside the jsonb identifier would ignore quarkus.hibernate-orm.mapping.timezone.default-storage. Same would happen if, say, an Integer column in IdentifierEntity was mapped as a string in the database: JSON serialization would still map it as an integer.

If you want to alter the way your entity gets serialized to JSON, you will have to use a custom FormatMapper, which doesn't have first-class support in Quarkus yet but has an unsupported workaround. You're claiming you tried, but if you want help on that, you should explain what didn't work exactly.

NORMALIZE will not preserve the timezone!

the OffsetDateTime inside identifier are not persisted with the right format, it is persisted with UTC format, im missing my timezone

You're using hibernate-orm.mapping.timezone.default-storage=NORMALIZE, so what you're getting is exactly the expected behavior: date/times are normalized to UTC.

See this section of the documentation:

NORMALIZE doesn’t store time zone information and will simply convert the timestamp to UTC

If you want to preserve the timezone, I'd suggest you use AUTO, which will use a native DB type that preserves the timezone if possible, or a separate column for the timezone otherwise.


Update after your edit

Your configuration is missing the quarkus prefix?

quarkus: # This is missing in your snippet
  hibernate-orm:
    mapping:
      timezone:
        default-storage: "NORMALIZE"
    jdbc:
       timezone: "Europe/Paris"
    unsupported-properties:
      - "hibernate.type.json_format_mapper": "pathToMyMapper"

I actually have my date right because the timezone is inserting when reading from database thanks to configuration but it is not persisted with timezone

Well that would be a problem since you'll lose the timezone on updates.

I'll reiterate:

If you want to preserve the timezone, I'd suggest you use AUTO, which will use a native DB type that preserves the timezone if possible, or a separate column for the timezone otherwise.

About your format mapper, sorry but we're getting too far from my area of expertise, let's home someone chimes in to tell you what's wrong in your implementation. You might want to show the code of CustomOffsetDateTimeSerializer.

yrodiere
  • 9,280
  • 1
  • 13
  • 35
  • Thanks a lot for your response. I added details concerning your first point in my post For your second point you are absolutely right i was misunderstanding how this worked. I actually have my date right because the timezone is inserting when reading from database thanks to configuration but it is not persisted with timezone – AJO Aug 24 '23 at 12:04
  • I have the prefix quarkus it was just at the top of the file, my bad. If i put AUTO, i dont have my timezone its not working at all. It seems like i would have to update my database and/or dialect – AJO Aug 24 '23 at 14:59
  • AUTO should work on any supported database/dialect, but you might need to update your DB schema if your DB/dialect doesn't have native support, because there'll be additional columns in your tables. If you're on a deprecated dialect or unsupported DB, yes, it's possible there is some bug. – yrodiere Aug 25 '23 at 08:13