web-dev-qa-db-fra.com

Fuseaux horaires dans SQL DATE vs Java.sql.Date

Je suis un peu confus par le comportement du type de données SQL DATE par rapport à celui de Java.sql.Date. Prenons l'exemple suivant:

select cast(? as date)           -- in most databases
select cast(? as date) from dual -- in Oracle

Préparons et exécutons l'instruction avec Java

PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setDate(1, new Java.sql.Date(0)); // GMT 1970-01-01 00:00:00
ResultSet rs = stmt.executeQuery();
rs.next();

// I live in Zurich, which is CET, not GMT. So the following prints -3600000, 
// which is CET 1970-01-01 00:00:00
// ... or   GMT 1969-12-31 23:00:00
System.out.println(rs.getDate(1).getTime());

En d'autres termes, l'horodatage GMT que je lie à la déclaration devient l'horodatage CET que je récupère. À quelle étape le fuseau horaire est-il ajouté et pourquoi?

Remarque:

  • J'ai observé que cela était vrai pour l'une de ces bases de données:

    DB2, Derby, H2, HSQLDB, Ingres, MySQL, Oracle, Postgres, SQL Server, Sybase ASE, Sybase SQL Anywhere

  • J'ai observé que c'était faux pour SQLite (qui n'a pas vraiment de vrais types de données DATE)
  • Tout cela n'est pas pertinent lors de l'utilisation de Java.sql.Timestamp au lieu de Java.sql.Date
  • Il s'agit d'une question similaire, qui ne répond pas à cette question, cependant: Java.util.Date vs Java.sql.Date
36
Lukas Eder

spécification JDBC ne définit aucun détail concernant le fuseau horaire. Néanmoins, la plupart d'entre nous connaissent la peine de devoir faire face aux écarts de fuseau horaire JDBC; il suffit de regarder toutes les questions StackOverflow !

En fin de compte, la gestion du fuseau horaire pour les types de base de données date/heure se résume au serveur de base de données, au pilote JDBC et à tout le reste. Vous êtes même à la merci des bogues des pilotes JDBC; PostgreSQL a corrigé un bug dans la version 8.

Les méthodes Statement.getTime, .getDate et .getTimestamp transmises à un objet Calendar faisaient tourner le fuseau horaire dans la mauvaise direction.

Lorsque vous créez une nouvelle date à l'aide de new Date(0) (affirmons que vous utilisez Oracle JavaSE Java.sql.Date , votre date est créée

en utilisant la valeur de temps donnée en millisecondes. Si la valeur en millisecondes donnée contient des informations d'heure, le pilote définira les composants d'heure sur l'heure du fuseau horaire par défaut (le fuseau horaire de la machine virtuelle Java exécutant l'application) qui correspond à zéro GMT.

Ainsi, new Date(0) devrait utiliser GMT.

Lorsque vous appelez ResultSet.getDate(int) , vous exécutez une implémentation JDBC. La spécification JDBC ne dicte pas comment une implémentation JDBC doit gérer les détails du fuseau horaire; vous êtes donc à la merci de l'implémentation. En regardant Oracle 11g Oracle.sql.DATE JavaDoc, il ne semble pas qu'Oracle DB stocke les informations de fuseau horaire, il effectue donc ses propres conversions pour obtenir la date dans un Java.sql.Date . Je n'ai aucune expérience avec Oracle DB, mais je suppose que l'implémentation JDBC utilise les paramètres de fuseau horaire du serveur et de votre JVM locale pour effectuer la conversion de Oracle.sql.DATE En Java.sql.Date.


Vous mentionnez que plusieurs implémentations RDBMS gèrent correctement le fuseau horaire, à l'exception de SQLite. Voyons comment H2 et SQLite fonctionnent lorsque vous envoyez des valeurs de date au pilote JDBC et lorsque vous obtenez des valeurs de date à partir du pilote JDBC.

Le pilote H2 JDBC PrepStmt.setDate(int, Date) utilise ValueDate.get(Date) , qui appelle DateTimeUtils.dateValueFromDate(long) qui fait une conversion de fuseau horaire.

L'utilisation de ceci pilote JDBC SQLite , PrepStmt.setDate(int, Date) appelle PrepStmt.setObject(int, Object) et ne fait aucune conversion de fuseau horaire .

Le pilote H2 JDBC JdbcResultSet.getDate(int) renvoie get(columnIndex).getDate(). get(int) renvoie un H2 Value pour la colonne spécifiée. Comme le type de colonne est DATE, H2 utilise ValueDate . ValueDate.getDate() appelle DateTimeUtils.convertDateValueToDate(long) , ce qui crée finalement un Java.sql.Date après une conversion de fuseau horaire.

En utilisant ce pilote SQLite JDBC , le code RS.getDate(int) est beaucoup plus simple; il retourne juste un Java.sql.Date en utilisant la valeur de date long stockée dans la base de données.

Nous voyons donc que le pilote H2 JDBC est intelligent pour gérer les conversions de fuseau horaire avec des dates alors que le pilote JDBC SQLite ne l'est pas (pour ne pas dire que cette décision n'est pas intelligente, elle pourrait bien convenir aux décisions de conception SQLite). Si vous poursuivez la source des autres pilotes RDBMS JDBC que vous mentionnez, vous constaterez probablement que la plupart approchent de la date et du fuseau horaire de la même manière que H2.

Bien que les spécifications JDBC ne détaillent pas la gestion des fuseaux horaires, il est logique que les concepteurs d'implémentation RDBMS et JDBC aient pris en compte le fuseau horaire et le gèrent correctement; surtout s'ils veulent que leurs produits soient commercialisables sur la scène mondiale. Ces concepteurs sont sacrément intelligents et je ne suis pas surpris que la plupart d'entre eux réussissent, même en l'absence de spécifications concrètes.


J'ai trouvé ce blog Microsoft SQL Server, tilisation des données de fuseau horaire dans SQL Server 2008 , qui explique comment le fuseau horaire complique les choses:

les fuseaux horaires sont un domaine complexe et chaque application devra déterminer comment vous allez gérer les données de fuseau horaire pour rendre les programmes plus conviviaux.

Malheureusement, il n'existe actuellement aucune autorité internationale normalisée pour les noms et les valeurs de fuseau horaire. Chaque système doit utiliser un système de son choix, et tant qu'il n'y aura pas de norme internationale, il n'est pas possible d'essayer de faire en sorte que SQL Server en fournisse un, et causerait finalement plus de problèmes qu'il n'en résoudrait.

38
Dan Cruz

C'est le pilote jdbc qui effectue la conversion. Il doit convertir l'objet Date dans un format acceptable par le format db/wire et lorsque ce format n'inclut pas de fuseau horaire, la tendance est de revenir par défaut au paramètre de fuseau horaire de la machine locale lors de l'interprétation de la date. Ainsi, le scénario le plus probable, étant donné la liste des pilotes que vous spécifiez, est que vous définissez la date sur GMT 1970-1-1 00:00:00 mais la date interprétée lorsque vous la définissez sur la déclaration était CET 1970-1-1 1:00:00. Étant donné que la date n'est que la partie date, vous obtenez 1970-1-1 (sans fuseau horaire) envoyé au serveur et renvoyé à vous. Lorsque le conducteur récupère la date et que vous y accédez en tant que date, il voit 1970-1-1 et l'interprète à nouveau avec le fuseau horaire local, à savoir CET 1970-1-1 00:00:00 ou GMT 1969-12. -31 23h00:00. Par conséquent, vous avez "perdu" une heure par rapport à la date d'origine.

11
philwb

Les objets Java.util.Date et Oracle/MySQL Date sont simplement des représentations d'un point dans le temps, quel que soit leur emplacement. Cela signifie qu'il est très susceptible d'être stocké en interne comme le nombre de milli/nano secondes depuis l '"Epoch", GMT 1970-01-01 00:00:00.

Lorsque vous lisez à partir du jeu de résultats, votre appel à "rs.getDate ()" indique au jeu de résultats de prendre les données internes contenant le point dans le temps et de les convertir en un objet Java Date. l'objet de date est créé sur votre machine locale, donc Java choisira votre fuseau horaire local, qui est CET pour Zurich.

La différence que vous voyez est une différence de représentation, pas une différence de temps.

5
Rolf

La classe Java.sql.Date Correspond à SQL DATE, qui ne stocke pas d'informations sur l'heure ou le fuseau horaire. La façon dont cela est accompli consiste à "normaliser" la date, comme le dit javadoc :

Pour se conformer à la définition de SQL DATE, les valeurs en millisecondes encapsulées par une instance Java.sql.Date doivent être "normalisées" en définissant les heures, minutes, secondes et millisecondes à zéro dans le fuseau horaire particulier auquel l'instance est associée .

Cela signifie que lorsque vous travaillez en UTC + 1 et demandez à la base de données une DATE, une implémentation conforme fait exactement ce que vous avez observé: renvoyer un Java.sql.Date avec une valeur en millisecondes qui correspond à la date en question à 00:00:00 UTC + 1 indépendamment de la façon dont les données sont entrées dans la base de données en premier lieu.

Les pilotes de base de données peuvent permettre de modifier ce comportement via des options si ce n'est pas ce que vous voulez.

En revanche, lorsque vous transmettez un Java.sql.Date À la base de données, le pilote utilise le fuseau horaire par défaut pour séparer les composants de date et d'heure de la valeur en millisecondes. Si vous utilisez 0 Et que vous êtes en UTC + X, la date sera 1970-01-01 pour X> = 0 et 1969-12-31 pour X <0.


Sidenote: Il est étrange de voir que la documentation pour le constructeur Date(long) diffère de l'implémentation. Le javadoc dit ceci:

Si la valeur en millisecondes donnée contient des informations d'heure, le pilote définira les composants d'heure sur l'heure du fuseau horaire par défaut (le fuseau horaire de la machine virtuelle Java exécutant l'application) qui correspond à zéro GMT.

Cependant, ce qui est réellement implémenté dans OpenJDK est le suivant:

public Date(long date) {
    // If the millisecond date value contains time info, mask it out.
    super(date);

}

Apparemment, ce "masquage" n'est pas implémenté. Tout aussi bien parce que le comportement spécifié n'est pas bien spécifié, par ex. 1970-01-01 00:00:00 GMT-6 = 1970-01-01 06:00:00 GMT doit-il être mappé au 1970-01-01 00:00:00 GMT = 1969-12-31 18:00: 00 GMT-6 ou jusqu'au 1970-01-01 18:00:00 GMT-6?

4
Joni

Il y a une surcharge de getDate qui accepte un Calendar, est-ce que cela pourrait résoudre votre problème? Ou vous pouvez utiliser un objet Calendar pour convertir les informations.

Autrement dit, en supposant que la récupération est le problème.

Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setDate(1, new Date(0)); // I assume this is always GMT
ResultSet rs = stmt.executeQuery();
rs.next();

//This will output 0 as expected
System.out.println(rs.getDate(1, gmt).getTime());

Alternativement, en supposant que le stockage est le problème.

Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
PreparedStatement stmt = connection.prepareStatement(sql);
gmt.setTimeInMillis(0) ;
stmt.setDate(1, gmt.getTime()); //getTime returns a Date object
ResultSet rs = stmt.executeQuery();
rs.next();

//This will output 0 as expected
System.out.println(rs.getDate(1).getTime());

Je n'ai pas d'environnement de test pour cela, vous devrez donc savoir quelle est la bonne hypothèse. Je crois également que getTime renvoie les paramètres régionaux actuels, sinon vous devrez chercher comment faire une conversion rapide du fuseau horaire.

2
Guvante

JDBC 4.2 et Java.time

PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setObject(1, LocalDate.Epoch); // The Epoch year LocalDate, '1970-01-01'.
ResultSet rs = stmt.executeQuery();
rs.next();

// It doesnt matter where you live or your JVM’s time zone setting
// since a LocalDate doesn’t have or use time zone
System.out.println(rs.getObject(1, LocalDate.class));

Je n'ai pas testé, mais à moins qu'il n'y ait un bug dans votre pilote JDBC, la sortie dans tous les fuseaux horaires est:

1970-01-01

LocalDate est une date sans heure et sans fuseau horaire. Il n'y a donc aucune valeur en millisecondes à obtenir et aucune heure de la journée à confondre. LocalDate fait partie de Java.time, la moderne Java API de date et d'heure. JDBC 4.2 spécifie que vous pouvez transférer des objets Java.time, y compris LocalDate vers et à partir de votre base de données SQL via les méthodes setObject et getObject que j'utilise dans l'extrait (donc pas setDate ni getDate).

Je pense que pratiquement nous utilisons tous les pilotes JDBC 4.2.

Qu'est-ce qui a mal tourné dans votre code?

Java.sql.Date Est un hack essayant (pas très bien) de déguiser un Java.util.Date En date sans heure de la journée. Je vous recommande de ne pas utiliser ce cours.

De la documentation:

Pour se conformer à la définition de SQL DATE, les valeurs en millisecondes encapsulées par une instance de Java.sql.Date Doivent être "normalisées" en définissant les heures, minutes, secondes et millisecondes à zéro dans le fuseau horaire particulier auquel l'instance est associée.

"Associé" peut être vague. Cela signifie, je crois, que la valeur en millisecondes doit indiquer le début de la journée dans le fuseau horaire par défaut de votre JVM (Europe/Zurich dans votre cas, je suppose). Ainsi, lorsque vous créez new Java.sql.Date(0) égal à GMT 1970-01-01 00:00:00, c'est vraiment vous qui créez un objet Date incorrect, car cette heure est 01:00:00 dans votre fuseau horaire. JDBC ignore l'heure du jour (nous nous attendions peut-être à un message d'erreur, mais nous n'en avons pas). Ainsi, SQL obtient et renvoie une date du 01/01/1970. JDBC traduit correctement cela en Date contenant CET 1970-01-01 00:00:00. Ou une valeur en millisecondes de -3 600 000. Oui, c'est déroutant. Comme je l'ai dit, n'utilisez pas cette classe.

Si vous aviez été à un décalage GMT négatif (par exemple la plupart des fuseaux horaires dans les Amériques), new Java.sql.Date(0) aurait été interprété comme 1969-12-31, donc cela aurait été la date à laquelle vous aviez passé et est revenu de SQL.

Pour aggraver les choses, pensez à ce qui se passe lorsque le paramètre de fuseau horaire de la JVM est modifié. N'importe quelle partie de votre programme et tout autre programme s'exécutant dans la même JVM peut le faire à tout moment. Cela entraîne généralement la non validité de tous les objets Java.sql.Date Créés avant la modification et peut parfois décaler leur date d'un jour (dans de rares cas de deux jours).

Liens

1
Ole V.V.

Je pense que les problèmes de fuseau horaire sont trop compliqués. Lorsque vous transmettez une date/heure à une instruction préparée, elle envoie uniquement des informations de date et/ou d'heure au serveur de base de données. Conformément à la norme de facto, il ne manipule pas les différences de fuseau horaire entre le serveur de base de données et jvm. Et plus tard, lorsque vous lisez la date/l'heure de l'ensemble de résultats. Il lit simplement les informations de date et d'heure dans la base de données et crée la même date/heure dans le fuseau horaire jvm.

0
Syed Aqeel Ashiq