Bagaimana menyimpan tanggal / waktu dan cap waktu di zona waktu UTC dengan JPA dan Hibernate

106

Bagaimana cara mengkonfigurasi JPA / Hibernate untuk menyimpan tanggal / waktu dalam database sebagai zona waktu UTC (GMT)? Pertimbangkan entitas JPA beranotasi ini:

public class Event {
    @Id
    public int id;

    @Temporal(TemporalType.TIMESTAMP)
    public java.util.Date date;
}

Jika tanggal 2008-Feb-03 9:30 Pacific Standard Time (PST), maka saya ingin waktu UTC 2008-Feb-03 17:30 disimpan dalam database. Demikian juga, ketika tanggal diambil dari database, saya ingin itu ditafsirkan sebagai UTC. Jadi dalam hal ini jam 530 sore adalah jam 530 sore UTC. Saat itu ditampilkan, itu akan diformat sebagai 9:30 PST.

Steve Kuo
sumber
1
Jawaban Vlad Mihalcea memberikan jawaban yang diperbarui (untuk Hibernate 5.2+)
Dinei

Jawaban:

73

Dengan Hibernate 5.2, Anda sekarang dapat memaksa zona waktu UTC menggunakan properti konfigurasi berikut:

<property name="hibernate.jdbc.time_zone" value="UTC"/>

Untuk lebih jelasnya, lihat artikel ini .

Vlad Mihalcea
sumber
19
Saya juga menulis yang itu: D Sekarang, tebak siapa yang menambahkan dukungan untuk fitur ini di Hibernate?
Vlad Mihalcea
Oh, baru saja saya menyadari nama dan gambar di profil Anda dan artikel itu sama ...
Kerja
@VladMihalcea jika itu untuk Mysql, seseorang perlu memberitahu MySql untuk menggunakan zona waktu dengan menggunakan useTimezone=truedalam string koneksi. Kemudian hanya properti pengaturan yang hibernate.jdbc.time_zoneakan bekerja
TheCoder
Sebenarnya, Anda perlu menyetel useLegacyDatetimeCodeke false
Vlad Mihalcea
2
hibernate.jdbc.time_zone tampaknya diabaikan atau tidak berpengaruh saat digunakan dengan PostgreSQL
Alex R
48

Sepengetahuan saya, Anda perlu meletakkan seluruh aplikasi Java Anda di zona waktu UTC (sehingga Hibernate akan menyimpan tanggal dalam UTC), dan Anda harus mengonversi ke zona waktu apa pun yang diinginkan saat Anda menampilkan sesuatu (setidaknya kami melakukannya cara ini).

Saat memulai, kami melakukan:

TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));

Dan atur zona waktu yang diinginkan ke DateFormat:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
mitchnull.dll
sumber
5
mitchnull, solusi Anda tidak akan berfungsi di semua kasus karena delegasi Hibernasi mengatur tanggal ke driver JDBC dan setiap driver JDBC menangani tanggal dan zona waktu secara berbeda. lihat stackoverflow.com/questions/4123534/… .
Derek Mahar
2
Tetapi jika saya memulai aplikasi saya dengan menginformasikan ke properti JVM "-Duser.timezone = + 00: 00", bukankah berperilaku sama?
rafa.ferreira
8
Sejauh yang saya tahu, ini akan berfungsi di semua kasus kecuali jika JVM dan server database berada di zona waktu yang berbeda.
Shane
stevekuo dan @mitchnull lihat solusi divestoclimb di bawah ini yang jauh lebih baik dan bukti efek samping stackoverflow.com/a/3430957/233906
Cerber
Apakah HibernateJPA mendukung anotasi "@Factory" dan "@Externalizer", begitulah cara saya melakukan penanganan utc datetime di perpustakaan OpenJPA. stackoverflow.com/questions/10819862/…
Siapa
44

Hibernate mengabaikan hal-hal zona waktu di Tanggal (karena tidak ada), tetapi sebenarnya lapisan JDBC yang menyebabkan masalah. ResultSet.getTimestampdan PreparedStatement.setTimestampkeduanya mengatakan di dokumen mereka bahwa mereka mengubah tanggal ke / dari zona waktu JVM saat ini secara default saat membaca dan menulis dari / ke database.

Saya menemukan solusi untuk ini di Hibernate 3.5 dengan subclassing org.hibernate.type.TimestampTypeyang memaksa metode JDBC ini untuk menggunakan UTC, bukan zona waktu lokal:

public class UtcTimestampType extends TimestampType {

    private static final long serialVersionUID = 8088663383676984635L;

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    @Override
    public Object get(ResultSet rs, String name) throws SQLException {
        return rs.getTimestamp(name, Calendar.getInstance(UTC));
    }

    @Override
    public void set(PreparedStatement st, Object value, int index) throws SQLException {
        Timestamp ts;
        if(value instanceof Timestamp) {
            ts = (Timestamp) value;
        } else {
            ts = new Timestamp(((java.util.Date) value).getTime());
        }
        st.setTimestamp(index, ts, Calendar.getInstance(UTC));
    }
}

Hal yang sama harus dilakukan untuk memperbaiki TimeType dan DateType jika Anda menggunakan tipe tersebut. Kelemahannya adalah Anda harus menentukan secara manual bahwa tipe ini akan digunakan sebagai ganti default pada setiap bidang Tanggal di POJO Anda (dan juga merusak kompatibilitas JPA murni), kecuali seseorang mengetahui metode penggantian yang lebih umum.

UPDATE: Hibernate 3.6 telah mengubah jenis API. Di 3.6, saya menulis kelas UtcTimestampTypeDescriptor untuk mengimplementasikan ini.

public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor {
    public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor();

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>( javaTypeDescriptor, this ) {
            @Override
            protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
                st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) );
            }
        };
    }

    public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicExtractor<X>( javaTypeDescriptor, this ) {
            @Override
            protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
                return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options );
            }
        };
    }
}

Sekarang, saat aplikasi dimulai, jika Anda menyetel TimestampTypeDescriptor.INSTANCE ke instance UtcTimestampTypeDescriptor, semua stempel waktu akan disimpan dan diperlakukan sebagai dalam UTC tanpa harus mengubah anotasi pada POJO. [Saya belum menguji ini]

divestoclimb
sumber
3
Bagaimana Anda memberi tahu Hibernate untuk menggunakan kebiasaan Anda UtcTimestampType?
Derek Mahar
divestoclimb, dengan versi Hibernate mana yang UtcTimestampTypekompatibel?
Derek Mahar
2
"ResultSet.getTimestamp dan PreparedStatement.setTimestamp mengatakan dalam dokumen mereka bahwa mereka mengubah tanggal ke / dari zona waktu JVM saat ini secara default saat membaca dan menulis dari / ke database." Apakah Anda punya referensi? Saya tidak melihat penyebutan ini di Java 6 Javadocs untuk metode ini. Menurut stackoverflow.com/questions/4123534/… , bagaimana metode ini menerapkan zona waktu ke driver JDBC tertentu Dateatau Timestampbergantung.
Derek Mahar
1
Saya berani bersumpah bahwa saya telah membacanya tentang penggunaan zona waktu JVM tahun lalu, tetapi sekarang saya tidak dapat menemukannya. Saya mungkin menemukannya di dokumen untuk driver JDBC tertentu dan digeneralisasikan.
divestoclimb
2
Untuk mendapatkan versi 3.6 dari contoh Anda untuk bekerja, saya harus membuat tipe baru yang pada dasarnya merupakan pembungkus di sekitar TimeStampType lalu setel jenis itu di lapangan.
Shaun Stone
17

Dengan Spring Boot JPA, gunakan kode di bawah ini di file application.properties Anda dan jelas Anda dapat mengubah zona waktu sesuai pilihan Anda

spring.jpa.properties.hibernate.jdbc.time_zone = UTC

Kemudian di file kelas Entitas Anda,

@Column
private LocalDateTime created;
JavaGeek
sumber
11

Menambahkan jawaban yang sepenuhnya berdasarkan dan berhutang budi kepada divestoclimb dengan petunjuk dari Shaun Stone. Hanya ingin menjelaskannya secara detail karena ini masalah umum dan solusinya agak membingungkan.

Ini menggunakan Hibernate 4.1.4. Final, meskipun saya mencurigai apa pun setelah 3.6 akan berfungsi.

Pertama, buat UtcTimestampTypeDescriptor divestoclimb

public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor {
    public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor();

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>( javaTypeDescriptor, this ) {
            @Override
            protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
                st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) );
            }
        };
    }

    public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicExtractor<X>( javaTypeDescriptor, this ) {
            @Override
            protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
                return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options );
            }
        };
    }
}

Kemudian buat UtcTimestampType, yang menggunakan UtcTimestampTypeDescriptor alih-alih TimestampTypeDescriptor sebagai SqlTypeDescriptor dalam panggilan konstruktor super tetapi sebaliknya mendelegasikan semuanya ke TimestampType:

public class UtcTimestampType
        extends AbstractSingleColumnStandardBasicType<Date>
        implements VersionType<Date>, LiteralType<Date> {
    public static final UtcTimestampType INSTANCE = new UtcTimestampType();

    public UtcTimestampType() {
        super( UtcTimestampTypeDescriptor.INSTANCE, JdbcTimestampTypeDescriptor.INSTANCE );
    }

    public String getName() {
        return TimestampType.INSTANCE.getName();
    }

    @Override
    public String[] getRegistrationKeys() {
        return TimestampType.INSTANCE.getRegistrationKeys();
    }

    public Date next(Date current, SessionImplementor session) {
        return TimestampType.INSTANCE.next(current, session);
    }

    public Date seed(SessionImplementor session) {
        return TimestampType.INSTANCE.seed(session);
    }

    public Comparator<Date> getComparator() {
        return TimestampType.INSTANCE.getComparator();        
    }

    public String objectToSQLString(Date value, Dialect dialect) throws Exception {
        return TimestampType.INSTANCE.objectToSQLString(value, dialect);
    }

    public Date fromStringValue(String xml) throws HibernateException {
        return TimestampType.INSTANCE.fromStringValue(xml);
    }
}

Terakhir, ketika Anda menginisialisasi konfigurasi Hibernate Anda, daftarkan UtcTimestampType sebagai penimpaan tipe:

configuration.registerTypeOverride(new UtcTimestampType());

Sekarang stempel waktu tidak harus peduli dengan zona waktu JVM dalam perjalanannya ke dan dari database. HTH.

Shane
sumber
6
Akan sangat bagus untuk melihat solusi untuk JPA dan Konfigurasi Musim Semi.
Terong
2
Catatan tentang penggunaan pendekatan ini dengan kueri asli dalam Hibernate. Untuk menggunakan tipe yang diganti ini, Anda harus menyetel nilainya dengan query.setParameter (int pos, Object value), bukan query.setParameter (int pos, Date value, TemporalType temporalType). Jika Anda menggunakan yang terakhir maka Hibernate akan menggunakan implementasi tipe aslinya, karena mereka memiliki kode keras.
Nigel
Di mana saya harus memanggil pernyataan configuration.registerTypeOverride (new UtcTimestampType ()); ?
Stony
@Stony Dimanapun Anda menginisialisasi konfigurasi Hibernate Anda. Jika Anda memiliki HibernateUtil (kebanyakan), itu akan ada di sana.
Shane
1
Ini berfungsi, tetapi setelah memeriksanya saya menyadari bahwa itu tidak diperlukan untuk server postgres yang bekerja di zona waktu = "UTC" dan dengan semua jenis stempel waktu default sebagai "stempel waktu dengan zona waktu" (itu dilakukan secara otomatis kemudian). Tapi, berikut adalah versi tetap untuk Hibernate 4.3.5 GA sebagai kelas tunggal lengkap dan override spring factory bean pastebin.com/tT4ACXn6
Lukasz Frankowski
10

Anda akan mengira masalah umum ini akan diatasi dengan Hibernate. Tapi tidak! Ada beberapa "cara" untuk melakukannya dengan benar.

Yang saya gunakan adalah untuk menyimpan Date as a Long di database. Jadi saya selalu bekerja dengan milidetik setelah 1/1/70. Saya kemudian memiliki getter dan setter di Kelas saya yang mengembalikan / hanya menerima Tanggal. Jadi API tetap sama. Sisi negatifnya adalah saya memiliki kerinduan di database. JADI dengan SQL saya hanya bisa melakukan <,>, = perbandingan - bukan operator tanggal yang mewah.

Pendekatan lainnya adalah menggunakan jenis pemetaan ubahsuaian seperti yang dijelaskan di sini: http://www.hibernate.org/100.html

Saya pikir cara yang benar untuk menangani ini adalah dengan menggunakan Kalender daripada Tanggal sekalipun. Dengan Kalender Anda dapat mengatur TimeZone sebelum bertahan.

CATATAN: Stackoverflow konyol tidak mengizinkan saya berkomentar, jadi berikut adalah tanggapan untuk david a.

Jika Anda membuat objek ini di Chicago:

new Date(0);

Hibernate mempertahankannya sebagai "12/31/1969 18:00:00". Tanggal harus tanpa zona waktu, jadi saya tidak yakin mengapa penyesuaian akan dilakukan.

kode jari
sumber
1
Memalukan untukku! Anda benar dan tautan dari kiriman Anda menjelaskannya dengan baik. Sekarang saya rasa jawaban saya layak mendapat reputasi negatif :)
david a.
1
Tidak semuanya. Anda mendorong saya untuk memposting contoh yang sangat eksplisit tentang mengapa ini menjadi masalah.
jari kode
4
Saya bisa mempertahankan waktu dengan benar menggunakan objek Kalender sehingga mereka disimpan di DB sebagai UTC seperti yang Anda sarankan. Namun, ketika membaca kembali entitas bertahan dari database, Hibernate mengasumsikan mereka berada di zona waktu lokal dan objek kalender salah!
John K
1
John K, untuk mengatasi Calendarmasalah baca ini , saya pikir Hibernate atau JPA harus menyediakan beberapa cara untuk menentukan, untuk setiap pemetaan, zona waktu yang harus diterjemahkan Hibernate tanggal membaca dan menulis ke TIMESTAMPkolom.
Derek Mahar
joekutner, setelah membaca stackoverflow.com/questions/4123534/… , saya datang untuk membagikan pendapat Anda bahwa kita harus menyimpan milidetik sejak Epoch dalam database daripada Timestampkarena kita tidak dapat begitu saja mempercayai driver JDBC untuk menyimpan tanggal sebagai kami harapkan.
Derek Mahar
8

Ada beberapa zona waktu yang beroperasi di sini:

  1. Kelas Tanggal Java (util dan sql), yang memiliki zona waktu implisit UTC
  2. Zona waktu tempat JVM Anda berjalan, dan
  3. zona waktu default dari server database Anda.

Semua ini bisa berbeda. Hibernate / JPA memiliki kekurangan desain yang parah sehingga pengguna tidak dapat dengan mudah memastikan bahwa informasi zona waktu disimpan di server database (yang memungkinkan rekonstruksi waktu dan tanggal yang benar di JVM).

Tanpa kemampuan untuk (dengan mudah) menyimpan zona waktu menggunakan JPA / Hibernate maka informasi akan hilang dan begitu informasi hilang, membuatnya menjadi mahal (jika memungkinkan).

Saya berpendapat bahwa lebih baik untuk selalu menyimpan informasi zona waktu (harus default) dan pengguna kemudian harus memiliki kemampuan opsional untuk mengoptimalkan zona waktu (meskipun itu hanya benar-benar mempengaruhi tampilan, masih ada zona waktu implisit di tanggal mana pun).

Maaf, posting ini tidak memberikan solusi (yang telah dijawab di tempat lain) tetapi ini adalah rasionalisasi mengapa selalu menyimpan informasi zona waktu di sekitar itu penting. Sayangnya tampaknya banyak Ilmuwan Komputer dan praktisi pemrograman menentang perlunya zona waktu hanya karena mereka tidak menghargai perspektif "kehilangan informasi" dan bagaimana hal itu membuat hal-hal seperti internasionalisasi menjadi sangat sulit - yang sangat penting saat ini dengan situs web yang dapat diakses oleh klien dan orang di organisasi Anda saat mereka bergerak di seluruh dunia.

Moa
sumber
1
"Hibernate / JPA memiliki kekurangan desain yang parah" Saya akan mengatakan itu adalah kekurangan dalam SQL, yang secara tradisional memungkinkan zona waktu menjadi implisit, dan oleh karena itu berpotensi apa saja. SQL konyol.
Raedwald
3
Sebenarnya, alih-alih selalu menyimpan zona waktu, Anda juga dapat menstandarkan pada satu zona waktu (biasanya UTC), dan mengonversi semuanya ke zona waktu ini saat bertahan (dan kembali saat membaca). Ini yang biasanya kami lakukan. Namun, JDBC tidak mendukungnya secara langsung baik: - /.
sleske
3

Silakan lihat proyek saya di Sourceforge yang memiliki tipe pengguna untuk tipe Tanggal dan Waktu SQL standar serta JSR 310 dan Joda Time. Semua tipe mencoba untuk mengatasi masalah pengimbangan. Lihat http://sourceforge.net/projects/usertype/

EDIT: Menanggapi pertanyaan Derek Mahar yang dilampirkan pada komentar ini:

"Chris, apakah tipe pengguna Anda berfungsi dengan Hibernate 3 atau lebih tinggi? - Derek Mahar 7 November '10 pukul 12:30"

Ya, tipe ini mendukung versi Hibernate 3.x termasuk Hibernate 3.6.

Chris Pheby
sumber
2

Tanggal tidak berada dalam zona waktu mana pun (ini adalah kantor milidetik dari momen yang ditentukan dalam waktu yang sama untuk semua orang), tetapi DB yang mendasari umumnya menyimpan stempel waktu dalam format politik (tahun, bulan, hari, jam, menit, detik,. ..) yang peka terhadap zona waktu.

Untuk lebih serius, Hibernate HARUS diizinkan untuk diberi tahu dalam beberapa bentuk pemetaan bahwa tanggal DB berada dalam zona waktu ini-dan-itu sehingga ketika memuat atau menyimpannya, ia tidak menganggapnya sendiri ...


sumber
1

Saya mengalami masalah yang sama ketika saya ingin menyimpan tanggal di DB sebagai UTC dan menghindari penggunaan varchardan String <-> java.util.Datekonversi eksplisit , atau mengatur seluruh aplikasi Java saya dalam zona waktu UTC (karena ini dapat menyebabkan masalah tak terduga lainnya, jika JVM adalah dibagikan di banyak aplikasi).

Jadi, ada proyek sumber terbuka DbAssist, yang memungkinkan Anda untuk dengan mudah memperbaiki tanggal baca / tulis sebagai UTC dari database. Karena Anda menggunakan Anotasi JPA untuk memetakan bidang di entitas, yang harus Anda lakukan adalah menyertakan ketergantungan berikut ke pomfile Maven Anda :

<dependency>
    <groupId>com.montrosesoftware</groupId>
    <artifactId>DbAssist-5.2.2</artifactId>
    <version>1.0-RELEASE</version>
</dependency>

Kemudian Anda menerapkan perbaikan (untuk contoh Hibernate + Spring Boot) dengan menambahkan @EnableAutoConfigurationanotasi sebelum kelas aplikasi Spring. Untuk instruksi instalasi setup lainnya dan contoh penggunaan lainnya, cukup merujuk ke github proyek .

Hal baiknya adalah Anda tidak perlu memodifikasi entitas sama sekali; Anda bisa membiarkan java.util.Dateladang mereka apa adanya.

5.2.2harus sesuai dengan versi Hibernate yang Anda gunakan. Saya tidak yakin, versi mana yang Anda gunakan dalam proyek Anda, tetapi daftar lengkap perbaikan yang disediakan tersedia di halaman wiki github proyek . Alasan mengapa perbaikan berbeda untuk berbagai versi Hibernate adalah karena pembuat Hibernate mengubah API beberapa kali antara rilis.

Secara internal, perbaikan menggunakan petunjuk dari divestoclimb, Shane dan beberapa sumber lain untuk membuat kebiasaan UtcDateType. Kemudian memetakan standar java.util.Datedengan kebiasaan UtcDateTypeyang menangani semua penanganan zona waktu yang diperlukan. Pemetaan jenis dilakukan dengan menggunakan @Typedefanotasi di package-info.javafile yang disediakan .

@TypeDef(name = "UtcDateType", defaultForType = Date.class, typeClass = UtcDateType.class),
package com.montrosesoftware.dbassist.types;

Anda dapat menemukan artikel di sini yang menjelaskan mengapa pergeseran waktu seperti itu terjadi dan apa saja pendekatan untuk menyelesaikannya.

orim
sumber
1

Hibernasi tidak memungkinkan untuk menentukan zona waktu dengan penjelasan atau cara lain. Jika Anda menggunakan Kalender dan bukan tanggal, Anda bisa menerapkan solusi menggunakan properti HIbernate AccessType dan menerapkan pemetaan sendiri. Solusi yang lebih canggih adalah menerapkan UserType kustom untuk memetakan Tanggal atau Kalender Anda. Kedua solusi dijelaskan dalam postingan blog saya di sini: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

joobik
sumber