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.
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.
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-dessousJava.sql.Time
a une résolution en millisecondes mais Java.time.LocalTime
a une résolution en nanosecondesPar 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());