web-dev-qa-db-fra.com

Occasion manquée de corriger la gestion des dates JDBC dans Java 8?

Est-ce qu'un Java 8 + expert JDBC peut me dire si quelque chose ne va pas dans le raisonnement suivant? Et, si dans les secrets de Dieu, pourquoi cela n'a pas été fait?

Un Java.sql.Date Est actuellement le type utilisé par JDBC pour mapper au type SQL DATE, qui représente une date sans heure et sans fuseau horaire. Mais cette classe est terriblement conçue, car il s'agit en fait d'une sous-classe de Java.util.Date, Qui stocke un instant précis dans le temps, jusqu'à la milliseconde.

Pour représenter la date 2015-09-13 dans la base de données, nous sommes donc obligés de choisir un fuseau horaire, analyser la chaîne "2015-09-13T00: 00: 00.000" dans ce fuseau horaire en tant que Java.util.Date pour obtenir une milliseconde valeur, puis construire un Java.sql.Date à partir de cette valeur en millisecondes, et enfin appeler setDate() sur l'instruction préparée, en passant un calendrier contenant le fuseau horaire choisi pour que le pilote JDBC puisse correctement recalculer la date 2015-09-13 à partir de cette valeur en millisecondes. Ce processus est rendu un peu plus simple en utilisant le fuseau horaire par défaut partout, et en ne passant pas un calendrier.

Java 8 introduit une classe LocalDate, qui est bien mieux adaptée au type de base de données DATE, car ce n'est pas un moment précis dans le temps et ne dépend donc pas du fuseau horaire. Et Java 8 introduit également des méthodes par défaut, qui permettraient d'apporter des modifications rétrocompatibles aux interfaces PreparedStatement et ResultSet.

Alors, n'avons-nous pas raté une énorme occasion de nettoyer le gâchis dans JDBC tout en conservant la compatibilité descendante? Java 8 aurait simplement pu ajouter ces méthodes par défaut à PreparedStatement et ResultSet:

default public void setLocalDate(int parameterIndex, LocalDate localDate) {
    if (localDate == null) {
        setDate(parameterIndex, null);
    }
    else {
        ZoneId utc = ZoneId.of("UTC");
        Java.util.Date utilDate = Java.util.Date.from(localDate.atStartOfDay(utc).toInstant());
        Date sqlDate = new Date(utilDate.getTime());
        setDate(parameterIndex, sqlDate, Calendar.getInstance(TimeZone.getTimeZone(utc)));
    }
}

default LocalDate getLocalDate(int parameterIndex) {
    ZoneId utc = ZoneId.of("UTC");
    Date sqlDate = getDate(parameterIndex, Calendar.getInstance(TimeZone.getTimeZone(utc)));
    if (sqlDate == null) {
        return null;
    }

    Java.util.Date utilDate = new Java.util.Date(sqlDate.getTime());
    return utilDate.toInstant().atZone(utc).toLocalDate();
}

Bien sûr, le même raisonnement s'applique à la prise en charge d'Instant pour le type TIMESTAMP et à la prise en charge de LocalTime pour le type TIME.

38
JB Nizet

Pour représenter la date 2015-09-13 dans la base de données, nous sommes donc obligés de choisir un fuseau horaire, analyser la chaîne "2015-09-13T00: 00: 00.000" dans ce fuseau horaire en tant que Java.util.Date pour obtenir une milliseconde , puis construisez un Java.sql.Date à partir de cette valeur en millisecondes, et enfin appelez setDate () sur l'instruction préparée, en passant un calendrier contenant le fuseau horaire choisi pour que le pilote JDBC puisse correctement recalculer la date 2015-09 -13 à partir de cette valeur en millisecondes

Pourquoi? Il suffit d'appeler

Date.valueOf("2015-09-13"); // From String
Date.valueOf(localDate);    // From Java.time.LocalDate

Le comportement sera le bon dans tous les pilotes JDBC: la date locale sans fuseau horaire. L'opération inverse est:

date.toString();    // To String
date.toLocalDate(); // To Java.time.LocalDate

Vous ne devriez jamais compter sur Java.sql.Date la dépendance de Java.util.Date , et le fait qu'il hérite ainsi de la sémantique d'un Java.time.Instant via Date(long) ou Date.getTime()

Alors, n'avons-nous pas raté une énorme occasion de nettoyer le gâchis dans JDBC tout en conservant la compatibilité descendante? [...]

Ça dépend. spécification JDBC 4.2 spécifie que vous pouvez lier un type LocalDate via setObject(int, localDate) , et récupérer un LocalDate tapez via getObject(int, LocalDate.class) , si le pilote est prêt pour cela. Pas aussi élégant que les méthodes default plus formelles, comme vous l'avez suggéré, bien sûr.

29
Lukas Eder

Réponse courte: non, la gestion de la date et de l'heure est fixée dans Java 8/JDBC 4.2 car elle prend en charge types de date et d'heure Java 8 . Cela ne peut pas être émulé à l'aide de méthodes par défaut.

Java 8 aurait simplement pu ajouter ces méthodes par défaut à PreparedStatement et ResultSet:

Ce n'est pas possible pour plusieurs raisons:

  • Java.time.OffsetDateTime n'a pas d'équivalent dans Java.sql
  • Java.time.LocalDateTime interrompt les transitions DST lors de la conversion via Java.sql.Timestamp car ce dernier dépend du fuseau horaire de la JVM, voir le code ci-dessous
  • Java.sql.Time a une résolution en millisecondes mais Java.time.LocalTime a une résolution en nanosecondes

Par conséquent, vous avez besoin d'une prise en charge appropriée du pilote, les méthodes par défaut devraient être converties via Java.sql types qui introduisent une perte de données.

Si vous exécutez ce code dans un fuseau horaire JVM avec l'heure d'été, il se cassera. Il recherche la transition suivante dans laquelle les horloges sont "mises en avant" et sélectionne un LocalDateTime qui est en plein milieu de la transition. Ceci est parfaitement valable car Java LocalDateTime ou SQL TIMESTAMP n'ont pas de fuseau horaire et donc pas de règles de fuseau horaire et donc pas d'heure d'été. Java.sql.Timestamp d'autre part est lié au fuseau horaire JVM et donc soumis à l'heure d'été.

ZoneId systemTimezone = ZoneId.systemDefault();
Instant now = Instant.now();

ZoneRules rules = systemTimezone.getRules();
ZoneOffsetTransition transition = rules.nextTransition(now);
assertNotNull(transition);
if (!transition.getDateTimeBefore().isBefore(transition.getDateTimeAfter())) {
  transition = rules.nextTransition(transition.getInstant().plusSeconds(1L));
  assertNotNull(transition);
}

Duration gap = Duration.between(transition.getDateTimeBefore(), transition.getDateTimeAfter());
LocalDateTime betweenTransitions = transition.getDateTimeBefore().plus(gap.dividedBy(2L));

Timestamp timestamp = Java.sql.Timestamp.valueOf(betweenTransitions);
assertEquals(betweenTransitions, timestamp.toLocalDateTime());
6