Question

Java & amp; Oracle possède un type timestamp appelé Date. Les développeurs ont tendance à les manipuler comme s’il s’agissait de dates du calendrier , que j’ai vu causer de vilains bugs ponctuels.

  1. Pour une quantité de date de base, vous pouvez simplement couper la partie heure lors de la saisie, c’est-à-dire réduire la précision. Mais si vous faites cela avec une plage de dates (par exemple: 9 / 29-9 / 30 ), la différence entre ces deux valeurs est d’un jour et non de deux. En outre, les comparaisons de plages requièrent soit 1 ) une opération tronquée: start < trunc(now) <= end, ou 2) arithmétique: start < now < (end + 24hrs). Pas horrible, mais pas DRY .

  2. Une alternative consiste à utiliser des horodatages réels: 9/29 00:00:00 - 10/1 00:00:00. (minuit à minuit, n'inclut donc aucune partie d'octobre). Désormais, les durées sont intrinsèquement correctes et les comparaisons de plages plus simples: start <= now < end. Certainement plus propre pour le traitement interne, cependant les dates de fin doivent être converties lors de la saisie initiale (+1), et pour la sortie (-1), en supposant une métaphore de la date du calendrier au niveau utilisateur.

Comment gérez-vous les plages de dates de votre projet? Y a-t-il d'autres alternatives? Je suis particulièrement intéressé par la façon dont vous gérez cela, à la fois du côté de Java et du côté de Oracle.

Était-ce utile?

La solution

Voici comment nous le faisons.

  1. Utiliser des horodatages.

  2. Utilisez des intervalles semi-ouverts pour la comparaison: start <= now < end.

Ignorez les gémissements qui insistent sur le fait que BETWEEN est essentiel au succès de SQL.

Avec cela, une série de plages de dates est vraiment facile à auditer. La valeur de la base de données pour 9/30 to 10/1 comprend un jour (9/30). Le début de l'intervalle suivant doit être égal à la fin de l'intervalle précédent. Cette interval[n-1].end == interval[n].start règle est utile pour les audits.

Lorsque vous affichez, si vous le souhaitez, vous pouvez afficher les start et end - 1 formatés. Il se trouve que vous pouvez éduquer les gens pour qu’ils comprennent que la & "; Fin &"; C'est en fait le premier jour où la règle n'est plus vraie. Donc & Quot; 9/30 à 10/1 & Quot; signifie " valide à partir du 9/30, n'est plus valide à partir du 10/1 &>;

Autres conseils

Oracle dispose du datatype TIMESTAMP . Il stocke l'année, le mois et le jour du type de données DATE, ainsi que les valeurs d'heure, de minute, de seconde et de seconde fractionnaire.

Voici un thread sur asktom.oracle.com à propos de l'arithmétique des dates.

Je partage ce que S.Lott a expliqué. Nous avons une suite de produits qui utilise énormément les plages de dates et l’une de nos leçons apprises est de travailler avec de telles plages. En passant, nous appelons la date de fin exclusive si elle ne fait plus partie de la plage (IOW, intervalle mi-ouvert). En revanche, il s’agit d’une date de fin inclusive si elle compte comme une partie de la plage qui n’a de sens que s’il n’ya pas de tranche de temps.

Les utilisateurs attendent généralement des entrées / sorties de plages de dates incluses. Dans tous les cas, convertissez les entrées utilisateur le plus rapidement possible en plages de dates de fin exclusives et convertissez toute plage de dates le plus tard possible, le cas échéant.

Dans la base de données, stockez toujours les plages de dates de fin exclusives. S'il existe des données existantes avec des plages de dates de fin inclusives, migrez-les si possible sur la base de données ou convertissez-les en plage de dates de fin exclusive dès que possible lors de la lecture des données.

J'utilise le type de données de date d'Oracle et sensibilise les développeurs à la question des composants temporels affectant les conditions aux limites.

Une contrainte de base de données empêchera également la spécification accidentelle d'un composant heure dans une colonne qui devrait en être vide et indique également à l'optimiseur qu'aucune des valeurs n'a de composant heure.

Par exemple, la contrainte CHECK (MY_DATE = TRUNC (MY_DATE)) empêche de placer une valeur avec une heure autre que 00:00:00 dans la colonne my_date et permet également à Oracle d'inférer qu'un prédicat tel que MY_DATE = TO_DATE ('2008-09-12 15:00:00') ne sera jamais vrai et, par conséquent, aucune ligne ne sera renvoyée de la table car elle peut être étendue à:

MY_DATE = TO_DATE('2008-09-12 15:00:00') AND
TO_DATE('2008-09-12 15:00:00') = TRUNC(TO_DATE('2008-09-12 15:00:00'))

Ceci est automatiquement faux, bien sûr.

Bien qu'il soit parfois tentant de stocker des dates sous forme de nombres tels que 20080915, cela peut entraîner des problèmes d'optimisation des requêtes. Par exemple, combien de valeurs juridiques y a-t-il entre 20 071 231 et 20 070 101? Que diriez-vous entre les dates 31 décembre 2007 et 1er janvier 2008? Il permet également la saisie de valeurs illégales, telles que 20070100.

Ainsi, si vous avez des dates sans composantes horaires, définir une plage devient facile:

select ...
from   ...
where  my_date Between date '2008-01-01' and date '2008-01-05'

Lorsqu'il existe un composant horaire, vous pouvez effectuer l'une des opérations suivantes:

select ...
from   ...
where  my_date >= date '2008-01-01' and
       my_date  < date '2008-01-06'

ou

select ...
from   ...
where  my_date Between date '2008-01-01'
                   and date '2008-01-05'-(1/24/60/60)

Notez l'utilisation de (1/24/60/60) au lieu d'un nombre magique. Il est assez courant dans Oracle d’effectuer une arithmétique sur les dates en ajoutant des fractions de jour définies: 3/24 pendant trois heures, 27/24/60 pendant 27 minutes. Oracle math de ce type est exact et ne souffre pas d’erreurs d’arrondi, donc:

select 27/24/60 from dual;

... donne 0.01875, pas 0.01874999999999 ou autre.

Je ne vois pas encore les types de données d'intervalle publiés.

Oracle propose également des types de données pour votre scénario exact. Il existe également des types de données INTERVAL ANNÉE À MOIS et INTERVAL JOUR À SECOND.

À partir de la documentation 10gR2.

  

INTERVALLE ANNÉE À MOIS stocke une période   de temps en utilisant l'année et le mois   champs datetime. Ce type de données est   utile pour représenter la différence   entre deux valeurs datetime quand seulement   les valeurs d'année et de mois sont   significatif.

     

ANNÉE D'INTERVALLE ((year_precision)] TO   MOIS

     

où year_precision est le nombre de   chiffres dans le champ YEAR datetime. le   La valeur par défaut de year_precision est 2.

     

INTERVAL DAY TO SECOND Type de données

     

INTERVAL DAY TO SECOND enregistre une période   de temps en termes de jours, heures,   minutes et secondes. Ce type de données est   utile pour représenter le précis   différence entre deux datetime   valeurs.

     

Spécifiez ce type de données comme suit:

     

INTERVAL DAY [(day_precision)] TO   SECONDE   [(fractional_seconds_precision)]

     

     

day_precision est le nombre de chiffres   dans le champ jour date. Accepté   les valeurs sont comprises entre 0 et 9. La valeur par défaut est 2.

     

fractional_seconds_precision est le   nombre de chiffres dans la fraction   partie du champ SECOND datetime.   Les valeurs acceptées sont comprises entre 0 et 9. Le   la valeur par défaut est 6.

     

Vous avez beaucoup de flexibilité   lorsque vous spécifiez des valeurs d'intervalle comme   littéraux. Veuillez vous référer à & Quot; Interval   Littéraux & Quot; pour des informations détaillées sur   spécifiez les valeurs d'intervalle sous forme de littéraux.   Voir aussi & Quot; Date / heure et intervalle   Exemples & Quot; pour un exemple en utilisant   intervalles.

Sur la base de mes expériences, il existe quatre façons principales de le faire:

1) Convertissez la date en un nombre entier d'époque (secondes depuis le 1er janvier 1970) et enregistrez-la dans la base de données sous forme d'entier.

2) Convertissez la date en un entier AAAAMMJJHHMMSS et enregistrez-la dans la base de données sous forme d'entier.

3) Stockez-le comme une date

4) Stockez-le en tant que chaîne

Je suis toujours resté fidèle à 1 et 2, car cela vous permet d'effectuer une arithmétique simple et rapide avec la date et de ne pas vous fier aux fonctionnalités de la base de données sous-jacente.

Sur la base de votre première phrase, vous tombez sur l'une des & fonctionnalités cachées & "; (bogues) de Java: java.util.Date aurait dû être immuable, mais ce n’est pas le cas. (Java 7 promet de résoudre ce problème avec une nouvelle API de date / heure.) Presque toutes les applications d'entreprise comptent sur différentes temporelles modèles et, à un moment donné, vous devrez faire du calcul arithmétique sur la date et l'heure.

Idéalement, vous pouvez utiliser le temps Joda , utilisé par Google Agenda. Si vous ne pouvez pas faire cela, je suppose une API qui consiste en un wrapper autour de <=> avec des méthodes de calcul similaires à Grails / Rails, et en une plage de votre wrapper (c'est-à-dire une paire ordonnée indiquant le début et la fin d'une heure). période) sera suffisant.

Sur mon projet actuel (une application de chronométrage HR), nous essayons de normaliser toutes nos dates sur le même fuseau horaire pour Oracle et Java. Heureusement, nos exigences en matière de localisation sont légères (= 1 fuseau horaire suffit). Lorsqu'un objet persistant n'a pas besoin d'une précision plus fine qu'un jour, nous utilisons l'horodatage à partir de minuit. J'irais plus loin et j'insisterais pour jeter les millisecondes supplémentaires à la granularité la plus grossière qu'un objet persistant puisse tolérer (cela simplifiera le traitement).

Toutes les dates peuvent être stockées sans ambiguïté sous forme d'horodatages GMT (c'est-à-dire sans fuseau horaire ou l'heure d'été) en stockant le résultat de getTime () sous forme d'un entier long.

Dans les cas où des manipulations de jour, de semaine, de mois, etc. sont nécessaires dans les requêtes de base de données, et lorsque les performances des requêtes sont primordiales, les horodatages (normalisés avec une granularité supérieure à plusieurs millisecondes) peuvent être liés à un tableau de répartition des dates comportant des colonnes. pour le jour, la semaine, le mois, etc., de sorte que les fonctions de date / heure coûteuses ne doivent pas être utilisées dans les requêtes.

Alan a raison. Le temps de Joda est excellent. java.util.Date et le calendrier sont juste une honte.

Si vous avez besoin d’horodatages, utilisez le type de date Oracle avec l’heure, nommez la colonne avec un suffixe tel que _tmst. Lorsque vous lisez les données en Java, insérez-les dans un objet DateTime joda time. pour vous assurer que le fuseau horaire est correct, considérez qu'il existe dans Oracle des types de données spécifiques qui stockeront les horodatages avec le fuseau horaire. Ou vous pouvez créer une autre colonne dans la table pour stocker l'ID de fuseau horaire. Les valeurs pour l'ID de fuseau horaire doivent correspondre à l'ID de nom complet standard pour les fuseaux horaires, voir http://java.sun.com/j2se/1.4.2/docs/api/java/util/TimeZone.html#getTimeZone%28java.lang.String% 29 . Si vous utilisez une autre colonne pour le dta TZ, lorsque vous lisez les données au format java, utilisez l’objet DateTime mais définissez le fuseau horaire sur l’objet DateTime à l’aide de .withZoneRetainFields pour définir le fuseau horaire.

Si vous avez seulement besoin des données de date (sans horodatage), utilisez le type de date dans la base de données sans heure. encore une fois nommez-le bien. dans ce cas, utilisez l'objet DateMidnight de jodatime.

ligne de fond: utilisez le système de types de la base de données et la langue que vous utilisez. Apprenez-les et profitez des avantages de disposer d'une API et d'une syntaxe de langage expressives pour traiter votre problème.

UPDATE: le projet Joda-Time est maintenant en mode maintenance. Son équipe conseille la migration vers les classes java.time intégrées à Java.

Joda-Time

Joda-Time propose 3 classes pour représenter une durée: intervalle, durée et période.

La norme ISO 8601 spécifie comment formater des chaînes représentant un Durée et un < a href = "https://en.wikipedia.org/wiki/ISO_8601#Time_intervals" rel = "nofollow noreferrer"> Intervalle . Joda-Time analyse et génère de telles chaînes.

Le fuseau horaire est une considération cruciale. Votre base de données devrait stocker ses valeurs date-heure en UTC. Mais votre logique métier devra peut-être prendre en compte les fuseaux horaires. Le début d'un & Quot; jour & Quot; dépend du fuseau horaire. Au fait, utilisez les noms de fuseau horaire appropriés au lieu de codes de 3 ou 4 lettres.

La réponse correcte de S.Lott recommande judicieusement d'utiliser la logique semi-ouverte, qui fonctionne généralement mieux avec la date. travail à temps partiel. Le début d'un intervalle de temps est inclusif , tandis que la fin est exclusive . Joda-Time utilise une logique semi-ouverte dans ses méthodes.

diagramme définissant une semaine supérieure ou égale au jour 1 et inférieure au jour 8

DateTimeZone timeZone_NewYork = DateTimeZone.forID( "America/New_York" );
DateTime start = new DateTime( 2014, 9, 29, 15, 16, 17, timeZone_NewYork );
DateTime stop = new DateTime( 2014, 9, 30, 1, 2, 3, timeZone_NewYork );

int daysBetween = Days.daysBetween( start, stop ).getDays();

Period period = new Period( start, stop );

Interval interval = new Interval( start, stop );
Interval intervalWholeDays = new Interval( start.withTimeAtStartOfDay(), stop.plusDays( 1 ).withTimeAtStartOfDay() );

DateTime lateNight29th = new DateTime( 2014, 9, 29, 23, 0, 0, timeZone_NewYork );
boolean containsLateNight29th = interval.contains( lateNight29th );

Sauvegarde sur console & # 8230;

System.out.println( "start: " + start );
System.out.println( "stop: " + stop );
System.out.println( "daysBetween: " + daysBetween );
System.out.println( "period: " + period ); // Uses format: PnYnMnDTnHnMnS
System.out.println( "interval: " + interval );
System.out.println( "intervalWholeDays: " + intervalWholeDays );
System.out.println( "lateNight29th: " + lateNight29th );
System.out.println( "containsLateNight29th: " + containsLateNight29th );

Lorsque vous exécutez & # 8230;

start: 2014-09-29T15:16:17.000-04:00
stop: 2014-09-30T01:02:03.000-04:00
daysBetween: 0
period: PT9H45M46S
interval: 2014-09-29T15:16:17.000-04:00/2014-09-30T01:02:03.000-04:00
intervalWholeDays: 2014-09-29T00:00:00.000-04:00/2014-10-01T00:00:00.000-04:00
lateNight29th: 2014-09-29T23:00:00.000-04:00
containsLateNight29th: true

Je stocke toutes les dates en millisecondes. Je n'utilise pas du tout les champs d'horodatage / date / heure.

Donc, je dois le manipuler aussi longtemps. Cela signifie que je n'utilise pas les mots clés 'before', 'after', 'now' dans mes requêtes SQL.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top