JPA ve Hazırda Bekletme ile UTC saat diliminde tarih / saat ve zaman damgaları nasıl saklanır


106

JPA / Hibernate'i bir tarih / saati veritabanında UTC (GMT) saat dilimi olarak saklayacak şekilde nasıl yapılandırabilirim? Şu ek açıklamalı JPA varlığını düşünün:

public class Event {
    @Id
    public int id;

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

Tarih 2008-Şubat-03 09:30 Pasifik Standart Saati (PST) ise, o zaman 2008-Şubat-03 17:30 UTC saatinin veritabanında depolanmasını istiyorum. Aynı şekilde, tarih veritabanından alındığında, UTC olarak yorumlanmasını istiyorum. Yani bu durumda 530pm, 530pm UTC'dir. Görüntülendiğinde 9:30 PST olarak biçimlendirilecektir.


1
Vlad Mihalcea cevabı (5.2+ Hazırda) güncelleştirilmiş cevap verir
Dinei

Yanıtlar:


73

Hazırda Bekletme 5.2 ile, artık aşağıdaki yapılandırma özelliğini kullanarak UTC saat dilimini zorlayabilirsiniz:

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

Daha fazla ayrıntı için bu makaleye göz atın .


19
Ben de bunu yazdım: D Şimdi, tahmin edin, Hibernate'de bu özellik için desteği kim ekledi?
Vlad Mihalcea

Oh, şimdi anladım ki profiliniz ve o makalelerdeki isim ve resim aynı ... İyi iş Vlad :)
Dinei

@VladMihalcea Mysql içinse, MySql'e useTimezone=truebağlantı dizesini kullanarak saat dilimini kullanmasını söylemek gerekir . O zaman sadece ayar özelliği hibernate.jdbc.time_zoneçalışacaktır
TheCoder

Aslında, useLegacyDatetimeCodeyanlış ayarlamanız gerekiyor
Vlad Mihalcea

2
hibernate.jdbc.time_zone göz ardı edilmiş gibi görünüyor veya PostgreSQL ile kullanıldığında hiçbir etkisi yok
Alex R

48

Bildiğim kadarıyla, tüm Java uygulamanızı UTC saat dilimine koymanız gerekir (böylece Hibernate, tarihleri ​​UTC olarak kaydeder) ve bir şeyleri görüntülerken istediğiniz zaman dilimine çevirmeniz gerekir (en azından biz yaparız) bu yoldan).

Başlangıçta şunları yapıyoruz:

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

Ve istenen saat dilimini DateFormat olarak ayarlayın:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))

5
mitchnull, çözümünüz her durumda çalışmayacaktır çünkü Hazırda Beklet, tarihleri ​​JDBC sürücüsüne atar ve her JDBC sürücüsü tarih ve saat dilimlerini farklı şekilde işler. stackoverflow.com/questions/4123534/… bakın .
Derek Mahar

2
Ancak uygulamamı JVM "-Duser.timezone = + 00: 00" özelliğine bildirmeye başlarsam, aynı davranış olmaz mı?
rafa.ferreira

8
Anladığım kadarıyla, JVM ve veritabanı sunucusunun farklı saat dilimlerinde olduğu durumlar dışında her durumda çalışacaktır.
Shane

stevekuo ve @mitchnull, aşağıda çok daha iyi ve yan etkilere karşı dayanıklı olan divestoclimb çözümüne bakın stackoverflow.com/a/3430957/233906
Cerber

HibernateJPA desteği "@Factory" ve "@Externalizer" ek açıklamasını yapın, OpenJPA kitaplığında datetime utc işlemesini bu şekilde yaparım. stackoverflow.com/questions/10819862/…
Kim

44

Hazırda bekletme, Tarihlerdeki saat dilimi konularından habersizdir (çünkü hiç yoktur), ancak aslında sorunlara neden olan JDBC katmanıdır. ResultSet.getTimestampve PreparedStatement.setTimestampher ikisi de belgelerinde, veritabanından / veritabanına okurken ve veritabanına yazarken varsayılan olarak tarihleri ​​geçerli JVM saat dilimine / diliminden dönüştürdüklerini söylüyor.

Hibernate 3.5'te org.hibernate.type.TimestampType, bu JDBC yöntemlerini yerel saat dilimi yerine UTC'yi kullanmaya zorlayan alt sınıflandırma yoluyla buna bir çözüm buldum :

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

Bu türleri kullanıyorsanız, TimeType ve DateType'ı düzeltmek için aynı şey yapılmalıdır. Bunun dezavantajı, birisi daha genel bir geçersiz kılma yöntemini bilmediği sürece, POJO'larınızdaki her Tarih alanındaki varsayılanlar yerine bu türlerin kullanılacağını (ve ayrıca saf JPA uyumluluğunu bozduğunu) manuel olarak belirtmeniz gerekmesidir.

GÜNCELLEME: Hibernate 3.6, API türlerini değiştirdi. 3.6'da, bunu uygulamak için bir UtcTimestampTypeDescriptor sınıfı yazdım.

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

Artık uygulama başladığında, TimestampTypeDescriptor.INSTANCE öğesini UtcTimestampTypeDescriptor örneğine ayarlarsanız, tüm zaman damgaları POJO'lardaki ek açıklamaları değiştirmek zorunda kalmadan UTC olarak saklanacak ve işlenecektir. [Bunu henüz test etmedim]


3
Hibernate'e özelinizi kullanmasını nasıl söylersiniz UtcTimestampType?
Derek Mahar

divestoclimb, Hibernate'in hangi sürümüyle UtcTimestampTypeuyumludur?
Derek Mahar

2
"ResultSet.getTimestamp ve PreparedStatement.setTimestamp, veritabanlarında okurken ve veritabanına yazarken varsayılan olarak tarihleri ​​geçerli JVM saat dilimine / saat diliminden dönüştürdüklerini söylüyor." Referansın var mı? Bu yöntemler için Java 6 Javadocs'ta bundan bahsetmiyorum. Stackoverflow.com/questions/4123534/… 'e göre , bu yöntemlerin belirli bir saat dilimini nasıl uyguladıkları Dateveya TimestampJDBC sürücüsüne bağlıdır.
Derek Mahar

1
Geçen yıl JVM saat dilimini kullanmakla ilgili olarak okuduğuma yemin edebilirdim, ancak şimdi bulamıyorum. Belirli bir JDBC sürücüsüne ait belgelerde bulmuş ve genelleştirilmiş olabilirim.
divestoclimb

2
Örneğinizin 3.6 sürümünü çalıştırmak için, temelde TimeStampType etrafında bir sarmalayıcı olan yeni bir tür oluşturmam ve ardından bu türü alanda ayarlamam gerekiyordu.
Shaun Taş

17

Spring Boot JPA ile, application.properties dosyanızda aşağıdaki kodu kullanın ve tabii ki saat dilimini tercihinize göre değiştirebilirsiniz.

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

Ardından Varlık sınıfı dosyanızda,

@Column
private LocalDateTime created;

11

Shaun Stone'dan bir ipucu ile tamamen dalışa dayalı ve buna minnettar olan bir cevap eklemek . Yaygın bir sorun olduğu ve çözümü biraz kafa karıştırıcı olduğu için ayrıntılı olarak açıklamak istedim.

Bu, Hazırda Bekletme 4.1.4'ü kullanıyor. Son, 3.6'dan sonra herhangi bir şeyin çalışacağından şüpheleniyorum.

Önce, divestoclimb'in UtcTimestampTypeDescriptor'unu oluşturun

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

Ardından, süper yapıcı çağrısında SqlTypeDescriptor olarak TimestampTypeDescriptor yerine UtcTimestampTypeDescriptor kullanan, ancak aksi takdirde her şeyi TimestampType'a devreten UtcTimestampType oluşturun:

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

Son olarak, Hazırda Bekletme yapılandırmanızı başlattığınızda, UtcTimestampType'ı bir tür geçersiz kılma olarak kaydedin:

configuration.registerTypeOverride(new UtcTimestampType());

Artık zaman damgaları, veritabanına gidip gelirken JVM'nin saat dilimiyle ilgilenmemelidir. HTH.


6
JPA ve Yay Yapılandırması için çözümü görmek harika olurdu.
Patlıcan

2
Bu yaklaşımı Hibernate'de yerel sorgularla kullanmaya ilişkin bir not. Bu geçersiz kılınan türlerin kullanılması için, değeri query.setParameter (int pos, Object değeri) ile ayarlamanız gerekir, query.setParameter (int pos, Date value, TemporalType temporalType) ile değil. İkincisini kullanırsanız, Hibernate, sabit kodlu oldukları için orijinal tür uygulamalarını kullanacaktır.
Nigel

Configuration.registerTypeOverride (new UtcTimestampType ()) ifadesini nerede çağırmalıyım; ?
Stony

@Stony Hazırda bekletme yapılandırmanızı nerede başlatırsanız başlatın. HibernateUtil'iniz varsa (çoğu var), orada olacaktır.
Shane

1
Çalışıyor, ancak kontrol ettikten sonra, timezone = "UTC" de çalışan postgres sunucusu için gerekli olmadığını ve tüm varsayılan zaman damgası türlerinin "saat dilimiyle birlikte zaman damgası" olarak kullanılmasının gerekli olmadığını fark ettim (o zaman otomatik olarak yapılır). Ancak, Hibernate 4.3.5 GA için tam tek sınıf ve geçersiz kılınan yay fabrikası fasulyesi pastebin.com/tT4ACXn6
Lukasz Frankowski

10

Bu yaygın sorunun Hibernate tarafından çözüleceğini düşünürsünüz. Ama değil! Doğru yapmak için birkaç "hack" var.

Kullandığım tek, tarihi veritabanında Long olarak saklamaktır. Bu yüzden 1/1 / 70'den sonra her zaman milisaniyelerle çalışıyorum. Daha sonra Sınıfımda yalnızca Tarihleri ​​döndüren / kabul eden alıcılar ve ayarlayıcılar var. Dolayısıyla API aynı kalır. Kötü tarafı, veritabanında uzun sürem olması. SO SQL ile hemen hemen sadece <,>, = karşılaştırmalar yapabilirim - fantezi tarih operatörlerini değil.

Başka bir yaklaşım, burada açıklandığı gibi özel bir eşleme türü kullanmaktır: http://www.hibernate.org/100.html

Bununla başa çıkmanın doğru yolu, bir Tarih yerine Takvim kullanmaktır. Takvim ile, devam etmeden önce Zaman Dilimini ayarlayabilirsiniz.

NOT: Aptalca yığın akışı yorum yapmama izin vermiyor, bu yüzden işte david a.

Bu nesneyi Chicago'da oluşturursanız:

new Date(0);

Hazırda bekletme, "12/31/1969 18:00:00" olarak devam eder. Tarihlerde saat dilimi olmamalıdır, bu nedenle ayarlamanın neden yapılacağından emin değilim.


1
Yazıklar olsun bana! Haklıydın ve gönderindeki bağlantı bunu iyi açıklıyor. Şimdi sanırım cevabım bazı olumsuz şöhreti hak ediyor :)
david a.

1
Bir şey değil. Beni bunun neden bir sorun olduğuna dair çok açık bir örnek yayınlamaya teşvik ettiniz.
codefinger

4
Takvim nesnesini kullanarak zamanları doğru bir şekilde ısrar edebildim, böylece önerdiğiniz gibi bunlar DB'de UTC olarak saklanacak. Ancak, kalıcı varlıkları veritabanından geri okurken, Hazırda Beklet bunların yerel saat diliminde olduğunu ve Takvim nesnesinin yanlış olduğunu varsayar!
John K

1
John K., bu Calendarokuma sorununu çözmek için , Hibernate veya JPA'nın, her eşleme için, Hibernate'in okuduğu ve bir TIMESTAMPsütuna yazdığı tarihi çevirmesi gereken saat dilimini belirtmek için bir yol sağlaması gerektiğini düşünüyorum .
Derek Mahar

joekutner, stackoverflow.com/questions/4123534/… 'i okuduktan sonra Timestamp, JDBC sürücüsüne tarihleri ​​şu şekilde saklamak için mutlaka güvenemeyeceğimiz için Epoch'tan bu yana milisaniyeleri veritabanında saklamamız gerektiğine dair fikrinizi paylaşmaya geldim. beklerdik.
Derek Mahar

8

Burada işlemde olan birkaç saat dilimi vardır:

  1. UTC'nin örtük saat dilimlerine sahip Java'nın Date sınıfları (util ve sql)
  2. JVM'nizin çalıştığı saat dilimi ve
  3. veritabanı sunucunuzun varsayılan saat dilimi.

Bunların hepsi farklı olabilir. Hazırda Bekletme / JPA, bir kullanıcının saat dilimi bilgilerinin veritabanı sunucusunda korunmasını kolayca sağlayamadığından ciddi bir tasarım eksikliğine sahiptir (bu, JVM'de doğru zaman ve tarihlerin yeniden yapılandırılmasına olanak tanır).

Zaman dilimini JPA / Hibernate kullanarak (kolayca) saklama yeteneği olmadan, bilgi kaybolur ve bilgi bir kez kaybedildiğinde onu oluşturmak pahalı hale gelir (eğer mümkünse).

Saat dilimi bilgilerini her zaman saklamanın daha iyi olduğunu (varsayılan olmalıdır) ve kullanıcıların, saat dilimini uzakta optimize etme konusunda isteğe bağlı bir yeteneğe sahip olması gerektiğini savunabilirim (yalnızca görüntülemeyi gerçekten etkilese de, herhangi bir tarihte hala örtük bir saat dilimi vardır).

Üzgünüz, bu gönderi bir çözüm sağlamaz (bu başka bir yerde yanıtlanmıştır), ancak zaman dilimi bilgilerinin her zaman etrafta saklanmasının neden önemli olduğunun bir mantığıdır. Ne yazık ki pek çok Bilgisayar Bilimcisi ve programlama pratisyeni, saat dilimlerine duyulan ihtiyaca karşı çıkıyorlar çünkü "bilgi kaybı" perspektifini ve bunun uluslararasılaşma gibi şeyleri nasıl çok zorlaştırdığını takdir etmiyorlar - ki bu günümüzde web siteleri için çok önemli. kuruluşunuzdaki müşteriler ve insanlar dünya çapında hareket ederken.


1
"Hazırda bekletme / JPA ciddi bir tasarım eksikliğine sahip" Bunun, geleneksel olarak saat diliminin örtük olmasına ve dolayısıyla potansiyel olarak herhangi bir şeye izin veren SQL'deki bir eksiklik olduğunu söyleyebilirim. Aptal SQL.
Raedwald

3
Aslında, her zaman zaman dilimini saklamak yerine, bir saat dilimini (genellikle UTC) standartlaştırabilir ve devam ederken (ve okurken geri dönerken) her şeyi bu saat dilimine dönüştürebilirsiniz. Bu genellikle yaptığımız şeydir. Ancak, JDBC bunu doğrudan da desteklemez: - /.
sleske

3

Lütfen, standart SQL Tarih ve Saat türleri ile JSR 310 ve Joda Saati için kullanıcı türlerine sahip Sourceforge'daki projeme bir göz atın. Tüm türler, dengeleme sorununu gidermeye çalışır. Bkz. Http://sourceforge.net/projects/usertype/

DÜZENLEME: Derek Mahar'ın bu yoruma ekli sorusuna yanıt olarak:

"Chris, kullanıcı türleriniz Hibernate 3 veya üzeri ile çalışıyor mu? - Derek Mahar 7 Kasım 2010, 12: 30'da"

Evet, bu türler Hibernate 3.6 dahil Hibernate 3.x sürümlerini destekler.


2

Tarih herhangi bir saat diliminde değildir (herkes için aynı zamanda belirli bir andan itibaren bir milisaniye ofisidir), ancak temeldeki (R) DB'ler genellikle zaman damgalarını siyasi biçimde (yıl, ay, gün, saat, dakika, saniye,. ..) bu, saat dilimine duyarlıdır.

Ciddi olmak gerekirse , Hazırda Bekletme'ye, bir tür eşleme içinde DB tarihinin şu ve bu zaman diliminde olduğunun söylenmesine izin verilmesi GEREKİR , böylece yüklediğinde veya sakladığında kendine ait olduğunu varsaymasın ...


1

Tarihleri ​​DB'de UTC olarak saklamak varcharve açık String <-> java.util.Datedönüşümleri kullanmaktan ve açık dönüşümlerden kaçınmak veya tüm Java uygulamamı UTC saat diliminde ayarlamaktan kaçınmak istediğimde aynı sorunla karşılaştım (çünkü bu, JVM ise başka bir beklenmedik sorunlara yol açabilir. birçok uygulama arasında paylaşılır).

Dolayısıyla, DbAssistokuma / yazma bilgisini veri tabanından UTC tarihi olarak kolayca düzeltmenize izin veren açık kaynaklı bir proje var . Varlıktaki alanları eşlemek için JPA Ek Açıklamaları kullandığınızdan, tek yapmanız gereken aşağıdaki bağımlılığı Maven pomdosyanıza dahil etmektir :

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

Sonra düzeltmeyi (Hibernate + Spring Boot örneği için) @EnableAutoConfigurationSpring uygulama sınıfından önce açıklama ekleyerek uygularsınız . Diğer kurulumlar için kurulum talimatları ve daha fazla kullanım örnekleri için, sadece projenin github'ına bakın .

İyi olan şey, varlıkları değiştirmenize gerek olmamasıdır; java.util.Datetarlalarını oldukları gibi bırakabilirsiniz .

5.2.2kullandığınız Hazırda Bekletme sürümüne karşılık gelmelidir. Projenizde hangi sürümü kullandığınızdan emin değilim, ancak sağlanan düzeltmelerin tam listesi projenin github'unun wiki sayfasında mevcuttur . Düzeltmenin çeşitli Hazırda Bekletme sürümleri için farklı olmasının nedeni, Hibernate içerik oluşturucularının API'yi sürümler arasında birkaç kez değiştirmesidir.

Dahili olarak düzeltme, bir özel oluşturmak için divestoclimb, Shane ve birkaç başka kaynaktan gelen ipuçlarını kullanır UtcDateType. Ardından , gerekli tüm zaman dilimi işlemlerini java.util.Dategerçekleştiren özel ile standardı eşler UtcDateType. Türlerin eşlenmesi @Typedef, sağlanan package-info.javadosyadaki açıklama kullanılarak elde edilir .

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

Burada neden böyle bir zaman kaymasının meydana geldiğini ve bunu çözmek için yaklaşımların neler olduğunu açıklayan bir makale bulabilirsiniz .


1

Hazırda Bekletme, saat dilimlerinin açıklama veya başka bir yöntemle belirlenmesine izin vermez. Tarih yerine Takvim kullanıyorsanız, AccessType HIbernate özelliğini kullanarak ve eşlemeyi kendiniz uygulayarak bir geçici çözüm uygulayabilirsiniz. Daha gelişmiş çözüm, Tarihinizi veya Takviminizi eşlemek için özel bir Kullanıcı Türü uygulamaktır. Her iki çözüm de buradaki blog yazımda açıklanmıştır: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.