Bỏ qua các múi giờ hoàn toàn trong Rails và PostgreSQL


164

Tôi đang xử lý ngày và giờ trong Rails và Postgres và gặp vấn đề này:

Cơ sở dữ liệu trong UTC.

Người dùng đặt múi giờ lựa chọn trong ứng dụng Rails, nhưng nó chỉ được sử dụng khi lấy thời gian cục bộ của người dùng để so sánh thời gian.

Người dùng lưu trữ một thời gian, giả sử ngày 17 tháng 3 năm 2012, 7 giờ tối. Tôi không muốn chuyển đổi múi giờ hoặc múi giờ được lưu trữ. Tôi chỉ muốn ngày và thời gian đó được lưu. Theo cách đó, nếu người dùng thay đổi múi giờ của họ, Nó vẫn sẽ hiển thị ngày 17 tháng 3 năm 2012, 7 giờ tối.

Tôi chỉ sử dụng múi giờ do người dùng chỉ định để nhận bản ghi 'trước' hoặc 'sau' thời gian hiện tại trong múi giờ địa phương của người dùng.

Tôi hiện đang sử dụng 'dấu thời gian không có múi giờ' nhưng khi tôi truy xuất các bản ghi, đường ray (?) Chuyển đổi chúng thành múi giờ trong ứng dụng mà tôi không muốn.

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

Vì các bản ghi trong cơ sở dữ liệu dường như xuất hiện dưới dạng UTC, nên việc hack của tôi là lấy thời gian hiện tại, xóa múi giờ bằng 'Date.strptime (str, "% m /% d /% Y")' và sau đó thực hiện truy vấn với điều đó:

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

Có vẻ như phải có một cách dễ dàng hơn để bỏ qua các múi giờ xung quanh. Có ý kiến ​​gì không?

Câu trả lời:


347

Kiểu dữ liệu timestamplà tên viết tắt của timestamp without time zone.
Các tùy chọn khác timestamptzlà viết tắt của timestamp with time zone.

timestamptzlà loại ưa thích trong gia đình ngày / giờ, theo nghĩa đen. Nó đã typispreferredđược thiết lập pg_type, có thể có liên quan:

Lưu trữ nội bộ và kỷ nguyên

Trong nội bộ, dấu thời gian chiếm 8 byte lưu trữ trên đĩa và trong RAM. Đó là một giá trị số nguyên biểu thị số micrô giây từ kỷ nguyên Postgres, 2000-01-01 00:00:00 UTC.

Postgres cũng có kiến ​​thức tích hợp về thời gian UNIX thường được sử dụng tính từ giây UNIX, 1970-01-01 00:00:00 UTC và sử dụng chức năng đó trong các chức năng to_timestamp(double precision)hoặc EXTRACT(EPOCH FROM timestamptz).

Mã nguồn:

* Dấu thời gian, cũng như các trường h / m / s của các khoảng, được lưu trữ dưới dạng
* giá trị int64 với đơn vị micro giây. (Ngày xửa ngày xưa  
* giá trị gấp đôi với đơn vị giây.)

Và:

/ * Tương đương ngày Julian của Ngày 0 trong Unix và Postgres tính toán * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

Độ phân giải micro giây chuyển thành tối đa 6 chữ số phân số trong vài giây.

timestamp

Một giá trị được gõ như nói với Postgres rằng không có múi giờ nào được cung cấp rõ ràng. Múi giờ hiện tại được giả định. Postgres bỏ qua bất kỳ sửa đổi múi giờ được thêm vào do nhầm lẫn!timestamp [without time zone]

Không có giờ được thay đổi để hiển thị. Với cùng một cài đặt múi giờ, tất cả đều ổn. Đối với cài đặt múi giờ khác, ý nghĩa thay đổi, nhưng giá trịhiển thị giữ nguyên.

timestamptz

Xử lý timestamp with time zonelà khác nhau tinh tế. Tôi trích dẫn hướng dẫn ở đây :

Đối với timestamp with time zone, giá trị được lưu trữ nội bộ luôn ở UTC (Giờ phối hợp toàn cầu ...)

Nhấn mạnh đậm của tôi. Các múi giờ riêng của mình là không bao giờ được lưu trữ . Nó là một công cụ sửa đổi đầu vào được sử dụng để tính toán dấu thời gian theo UTC, được lưu trữ - hoặc và công cụ sửa đổi đầu ra được sử dụng để tính thời gian cục bộ để hiển thị - với độ lệch múi giờ được nối thêm. Nếu bạn không nối phần bù cho timestamptzđầu vào, cài đặt múi giờ hiện tại của phiên được giả định. Tất cả các tính toán được thực hiện với các giá trị dấu thời gian UTC. Nếu bạn phải (hoặc có thể phải) đối phó với nhiều hơn một múi giờ, hãy sử dụng timestamptz.

Các máy khách như psql hoặc pgAdmin hoặc bất kỳ ứng dụng nào giao tiếp qua libpq (như Ruby với đá quý pg) được hiển thị với dấu thời gian cộng với bù cho múi giờ hiện tại hoặc theo múi giờ được yêu cầu (xem bên dưới). Nó luôn luôn là cùng một thời điểm , chỉ có định dạng hiển thị khác nhau. Hoặc, như hướng dẫn đặt nó :

Tất cả các ngày và giờ nhận biết múi giờ được lưu trữ nội bộ trong UTC. Chúng được chuyển đổi thành giờ địa phương trong vùng được chỉ định bởi tham số cấu hình TimeZone trước khi được hiển thị cho máy khách.

Hãy xem xét ví dụ đơn giản này (trong psql):

db = # CHỌN dấu thời gian ' 2012 / 03-05 20:00 +03 ';
      dấu thời gian
------------------------
 2012 / 03-05 18:00:00 +01

Nhấn mạnh đậm của tôi. Chuyện gì đã xảy ra ở đây?
Tôi đã chọn một bù múi giờ tùy ý +3cho chữ đầu vào. Đối với Postgres, đây chỉ là một trong nhiều cách để nhập dấu thời gian UTC 2012-03-05 17:00:00. Kết quả của truy vấn được hiển thị cho cài đặt múi giờ hiện tại Vienna / Áo trong thử nghiệm của tôi, có phần bù +1trong mùa đông và +2trong thời gian mùa hè : 2012-03-05 18:00:00+01, vì nó rơi vào thời điểm mùa đông.

Postgres đã quên cách nhập giá trị này. Tất cả những gì nó nhớ là giá trị và kiểu dữ liệu. Giống như với một số thập phân. numeric '003.4', numeric '3.40'Hoặc numeric '+3.4'- tất cả các kết quả về giá trị nội bộ cùng chính xác.

AT TIME ZONE

Ngay khi bạn nắm bắt được logic này, bạn có thể làm bất cứ điều gì bạn muốn. Tất cả những gì còn thiếu bây giờ, là một công cụ để giải thích hoặc biểu thị các ký tự dấu thời gian theo một múi giờ cụ thể. Đó là nơi mà AT TIME ZONEcấu trúc xuất hiện. Có hai trường hợp sử dụng khác nhau. timestamptzđược chuyển đổi thành timestampvà ngược lại.

Để vào UTC timestamptz 2012-03-05 17:00:00+0:

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

... tương đương với:

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

Để hiển thị cùng thời điểm với EST timestamp(Giờ chuẩn miền đông):

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

Điều đó đúng, AT TIME ZONE 'UTC' hai lần . Cái đầu tiên diễn giải timestampgiá trị là dấu thời gian UTC (đã cho) trả về kiểu timestamptz. Điều thứ hai chuyển đổi timestamptzsang timestamptheo múi giờ cho 'EST' - những gì một chiếc đồng hồ trong thời gian hiển thị khu EST vào thời điểm đặc biệt này trong thời gian.

Ví dụ

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

Trả về 8 (hoặc 9) hàng giống hệt nhau với các cột dấu thời gian giữ cùng dấu thời gian UTC 2012-03-05 17:00:00. Hàng thứ 9 xảy ra để hoạt động trong múi giờ của tôi, nhưng là một cái bẫy xấu xa. Xem bên dưới.

Hàng 6 - 8 với tên múi giờ và viết tắt múi giờ cho giờ Hawaii phải tuân theo DST (thời gian tiết kiệm ánh sáng ban ngày) và có thể khác nhau, mặc dù hiện tại không. Tên múi giờ giống như 'US/Hawaii'nhận thức được các quy tắc DST và tất cả các thay đổi lịch sử tự động, trong khi tên viết tắt giống như HSTchỉ là một mã câm cho phần bù cố định. Bạn có thể cần phải viết thêm một chữ viết tắt khác cho mùa hè / giờ chuẩn. Các tên giải thích một cách chính xác bất kỳ dấu thời gian tại các múi giờ nhất định. Một chữ viết tắt là rẻ, nhưng cần phải là chữ viết đúng cho dấu thời gian đã cho:

Giờ tiết kiệm ánh sáng ban ngày không phải là một trong những ý tưởng sáng chói nhất mà nhân loại từng nghĩ ra.

② Row 9, đánh dấu là footgun tải làm việc cho tôi , nhưng chỉ bằng cách trùng hợp ngẫu nhiên. Nếu bạn sử dụng một cách rõ ràng theo nghĩa đen timestamp [without time zone], bất kỳ khoảng thời gian nào đều bị bỏ qua ! Chỉ có dấu thời gian trần được sử dụng. Giá trị sau đó được tự động ép buộc timestamptztrong ví dụ để khớp với loại cột. Đối với bước này, timezonecài đặt của phiên hiện tại được giả sử, đó là cùng múi giờ +1trong trường hợp của tôi (Châu Âu / Vienna). Nhưng có lẽ không phải trong trường hợp của bạn - sẽ dẫn đến một giá trị khác. Nói tóm lại: Đừng bỏ timestamptzchữ timestamphoặc bạn mất phần bù múi giờ.

Những câu hỏi của bạn

Người dùng lưu trữ một thời gian, giả sử ngày 17 tháng 3 năm 2012, 7 giờ tối. Tôi không muốn chuyển đổi múi giờ hoặc múi giờ được lưu trữ.

Múi giờ chính nó không bao giờ được lưu trữ. Sử dụng một trong các phương pháp trên để nhập dấu thời gian UTC.

Tôi chỉ sử dụng múi giờ do người dùng chỉ định để nhận bản ghi 'trước' hoặc 'sau' thời gian hiện tại trong múi giờ địa phương của người dùng.

Bạn có thể sử dụng một truy vấn cho tất cả khách hàng ở các múi giờ khác nhau.
Đối với thời gian toàn cầu tuyệt đối:

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

Đối với thời gian theo đồng hồ địa phương:

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

Không mệt mỏi với thông tin cơ bản, chưa? Có nhiều hơn trong hướng dẫn.


2
Chi tiết nhỏ, nhưng tôi nghĩ rằng dấu thời gian được lưu trữ bên trong dưới dạng số micrô giây kể từ 2000-01-01 - xem phần kiểu dữ liệu ngày / giờ của hướng dẫn. Kiểm tra riêng của tôi về nguồn dường như xác nhận nó. Thật kỳ lạ khi sử dụng một nguồn gốc khác nhau cho kỷ nguyên!
hại

2
@harmic Còn về epoch khác nhau Thực ra không lạ lắm. Đây trang Wikipedia liệt kê hai chục kỷ nguyên sử dụng bởi hệ thống máy tính khác nhau. Mặc dù thời đại Unix là phổ biến, nhưng nó không phải là duy nhất.
Basil Bourque

4
@ErwinBrandstetter Đây là một câu trả lời tuyệt vời , ngoại trừ một lỗ hổng nghiêm trọng. Như bình luận có hại, Postgres không sử dụng thời gian Unix. Theo tài liệu : (a) Kỷ nguyên là 2001-01-01 thay vì Unix '1970-01-01 và (b) Trong khi thời gian Unix có độ phân giải toàn bộ giây, Postgres giữ phân số của giây. Số chữ số phân số phụ thuộc vào tùy chọn thời gian biên dịch: 0 đến 6 khi lưu trữ số nguyên tám byte (mặc định) được sử dụng hoặc từ 0 đến 10 khi sử dụng bộ lưu trữ dấu phẩy động (không dùng nữa).
Basil Bourque

2
@BasilBourque: Tôi nhận thức được sai lầm đáng tiếc này. Nếu bạn không phiền, bạn rất sẵn lòng chỉnh sửa nó. Tôi đã thấy một số câu trả lời của bạn trong quá khứ và bạn giỏi về nó. Một chỉnh sửa nữa từ tôi sẽ buộc điều này đến wiki cộng đồng - theo thời gian tôi đã nỗ lực rất nhiều (và chỉnh sửa) để làm cho nó rõ ràng và toàn diện.
Erwin Brandstetter

2
ĐÚNG: Theo nhận xét trước đây của tôi, tôi đã trích dẫn không chính xác kỷ nguyên Postgres là năm 2001. Thực tế đó là năm 2000 .
Basil Bourque

1

Nếu bạn muốn giao dịch trong UTC theo mặc định:

Trong config/application.rb, thêm:

config.time_zone = 'UTC'

Sau đó, nếu bạn lưu trữ tên múi giờ người dùng hiện tại là current_user.timezonebạn có thể nói.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezonephải là tên múi giờ hợp lệ, nếu không bạn sẽ nhận được ArgumentError: Invalid Timezone, xem danh sách đầy đủ .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.