Java >> Java tutorial >  >> Tag >> hibernate

Forbedret OffsetDateTime og ZonedDateTime Mapping i Hibernate 6

At arbejde med tidsstempler med tidszoneoplysninger har altid været en kamp. Siden Java 8 introducerede Date and Time API, klasserne OffsetDateTime og ZonedDateTime er blevet de mest oplagte og mest brugte typer til at modellere et tidsstempel med tidszoneoplysninger. Og du kan forvente, at det er det eneste, du skal gøre at vælge en af ​​dem.

Men det er desværre ikke tilfældet, hvis du ønsker at bevare denne information i en relationel database. Selvom SQL-standarden definerer kolonnetypen TIMESTAMP_WITH_TIMEZONE , kun få databaser understøtter det. På grund af det understøtter JPA-specifikationen ikke OffsetDateTime og ZonedDateTime som attributtyper. Hibernate 5 understøtter dem ved at normalisere tidsstemplet og gemme det uden tidszoneoplysninger. Hibernate 6 forbedrer dette og giver en klar og fleksibel kortlægning for disse typer.

BEMÆRK:I Hibernate 6.0.0.Final er denne funktion stadig markeret som @Incubating og kan ændre sig i fremtidige udgivelser.

Sådan defineres tidszonehåndteringen

I Hibernate 6 kan du definere tidszonehåndteringen på 2 måder:

1. Du kan angive en standardhåndtering ved at indstille konfigurationsegenskaben hibernate.timezone.default_storage ejendom i din persistence.xml. TimeZoneStorageType enum definerer de understøttede konfigurationsværdier, som jeg diskuterer mere detaljeret i det følgende afsnit.

<persistence>
    <persistence-unit name="my-persistence-unit">
        <description>Hibernate example configuration - thorben-janssen.com</description>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="hibernate.timezone.default_storage" value="NORMALIZE"/>
			
			...
        </properties>
    </persistence-unit>
</persistence>

2. Du kan tilpasse tidszonehåndteringen af ​​hver enhedsattribut af typen ZonedDateTime eller OffsetDateTime ved at annotere det med @TimeZoneStorage og leverer en TimeZoneStorageType enum-værdi.

@Entity
public class ChessGame {
    
    @TimeZoneStorage(TimeZoneStorageType.NATIVE)
    private ZonedDateTime zonedDateTime;

    @TimeZoneStorage(TimeZoneStorageType.NATIVE)
    private OffsetDateTime offsetDateTime;
	
	...
}

5 forskellige TimezoneStorageTypes

Du kan vælge mellem 5 forskellige muligheder for at gemme tidszoneoplysninger. De beder Hibernate om at gemme tidsstemplet i en kolonne af typen TIMESTAMP_WITH_TIMEZONE , fasthold tidsstemplet og tidszonen i 2 separate kolonner, eller normaliser tidsstemplet til forskellige tidszoner. Jeg vil vise dig et eksempel på alle kortlægninger, og hvordan Hibernate håndterer dem i de følgende afsnit.

Alle eksempler vil være baseret på dette simple Skakspil enhedsklasse. Attributterne ZonedDateTime zonedDateTime og OffsetDateTime offsetDateTime skal gemme dagen og tidspunktet, hvor spillet blev spillet.

@Entity
public class ChessGame {
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    private ZonedDateTime zonedDateTime;

    private OffsetDateTime offsetDateTime;

    private String playerWhite;

    private String playerBlack;

    @Version
    private int version;
	
	...
}

Og jeg vil bruge denne testcase til at fortsætte et nyt Skakspil enhedsobjekt. Den indstiller zonedDateTime og offsetDateTime attributter til 2022-04-06 15:00 +04:00 . Efter at jeg har fastholdt entiteten, forpligter jeg transaktionen, starter en ny transaktion og henter den samme enhed fra databasen.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

ZonedDateTime zonedDateTime = ZonedDateTime.of(2022, 4, 6, 15, 0, 0, 0, ZoneId.of("UTC+4"));     
OffsetDateTime offsetDateTime = OffsetDateTime.of(2022, 4, 6, 15, 0, 0, 0, ZoneOffset.ofHours(4));

ChessGame game = new ChessGame();
game.setPlayerWhite("Thorben Janssen");
game.setPlayerBlack("A better player");
game.setZonedDateTime(zonedDateTime);
game.setOffsetDateTime(offsetDateTime);
em.persist(game);

em.getTransaction().commit();
em.close();


em = emf.createEntityManager();
em.getTransaction().begin();

ChessGame game2 = em.find(ChessGame.class, game.getId());
assertThat(game2.getZonedDateTime()).isEqualTo(zonedDateTime);
assertThat(game2.getOffsetDateTime()).isEqualTo(offsetDateTime);

em.getTransaction().commit();
em.close();

Lad os se nærmere på alle 5 TimeZoneStorageType muligheder.

TimeZoneStorageType.NATIVE

ADVARSEL:Da jeg forberedte eksemplerne til denne artikel ved hjælp af en h2-database, brugte Hibernate kolonnen timestamp(6) i stedet for tidsstemplet med tidszone. Dobbelttjek venligst, om Hibernate bruger den korrekte kolonnetype.

Det følgende afsnit beskriver den forventede adfærd.

Når du konfigurerer TimeZoneStorageType.NATIVE , Hibernate gemmer tidsstemplet i en kolonne af typen TIMESTAMP_WITH_TIMEZONE . Denne kolonnetype skal understøttes af din database.

@Entity
public class ChessGame {
    
    @TimeZoneStorage(TimeZoneStorageType.NATIVE)
    private ZonedDateTime zonedDateTime;

    @TimeZoneStorage(TimeZoneStorageType.NATIVE)
    private OffsetDateTime offsetDateTime;
	
	...
}

I dette tilfælde er håndteringen af ​​alle læseoperationer enkel, og der er ingen forskel på håndteringen af ​​enhver anden grundlæggende attributtype. Databasen gemmer tidsstemplet med tidszoneoplysninger. Hibernate skal blot indstille en ZonedDateTime eller OffsetDateTime objekt som en bindeparameter eller udtræk det fra resultatsættet.

13:10:55,725 DEBUG [org.hibernate.SQL] - insert into ChessGame (offsetDateTime, playerBlack, playerWhite, version, zonedDateTime, id) values (?, ?, ?, ?, ?, ?)
13:10:55,727 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [TIMESTAMP] - [2022-04-06T15:00+04:00]
13:10:55,735 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [2] as [VARCHAR] - [A better player]
13:10:55,735 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [3] as [VARCHAR] - [Thorben Janssen]
13:10:55,736 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [4] as [INTEGER] - [0]
13:10:55,736 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [5] as [TIMESTAMP] - [2022-04-06T15:00+04:00[UTC+04:00]]
13:10:55,736 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [6] as [BIGINT] - [1]
...
13:10:55,770 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.offsetDateTime,c1_0.playerBlack,c1_0.playerWhite,c1_0.version,c1_0.zonedDateTime from ChessGame c1_0 where c1_0.id=?
...
13:10:55,785 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [1] - [2022-04-06T13:00+02:00]
13:10:55,786 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [2] - [A better player]
13:10:55,786 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [3] - [Thorben Janssen]
13:10:55,786 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [0]
13:10:55,786 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [5] - [2022-04-06T13:00+02:00[Europe/Berlin]]
13:10:55,787 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [0]

TimeZoneStorageType.NORMALIZE

TimeZoneStorageType.NORMALIZE er den samme håndtering som leveret af Hibernate 5 og standardindstillingen i Hibernate 6.

@Entity
public class ChessGame {
    
    @TimeZoneStorage(TimeZoneStorageType.NORMALIZE)
    private ZonedDateTime zonedDateTime;

    @TimeZoneStorage(TimeZoneStorageType.NORMALIZE)
    private OffsetDateTime offsetDateTime;
	
	...
}

Den fortæller Hibernate at lade JDBC-driveren normalisere tidsstemplet til dens lokale tidszone eller tidszonen defineret i hibernate.jdbc.time_zone indstilling. Det gemmer derefter tidsstemplet uden tidszoneoplysninger i databasen.

Du kan ikke se dette, når du logger bindparameterværdierne for din INSERT-sætning. Hibernate her bruger stadig attributværdierne for dit enhedsobjekt.

11:44:00,815 DEBUG [org.hibernate.SQL] - insert into ChessGame (offsetDateTime, playerBlack, playerWhite, version, zonedDateTime, id) values (?, ?, ?, ?, ?, ?)
11:44:00,819 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [TIMESTAMP] - [2022-04-06T15:00+04:00]
11:44:00,838 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [2] as [VARCHAR] - [A better player]
11:44:00,839 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [3] as [VARCHAR] - [Thorben Janssen]
11:44:00,839 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [4] as [INTEGER] - [0]
11:44:00,839 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [5] as [TIMESTAMP] - [2022-04-06T15:00+04:00[UTC+04:00]]
11:44:00,840 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [6] as [BIGINT] - [1]

Men spor logning af ResourceRegistryStandardImpl klasse giver mere information om den udførte udarbejdede erklæring. Og der kan du se, at Hibernate normaliserede tidsstemplet fra 2022-04-06 15:00+04:00 til min lokale tidszone (UTC+2) og fjernede tidszoneforskydningen 2022-04-06 13:00:00 .

11:44:46,247 TRACE [org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl] - Closing prepared statement [prep3: insert into ChessGame (offsetDateTime, playerBlack, playerWhite, version, zonedDateTime, id) values (?, ?, ?, ?, ?, ?) {1: TIMESTAMP '2022-04-06 13:00:00', 2: 'A better player', 3: 'Thorben Janssen', 4: 0, 5: TIMESTAMP '2022-04-06 13:00:00', 6: 1}]

Når Hibernate læser tidsstemplet fra databasen, får JDBC-driveren tidsstemplet uden tidszoneoplysninger og tilføjer sin tidszone eller tidszonen defineret af hibernate.jdbc.time_zone indstilling.

11:55:17,225 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.offsetDateTime,c1_0.playerBlack,c1_0.playerWhite,c1_0.version,c1_0.zonedDateTime from ChessGame c1_0 where c1_0.id=?
11:55:17,244 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [1] - [2022-04-06T13:00+02:00]
11:55:17,245 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [2] - [A better player]
11:55:17,245 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [3] - [Thorben Janssen]
11:55:17,245 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [0]
11:55:17,245 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [5] - [2022-04-06T13:00+02:00[Europe/Berlin]]
11:55:17,247 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [0]

Som du kan se i log-outputtet, valgte Hibernate Skakspillet entity-objekt fra databasen og hentede det korrekte tidsstempel. Men på grund af den udførte normalisering er den ikke længere i tidszonen UTC+4, som jeg brugte, da jeg fortsatte entiteten. For at undgå tidszonekonverteringer skal du bruge TimeZoneStorageType.NATIVE eller TimeZoneStorageType.COLUMN .

Normalisering af tidsstempel kan være risikabelt

At normalisere dine tidsstempler og gemme dem uden tidszoneoplysninger kan virke som en simpel og indlysende løsning, hvis din database ikke understøtter kolonnetypen TIMESTAMP_WITH_TIMEZONE . Men det introducerer 2 risici:

  1. Hvis du ændrer din lokale tidszone eller kører servere i forskellige tidszoner, påvirker det denormaliseringen og resulterer i forkerte data.
  2. Tidszoner med sommertid kan ikke normaliseres sikkert, fordi de har 1 time, der eksisterer i sommer- og vintertid. Ved at fjerne tidszoneinformationen kan du ikke længere skelne mellem sommer- og vintertid, og du kan derfor ikke normalisere noget tidsstempel for den periode korrekt. For at undgå dette bør du altid bruge en tidszone uden sommertid, f.eks. UTC.

TimeZoneStorageType.NORMALIZE_UTC

ADVARSEL:Som beskrevet i HHH-15174 normaliserer Hibernate 6.0.0.Final ikke dit tidsstempel til UTC og anvender i stedet samme normalisering som TimeZoneStorageType.NORMALIZE .

Fejlen blev rettet i Hibernate 6.0.1.Final. Det følgende afsnit beskriver den korrekte adfærd.

TimeZoneStorageType.NORMALIZE_UTC er meget lig den tidligere omtalte TimeZoneStorageType.NORMALIZE . Den eneste forskel er, at dit tidsstempel bliver normaliseret til UTC i stedet for tidszonen for din JDBC-driver eller tidszonen konfigureret som hibernate.jdbc.time_zone .

@Entity
public class ChessGame {
    
    @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC)
    private ZonedDateTime zonedDateTime;

    @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC)
    private OffsetDateTime offsetDateTime;
	
	...
}

Hibernates håndtering af tidsstempler og den udførte normalisering under læse- og skriveoperationer er identisk med TimeZoneStorageType.NORMALIZE , som jeg forklarede meget detaljeret i det foregående afsnit.

TimeZoneStorageType.COLUMN

Når du konfigurerer TimeZoneStorageType.COLUMN , Hibernate gemmer tidsstemplet uden tidszoneoplysninger og tidszonens offset til UTC i separate databasekolonner.

@Entity
public class ChessGame {
    
    @TimeZoneStorage(TimeZoneStorageType.COLUMN)
	@TimeZoneColumn(name = "zonedDateTime_zoneOffset")
    private ZonedDateTime zonedDateTime;

    @TimeZoneStorage(TimeZoneStorageType.COLUMN)
	@TimeZoneColumn(name = "offsetDateTime_zoneOffset")
    private OffsetDateTime offsetDateTime;
	
	...
}

Hibernate bruger sin navnestrategi til at kortlægge entitetsattributten af ​​typen ZonedDateTime eller OffsetDateTime til en databasekolonne. Denne kolonne gemmer tidsstemplet. Som standard tilføjer Hibernate postfixet _tz til navnet på den kolonne for at få navnet på den kolonne, der indeholder tidszoneforskydningen. Du kan tilpasse dette ved at annotere din enhedsattribut med @TimeZoneColumn , som jeg gjorde i det forrige kodestykke.

Du kan tydeligt se denne håndtering, når du fortsætter med et nyt Skakspil entity-objekt og brug min anbefalede logningskonfiguration til udviklingsmiljøer.

12:31:45,654 DEBUG [org.hibernate.SQL] - insert into ChessGame (offsetDateTime, offsetDateTime_zoneOffset, playerBlack, playerWhite, version, zonedDateTime, zonedDateTime_zoneOffset, id) values (?, ?, ?, ?, ?, ?, ?, ?)
12:31:45,656 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [TIMESTAMP_UTC] - [2022-04-06T11:00:00Z]
12:31:45,659 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [2] as [INTEGER] - [+04:00]
12:31:45,660 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [3] as [VARCHAR] - [A better player]
12:31:45,660 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [4] as [VARCHAR] - [Thorben Janssen]
12:31:45,660 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [5] as [INTEGER] - [0]
12:31:45,660 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [6] as [TIMESTAMP_UTC] - [2022-04-06T11:00:00Z]
12:31:45,661 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [7] as [INTEGER] - [+04:00]
12:31:45,661 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [8] as [BIGINT] - [1]

Baseret på tidsstemplet og tidszoneforskydningen instansierer Hibernate en ny OffsetDateTime eller ZonedDateTime objekt, når det henter entitetsobjektet fra databasen.

12:41:26,082 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.offsetDateTime,c1_0.offsetDateTime_zoneOffset,c1_0.playerBlack,c1_0.playerWhite,c1_0.version,c1_0.zonedDateTime,c1_0.zonedDateTime_zoneOffset from ChessGame c1_0 where c1_0.id=?
...
12:41:26,094 DEBUG [org.hibernate.orm.results.loading.org.hibernate.orm.results.loading.embeddable] - Initializing composite instance [com.thorben.janssen.sample.model.ChessGame.offsetDateTime]
12:41:26,107 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [1] - [2022-04-06T11:00:00Z]
12:41:26,108 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [2] - [+04:00]
12:41:26,109 DEBUG [org.hibernate.orm.results.loading.org.hibernate.orm.results.loading.embeddable] - Created composite instance [com.thorben.janssen.sample.model.ChessGame.offsetDateTime] : 2022-04-06T15:00+04:00
...
12:41:26,109 DEBUG [org.hibernate.orm.results.loading.org.hibernate.orm.results.loading.embeddable] - Initializing composite instance [com.thorben.janssen.sample.model.ChessGame.zonedDateTime]
12:41:26,110 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [6] - [2022-04-06T11:00:00Z]
12:41:26,110 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [7] - [+04:00]
12:41:26,110 DEBUG [org.hibernate.orm.results.loading.org.hibernate.orm.results.loading.embeddable] - Created composite instance [com.thorben.janssen.sample.model.ChessGame.zonedDateTime] : 2022-04-06T15:00+04:00
...
12:41:26,112 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [3] - [A better player]
12:41:26,112 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [Thorben Janssen]
12:41:26,113 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [5] - [0]

TimeZoneStorageType.AUTO

Håndteringen af ​​TimeZoneStorageType.AUTO afhænger af Hibernates databasespecifikke dialekt. Hvis databasen understøtter kolonnetypen TIMESTAMP_WITH_TIMEZONE , Hibernate bruger TimeZoneStorageType.NATIVE . I alle andre tilfælde bruger Hibernate TimeZoneStorageType.COLUMN .

@Entity
public class ChessGame {
    
    @TimeZoneStorage(TimeZoneStorageType.AUTO)
    private ZonedDateTime zonedDateTime;

    @TimeZoneStorage(TimeZoneStorageType.AUTO)
    private OffsetDateTime offsetDateTime;
	
	...
}

Konklusion

Selvom SQL-standarden definerer kolonnetypen TIMESTAMP_WITH_TIMEZONE , ikke alle databaser understøtter det. Det gør håndteringen af ​​tidsstempler med tidszoneoplysninger overraskende kompleks.

Som jeg forklarede i en tidligere artikel, understøtter Hibernate 5 ZonedDateTime og OffsetDateTime som grundtyper. Det normaliserer tidsstemplet og gemmer det uden tidszoneoplysninger for at undgå problemer med databasekompatibilitet.

Hibernate 6 forbedrede denne håndtering ved at introducere flere kortlægningsmuligheder. Du kan nu vælge mellem:

  • TimeZoneStorageType.NATIVE for at gemme dit tidsstempel i en kolonne af typen TIMESTAMP_WITH_TIMEZONE ,
  • TimeZoneStorageType.NORMALIZE for at normalisere tidsstemplet til tidszonen for din JDBC-driver og bevare det uden tidszoneoplysninger,
  • TimeZoneStorageType.NORMALIZE_UTC for at normalisere tidsstemplet til UTC og bevare det uden tidszoneoplysninger,
  • TimeZoneStorageType.COLUMN at gemme tidsstemplet uden tidszoneoplysninger og forskydningen af ​​den angivne tidszone i 2 separate kolonner og
  • TimeZoneStorageType.AUTO at lade Hibernate vælge mellem TimeZoneStorageType.NATIVE og TimeZoneStorageType.COLUMN baseret på din databases muligheder.

Java tag