Rails ve PostgreSQL'de zaman dilimlerini tamamen yok sayma


164

Rails ve Postgres tarihleri ​​ve saatleri ile ilgileniyorum ve bu sorunla karşılaşıyorum:

Veritabanı UTC'dir.

Kullanıcı, Rails uygulamasında bir zaman dilimi seçer, ancak yalnızca kullanıcıları zamanları karşılaştırmak için yerel zaman alırken kullanılır.

Kullanıcı 17 Mart 2012 saat 19: 00'da bir zaman depolar. Saat dilimi dönüşümlerinin veya saat diliminin depolanmasını istemiyorum. Sadece tarih ve saatin kaydedilmesini istiyorum. Bu şekilde kullanıcı saat dilimini değiştirirse, 17 Mart 2012 19: 00'da gösterilir.

Kullanıcıların belirtilen saat dilimini, kullanıcıların yerel saat dilimindeki geçerli saatten önce 'önce' veya 'sonra' almak için kullanıyorum.

Şu anda 'zaman dilimi olmadan zaman damgası' kullanıyorum ama kayıtları aldığımda, raylar (?) Onları istemediğim uygulamadaki saat dilimine dönüştürüyor.

Appointment.first.time
 => Fri, 02 Mar 2012 19:00:00 UTC +00:00 

Veritabanındaki kayıtlar UTC olarak göründüğünden, bilgisayar korsanım şimdiki zamanı almak, 'Date.strptime (str, "% m /% d /% Y")' ile saat dilimini kaldırmak ve sonra benim bununla sorgu:

.where("time >= ?", date_start)

Her yerde saat dilimlerini görmezden gelmenin daha kolay bir yolu olmalı gibi görünüyor. Herhangi bir fikir?

Yanıtlar:


347

Veri türü timestamp, kısa adıdır timestamp without time zone.
Diğer seçenek timestamptzkısadır timestamp with time zone.

timestamptzolan tercih tarih / saat ailede tip anlamıyla. Bu gelmiştir typispreferredayarlanan pg_typeilgili olabilen,:

Dahili depolama ve dönem

Dahili olarak zaman damgaları diskte ve RAM'de 8 bayt depolama alanı kaplar . Postgres dönemindeki mikrosaniye sayısını, 2000-01-01 00:00:00 UTC'yi temsil eden bir tamsayı değeridir.

Postgres ayrıca UNIX döneminden itibaren yaygın olarak kullanılan UNIX zaman sayımı saniye bilgisine sahiptir , 1970-01-01 00:00:00 UTC ve bunu işlevlerde to_timestamp(double precision)veya kullanır EXTRACT(EPOCH FROM timestamptz).

Kaynak kodu:

* Zaman damgalarının yanı sıra aralıkların h / m / s alanları,
* mikrosaniye birimleri ile int64 değerleri. (Bir zamanlar  
* saniye birimleriyle çift değerler.)

Ve:

/ * Unix ve Postgres hesaplamasında 0. Günün Julian-tarih eşdeğerleri * /  
#define UNIX_EPOCH_JDATE 2440588 / * == tarih2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == tarih2j (2000, 1, 1) * /  

Mikrosaniye çözünürlüğü saniyeler için maksimum 6 kesirli basamağa çevirir.

timestamp

Olarak yazılan bir değer Postgres'e hiçbir zaman diliminin açıkça belirtilmediğini bildirir. Geçerli saat dilimi varsayılır. Postgres yanlışlıkla eklenen herhangi bir saat dilimi değiştiricisini yok sayar !timestamp [without time zone]

Gösterge için saat kaydırılmıyor. Aynı saat dilimi ayarıyla her şey yolunda. Farklı bir saat dilimi ayarı için anlam değişir, ancak değer ve ekran aynı kalır.

timestamptz

Kullanımı timestamp with time zoneoldukça farklıdır. Ben burada kılavuzu alıntı :

Çünkü timestamp with time zonedahili olarak depolanan değer her zaman UTC'dir (Evrensel Koordineli Zaman ...)

Cesur vurgu benim. Saat dilimi kendisi saklanan asla . Kaydedilen UTC zaman damgasını hesaplamak için kullanılan bir girdi değiştiricidir veya görüntülenen yerel saati hesaplamak için kullanılan çıkış değiştiricisidir. timestamptzGiriş için bir ofset eklemezseniz , oturumun geçerli saat dilimi ayarı varsayılır. Tüm hesaplamalar UTC zaman damgası değerleri ile yapılır. Birden fazla saat dilimi ile uğraşmanız gerekiyorsa (veya zorunda kalmanız gerekiyorsa) kullanın timestamptz.

Psql veya pgAdmin gibi istemciler veya libpq aracılığıyla iletişim kuran herhangi bir uygulama (pg gem ile Ruby gibi) geçerli saat dilimi için zaman damgası artı ofseti veya istenen bir saat dilimine göre sunulur (aşağıya bakın). Her zaman aynı noktadır , sadece görüntü formatı değişir. Veya kılavuzun belirttiği gibi :

Tüm saat dilimine uyan tarihler ve saatler dahili olarak UTC'de saklanır. İstemciye gösterilmeden önce TimeZone yapılandırma parametresi tarafından belirtilen bölgedeki yerel saate dönüştürülür .

Bu basit örneği düşünün (psql'de):

db = # SELECT zaman damgası '2012-03-05 20:00 +03 ';
      timestamptz
------------------------
 2012-03-05 18:00:00 +01

Cesur vurgu benim. Burada ne oldu? Girdi değişmezi için
rasgele bir saat dilimi sapması seçtim +3. Postgres için bu UTC zaman damgasını girmenin birçok yolundan sadece biridir 2012-03-05 17:00:00. Sorgunun sonucu, kış ve yaz aylarında ofseti olan testimde geçerli saat dilimi ayarı Viyana / Avusturya için görüntülenir : çünkü kışa düşer.+1+22012-03-05 18:00:00+01

Postgres bu değerin nasıl girildiğini zaten unutmuş. Tek hatırladığı değer ve veri türüdür. Ondalık bir sayı gibi. numeric '003.4', numeric '3.40'Ya numeric '+3.4'- aynı iç değerindeki tüm sonucu.

AT TIME ZONE

Bu mantığı kavradığınızda, istediğiniz her şeyi yapabilirsiniz. Şimdi eksik olan tek şey, zaman damgası değişmez değerlerini belirli bir saat dilimine göre yorumlamak veya temsil etmek için bir araçtır. AT TIME ZONEYapı burada devreye giriyor. İki farklı kullanım durumu var. timestamptzdönüştürülür timestampve tam tersi.

UTC'yi girmek için timestamptz 2012-03-05 17:00:00+0:

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

... şuna eşittir:

SELECT timestamptz '2012-03-05 17:00:00 UTC'

EST timestamp(Doğu Standart Saati) ile aynı noktayı görüntülemek için :

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

Bu doğru, AT TIME ZONE 'UTC' iki kez . Birincisi, timestampdeğeri türü döndüren (verilen) UTC zaman damgası olarak yorumlar timestamptz. İkincisi ise dönüştürür timestamptziçin timestampverilen zaman diliminde 'EST' in - Ne zamandır bu eşsiz noktasında saat dilimi EST ekranlarda bir saat.

Örnekler

SELECT ts AT TIME ZONE 'UTC'
FROM  (
   VALUES
      (1, timestamptz '2012-03-05 17:00:00+0')
    , (2, timestamptz '2012-03-05 18:00:00+1')
    , (3, timestamptz '2012-03-05 17:00:00 UTC')
    , (4, timestamp   '2012-03-05 11:00:00'  AT TIME ZONE '+6') 
    , (5, timestamp   '2012-03-05 17:00:00'  AT TIME ZONE 'UTC') 
    , (6, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'US/Hawaii')  -- 
    , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii')                  -- 
    , (8, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'HST')        -- 
    , (9, timestamp   '2012-03-05 18:00:00+1')  --  loaded footgun!
      ) t(id, ts);

Aynı UTC zaman damgasını taşıyan zaman damgası sütunlarına sahip 8 (veya 9) özdeş satırı döndürür 2012-03-05 17:00:00. 9. sıra benim zaman dilimimde çalışıyor, ama kötü bir tuzak. Aşağıya bakınız.

Hawaii Hawaii saati için saat dilimi adı ve saat dilimi kısaltması içeren 6 - 8 numaralı satırlar DST'ye (yaz saati uygulaması) tabidir ve şu anda olmasa da farklılık gösterebilir. Gibi bir zaman dilimi adı 'US/Hawaii'DST kurallarının ve tüm tarihsel vardiyaların otomatik olarak farkındayken, böyle bir kısaltma HSTsabit bir ofset için sadece aptal bir koddur. Yaz / standart süre için farklı bir kısaltma eklemeniz gerekebilir. Adı doğru yorumladığı herhangi belirli bir zaman diliminde de zaman damgası. Bir kısaltma ucuzdur, ancak verilen zaman damgası için doğru olanı olması gerekir:

Yaz Saati Uygulaması, insanlığın ortaya çıkardığı en parlak fikirler arasında değildir.

Foot Yüklü tabanca olarak işaretlenmiş Satır 9 benim için çalışıyor , ancak sadece tesadüflerle. Açık bir şekilde bir değişmez değeri kullanırsanız timestamp [without time zone], herhangi bir saat dilimi uzaklığı yoksayılır ! Yalnızca çıplak zaman damgası kullanılır. Daha sonra değer, timestamptzsütun türüyle eşleşmesi için örnekte otomatik olarak zorlanır . Bu adım için, benim durumumda (Avrupa / Viyana) timezoneaynı saat dilimi olan geçerli oturumun ayarlandığı varsayılmaktadır +1. Ama muhtemelen sizin durumunuzda değil - bu farklı bir değerle sonuçlanacaktır. Kısaca: timestamptzDeğişmez değerler yerleştirmeyin timestampveya saat dilimi farkını kaybedersiniz.

Sorularınız

Kullanıcı 17 Mart 2012 saat 19: 00'da bir zaman depolar. Saat dilimi dönüşümlerinin veya saat diliminin depolanmasını istemiyorum.

Zaman diliminin kendisi asla kaydedilmez. UTC zaman damgası girmek için yukarıdaki yöntemlerden birini kullanın.

Kullanıcıların belirtilen saat dilimini, kullanıcıların yerel saat dilimindeki geçerli saatten önce 'önce' veya 'sonra' almak için kullanıyorum.

Farklı saat dilimlerindeki tüm istemciler için tek bir sorgu kullanabilirsiniz.
Mutlak küresel zaman için:

SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

Yerel saate göre zaman için:

SELECT * FROM tbl WHERE time_col > now()::time

Henüz arka plan bilgisinden sıkılmadınız mı? Kılavuzda daha fazlası var.


2
Küçük detaylar, ancak zaman damgalarının 2000-01-01'den bu yana mikrosaniye sayısı olarak dahili olarak saklandığını düşünüyorum - kılavuzun tarih / saat veri türü bölümüne bakın. Kendi kaynak denetimlerim bunu doğrulıyor gibi görünüyor. Çağ için farklı bir kaynak kullanmak garip!
zararlı

2
@harmic Farklı çağlara gelince… Aslında o kadar da garip değil. Bu Wikipedia sayfası , çeşitli bilgisayar sistemleri tarafından kullanılan iki düzine dönemi listeler. İken Unix çağ yaygındır, sadece bir değil.
Basil Bourque

4
@ErwinBrandstetter Bu, ciddi bir kusur hariç, harika bir cevap. Harmic yorumladığı gibi Postgrees yok değil Unix zamanı kullanın. Göre doc : (a) dönem 2001-01-01 yerine UNIX' den 1970-01-01, ve (b) Unix zaman bütün saniyelik bir çözünürlüğe sahip olmakla birlikte Postgrees saniye bölümlerini tutan bir. Kesirli basamak sayısı derleme zamanı seçeneğine bağlıdır: Sekiz baytlık tamsayı depolama (varsayılan) kullanıldığında 0 ila 6 veya kayan noktalı depolama (kullanımdan kaldırıldı) kullanıldığında 0 ila 10.
Basil Bourque

2
@BasilBourque: Bu talihsiz hatanın farkındayım. Eğer sakıncası yoksa, düzenlemeye açıktırsınız. Geçmişte bazı cevaplarınızı gördüm ve sen de iyisin. Benden bir başka düzenleme bunu topluluk wiki'sine zorlardı - zamanla bunu net ve kapsamlı hale getirmek için çok çaba harcadım (ve düzenlemeler).
Erwin Brandstetter

2
DÜZELTME: Daha önceki yorumumda, Postgres dönemini 2001 olarak yanlış göstermiştim. Aslında 2000 .
Basil Bourque

1

Varsayılan olarak UTC ile uğraşmak istiyorsanız:

Şuraya config/application.rbekleyin:

config.time_zone = 'UTC'

Sonra, geçerli kullanıcı saklarsanız saat dilimi adını current_user.timezonesöyleyebilirsiniz.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezonegeçerli bir saat dilimi adı olmalıdır, aksi takdirde alacaksınız ArgumentError: Invalid Timezone, tam listeye bakın .

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.