web-dev-qa-db-fra.com

Ignorer complètement les fuseaux horaires dans Rails et PostgreSQL

Je traite des dates et des heures dans Rails et Postgres et je rencontre ce problème:

La base de données est en UTC.

L'utilisateur définit un fuseau horaire de choix dans l'application Rails, mais il ne doit être utilisé que pour obtenir l'heure locale de l'utilisateur afin de comparer les temps.

L'utilisateur stocke une heure, par exemple le 17 mars 2012 à 19 heures. Je ne veux pas que les conversions de fuseau horaire ou le fuseau horaire soient stockés. Je veux juste que cette date et ce temps soient économisés. Ainsi, si l'utilisateur modifiait son fuseau horaire, le programme serait toujours affiché le 17 mars 2012 à 19 heures.

J'utilise uniquement le fuseau horaire spécifié par l'utilisateur pour obtenir des enregistrements «avant» ou «après» l'heure actuelle dans le fuseau horaire local des utilisateurs. 

J'utilise actuellement «horodatage sans fuseau horaire», mais lorsque je récupère les enregistrements, Rails (?) Les convertit dans le fuseau horaire de l'application, ce que je ne souhaite pas. 

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

Parce que les enregistrements de la base de données semblent sortir au format UTC, mon bidouillage consiste à prendre l'heure actuelle, à supprimer le fuseau horaire avec "Date.strptime (str,"% m /% d /% Y ")", puis requête avec ça:

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

Il semble qu'il existe un moyen plus simple d'ignorer les fuseaux horaires. Des idées?

143
99miles

Le type de données timestamp est le nom abrégé de timestamp without time zone.
L'autre option timestamptz est l'abréviation de timestamp with time zone.

timestamptz est le type préféré dans la famille de date/heure, littéralement. typispreferred est défini dans pg_type , ce qui peut être pertinent:

Époque

En interne , les horodatages sont stockés en tant que compte à partir de Epoch . Postgres utilise une époque du premier moment du premier jour de l’année 2000 en UTC, c’est-à-dire 2000-01-01T00: 00: 00Z. Huit octets sont utilisés pour stocker le numéro de compte. Selon une option de compilation, ce nombre est soit:

  • Un entier de 8 octets (par défaut), avec 0 à 6 chiffres d'une fraction de seconde
  • Nombre en virgule flottante (obsolète), avec 0 à 10 chiffres d'une fraction de seconde, où la précision se dégrade rapidement pour les valeurs plus éloignées de Epoch.
    Les installations modernes de Postgres utilisent un entier de 8 octets.

Notez que Postgres n'utilise pas heure Unix . L’époque de Postgres est le premier moment du 2000-01-01 au lieu d’Unix 1970-01-01. Alors que le temps Unix a une résolution de secondes entières, Postgres conserve des fractions de secondes.

timestamp

Si vous définissez un type de données timestamp[without time zone], vous dites à Postgres: "Je ne fournis pas de fuseau horaire explicitement, supposons à la place le fuseau horaire actuel. Postgres enregistre l'horodatage tel quel - ignorer un modificateur de fuseau horaire si vous en ajoutez un!

Lorsque vous affichez plus tard timestamp, vous récupérez ce que vous avez saisi littéralement. Avec le même fuseau horaire, tout va bien. Si le paramètre de fuseau horaire de la session change, la signification de timestamp change également - la valeur reste la même.

timestamptz

La manipulation de timestamp with time zone est légèrement différente. je cite le manuel ici :

Pour timestamp with time zone, la valeur stockée en interne est toujours en UTC (Universal Coordinated Time ...)

Gras accent mien. Le fuseau horaire lui-même n'est jamais enregistré. C'est un modificateur d'entrée utilisé pour calculer l'horodatage UTC correspondant, qui est stocké - ou un modificateur de sortie utilisé pour calculer l'heure locale à afficher - avec un décalage de fuseau horaire ajouté. Si vous n'ajoutez pas de décalage pour timestamptz en entrée, le paramètre de fuseau horaire actuel de la session est utilisé. Tous les calculs sont effectués avec les valeurs d'horodatage UTC. Si vous devez (ou devez éventuellement) gérer plusieurs fuseaux horaires, utilisez timestamptz.

Les clients tels que psql ou pgAdmin ou toute application communiquant via libpq (comme Ruby avec le gem pg) sont présentés avec l'horodatage plus le décalage pour le fuseau horaire actuel ou selon un fuseau horaire demandé (voir ci-dessous). C'est toujours le même moment , seul le format d'affichage varie. Ou, comme le dit le manuel :

Toutes les dates et heures tenant compte du fuseau horaire sont stockées en interne au format UTC. Ils sont convertis en heure locale dans la zone spécifiée par le paramètre de configuration TimeZone avant d’être affichés sur le client.

Prenons cet exemple simple (en psql):

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

Gras accent mien. Que s'est-il passé ici?
J'ai choisi un décalage de fuseau horaire arbitraire +3 pour le littéral d'entrée. Pour Postgres, il ne s’agit que d’une des nombreuses façons de saisir l’horodatage UTC 2012-03-05 17:00:00. Le résultat de la requête est affiché pour le paramètre de fuseau horaire actuel Vienne/Autriche dans mon test, avec un décalage +1 pendant l'hiver et +2 pendant heure d'été: 2012-03-05 18:00:00+01, car l'heure d'hiver tombe.

Postgres a déjà oublié comment cette valeur a été entrée. Tout ce dont il se souvient est la valeur et le type de données. Juste comme avec un nombre décimal. numeric '003.4', numeric '3.40' ou numeric '+3.4' - tous ont exactement la même valeur interne.

AT TIME ZONE

Dès que vous comprenez cette logique, vous pouvez faire ce que vous voulez. Il ne manque plus qu’un outil permettant d’interpréter ou de représenter les littéraux d’horodatage en fonction d’un fuseau horaire spécifique. C'est là que la construction AT TIME ZONE entre en jeu. Il existe deux cas d'utilisation différents. timestamptz est converti en timestamp et vice versa.

Pour entrer le timestamptz2012-03-05 17:00:00+0 UTC:

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

... qui équivaut à:

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

Pour afficher le même moment que EST timestamp (heure normale de l'Est):

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

C'est vrai, AT TIME ZONE 'UTC' deux fois . Le premier interprète la valeur timestamp comme un horodatage UTC (donné) renvoyant le type timestamptz. La seconde convertit timestamptz en timestamp dans le fuseau horaire indiqué 'EST' - ce qu'une horloge du fuseau horaire EST affiche à ce moment unique.

Exemples

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

Retourne 8 (ou 9) lignes identiques avec une colonne timestamptz contenant le même horodatage UTC 2012-03-05 17:00:00. La 9ème ligne fonctionne en quelque sorte dans mon fuseau horaire, mais est un piège pervers. Voir ci-dessous.

① Lignes 6 - 8 avec fuseau horaire nom et fuseau horaire abréviation pour l'heure d'Hawaï sont soumis à l'heure d'été et peuvent différer, mais pas pour le moment. Un nom de fuseau horaire tel que 'US/Hawaii' connaît les règles de l'heure d'été et tous les changements d'historique automatiquement, tandis qu'une abréviation telle que HST n'est qu'un code muet pour un décalage fixe. Vous devrez peut-être ajouter une abréviation différente pour l'heure d'été/heure standard. Le nom interprète correctement tout horodatage du fuseau horaire indiqué. Une abréviation n'est pas chère, mais doit être la bonne pour l'horodatage donné:

L'heure d'été ne fait pas partie des idées les plus brillantes jamais proposées à l'humanité.

La ligne 9, marquée comme footgun chargé , fonctionne pour moi , mais uniquement par coïncidence. Si vous convertissez explicitement un littéral en timestamp [without time zone], tout décalage de fuseau horaire est ignoré ! Seul l'horodatage nu est utilisé. La valeur est ensuite automatiquement contrainte à timestamptz dans l'exemple pour correspondre au type de colonne. Pour cette étape, le paramètre timezone de la session en cours est utilisé, ce qui correspond au même fuseau horaire +1 dans mon cas (Europe/Vienne). Mais probablement pas dans votre cas - ce qui entraînera une valeur différente. En bref: ne transtypez pas les littéraux timestamptz en timestamp, sinon vous perdrez le décalage de fuseau horaire.

Vos questions

L'utilisateur enregistre une heure, par exemple le 17 mars 2012 à 19 heures. Je ne veux pas que les conversions de fuseau horaire ou le fuseau horaire soient stockés.

Le fuseau horaire n'est jamais enregistré. Utilisez l’une des méthodes ci-dessus pour entrer un horodatage UTC.

J'utilise uniquement le fuseau horaire spécifié par l'utilisateur pour obtenir les enregistrements "avant" ou "après" l'heure actuelle dans le fuseau horaire local des utilisateurs.

Vous pouvez utiliser une requête pour tous les clients de différents fuseaux horaires.
Pour le temps global absolu:

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

Pour l'heure en fonction de l'horloge locale:

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

Pas fatigué d'informations de fond, encore? Il y a plus dans le manuel.

328

Si vous souhaitez traiter en UTC par défaut:

Dans config/application.rb, ajoutez:

config.time_zone = 'UTC'

Ensuite, si vous stockez le nom du fuseau horaire de l'utilisateur actuel est current_user.timezone, vous pouvez le dire.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezone doit être un nom de fuseau horaire valide, sinon vous obtiendrez ArgumentError: Invalid Timezone, voir liste complète .

0
Dorian