7

Java 和 Oracle 都有一个称为 Date的时间戳类型。开发人员倾向于将它们当作日历日期来操作,我已经看到这会导致令人讨厌的一次性错误。

  1. 对于基本日期数量,您可以在输入时简单地切断时间部分,即降低精度。但是,如果您使用日期范围(例如:9/29-9/30)执行此操作,则这两个值之间的差异是 1 天,而不是 2。此外,范围比较需要 1)截断操作:start < trunc(now) <= end,或2)算术:start < now < (end + 24hrs)。不可怕,但不干燥

  2. 另一种方法是使用真正的时间戳:9/29 00:00:00 - 10/1 00:00:00。(午夜到午夜,因此不包括 10 月的任何部分)。现在持续时间本质上是正确的,并且范围比较更简单:start <= now < end. 对于内部处理来说当然更干净,但是结束日期确实需要在初始输入 (+1) 和输出 (-1) 时进行转换,假设在用户级别使用日历日期隐喻。

您如何处理项目的日期范围?还有其他选择吗?我对您如何在等式的 Java 和 Oracle 方面处理这个问题特别感兴趣。

4

11 回答 11

7

这是我们如何做到的。

  1. 使用时间戳。

  2. 使用半开区间进行比较: start <= now < end

忽略那些坚持认为 BETWEEN 对成功的 SQL 至关重要的抱怨。

有了这一系列的日期范围真的很容易审核。9/30 to 10/1包含一天 (9/30)的数据库值。下一个区间的开始必须等于前一个区间的结束。该interval[n-1].end == interval[n].start规则便于审计。

显示时,如果需要,可以显示格式化的startend-1。事实证明,您可以教育人们了解“结束”实际上是规则不再适用的第一天。所以“9/30 到 10/1”的意思是“从 9/30 开始有效,从 10/1 开始不再有效”。

于 2008-10-01T08:00:36.490 回答
4

Oracle 具有TIMESTAMP 数据类型。它存储 DATE 数据类型的年、月和日,以及小时、分钟、秒和小数秒值。

这是asktom.oracle.com 上关于日期算术的主题。

于 2008-10-01T02:50:14.897 回答
3

我赞同 S.Lott 的解释。我们有一个产品套件,它广泛使用日期时间范围,使用这样的范围是我们的经验教训之一。顺便说一句,如果结束日期不再是范围的一部分(IOW,半开区间) ,我们将结束日期称为排他结束日期。相反,如果它算作范围的一部分,则它是一个包含性的结束日期,只有在没有时间部分时才有意义。

用户通常期望包含日期范围的输入/输出。无论如何,尽快将用户输入转换为专有结束日期范围,并在必须向用户显示时尽可能晚地转换任何日期范围。

在数据库上,始终存储独占结束日期范围。如果存在包含结束日期范围的旧数据,请尽可能将它们迁移到数据库上,或者在读取数据时尽快转换为独占结束日期范围。

于 2008-11-15T23:40:45.010 回答
2

我使用 Oracle 的日期数据类型,并就影响边界条件的时间组件问题向开发人员提供教育。

数据库约束还将防止意外指定应该没有的列中的时间分量,并且还告诉优化器没有任何值具有时间分量。

例如,约束 CHECK (MY_DATE=TRUNC(MY_DATE)) 可防止将时间不是 00:00:00 的值放入 my_date 列,还允许 Oracle 推断诸如 MY_DATE = TO_DATE(' 2008-09-12 15:00:00') 永远不会是真的,因此不会从表中返回任何行,因为它可以扩展为:

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

这当然是自动错误的。

尽管有时将日期存储为数字(例如 20080915)很诱人,但这可能会导致查询优化问题。例如,在 20,071,231 和 20,070,101 之间有多少个合法值?在 2007 年 12 月 31 日和 2008 年 1 月 1 日这两个日期之间怎么样?它还允许输入非法值,例如 20070100。

因此,如果您有没有时间组件的日期,那么定义一个范围就变得很容易:

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

当有时间组件时,您可以执行以下操作之一:

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

或者

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

注意使用 (1/24/60/60) 代替幻数。在 Oracle 中,通过添加一天中定义的小数来执行日期算术是很常见的…… 3/24 表示三个小时,27/24/60 表示 27 分钟。这种类型的 Oracle 数学是精确的,不会出现舍入错误,因此:

select 27/24/60 from dual;

...给出 0.01875,而不是 0.01874999999999 或其他任何值。

于 2008-10-01T02:48:38.863 回答
1

我还没有看到发布的间隔数据类型。

Oracle 还具有适合您的确切场景的数据类型。Oracle 中也有 INTERVAL YEAR TO MONTH 和 INTERVAL DAY TO SECOND 数据类型。

来自 10gR2 文档。

INTERVAL YEAR TO MONTH 使用 YEAR 和 MONTH 日期时间字段存储一段时间。当只有年份和月份值很重要时,此数据类型可用于表示两个日期时间值之间的差异。

INTERVAL YEAR [(year_precision)] TO MONTH

其中 year_precision 是 YEAR 日期时间字段中的位数。year_precision 的默认值为 2。

INTERVAL DAY TO SECOND 数据类型

INTERVAL DAY TO SECOND 以天、小时、分钟和秒的形式存储一段时间。此数据类型可用于表示两个日期时间值之间的精确差异。

按如下方式指定此数据类型:

INTERVAL DAY [(day_precision)] 到 SECOND [(fractional_seconds_precision)]

在哪里

day_precision 是 DAY 日期时间字段中的位数。可接受的值为 0 到 9。默认值为 2。

fractional_seconds_precision 是 SECOND 日期时间字段的小数部分的位数。可接受的值为 0 到 9。默认值为 6。

将区间值指定为文字时,您有很大的灵活性。有关将区间值指定为文字的详细信息,请参阅“区间文字”。另请参阅“日期时间和间隔示例”以获取使用间隔的示例。

于 2008-10-01T15:26:29.120 回答
0

根据您的第一句话,您偶然发现了 Java 的一个隐藏“功能”(即错误):java.util.Date应该是不可变的,但它不是。(Java 7 承诺使用新的日期/时间 API 来解决这个问题。)几乎每个企业应用程序都依赖于各种时间模式,并且在某些时候您需要对日期和时间进行算术运算。

理想情况下,您可以使用Google 日历使用的Joda time 。如果你不能这样做,我猜一个 API 由一个包装器组成,其java.util.Date计算方法类似于 Grails/Rails,并且你的包装器范围(即指示时间段开始和结束的有序对)将就足够了。

在我当前的项目(一个 HR 计时应用程序)中,我们尝试将所有日期标准化为 Oracle 和 Java 的同一时区。幸运的是,我们的本地化要求是轻量级的(= 1 个时区就足够了)。当持久对象不需要比一天更精细的精度时,我们使用从午夜开始的时间戳。我会更进一步,坚持将额外的毫秒丢弃到持久对象可以容忍的最粗粒度(这将使您的处理更简单)。

于 2008-10-01T02:47:27.603 回答
0

根据我的经验,有四种主要方法可以做到这一点:

1) 将日期转换为纪元整数(自 1970 年 1 月 1 日以来的秒数)并将其作为整数存储在数据库中。

2) 将日期转换为 YYYYMMDDHHMMSS 整数,并将其作为整数存储在数据库中。

3)将其存储为日期

4)将其存储为字符串

我一直坚持使用 1 和 2,因为它使您能够对日期执行快速简单的算术运算,而不依赖于底层数据库功能。

于 2008-10-01T02:48:49.610 回答
0

通过将getTime()的结果存储为长整数,所有日期都可以明确存储为 GMT 时间戳(即没有时区或夏令时问题) 。

在数据库查询中需要日、周、月等操作的情况下,并且当查询性能至关重要时,可以将时间戳(标准化为比毫秒更高的粒度)链接到具有当天列的日期分解表、周、月等值,以便不必在查询中使用昂贵的日期/时间函数。

于 2008-10-01T03:48:34.350 回答
0

艾伦是对的——乔达时间很棒。java.util.Date 和 Calendar 只是一个耻辱。

如果您需要时间戳,请使用带有时间的 oracle 日期类型,请使用某种后缀(如 _tmst)命名列。当您将数据读入 java 时,将其放入 joda time DateTime 对象中。为确保时区正确,请考虑 oracle 中的特定数据类型,它们会将时间戳与时区一起存储。或者,您可以在表中创建另一列来存储时区 ID。时区 ID 的值应该是时区的标准全名 ID,请参阅http://java.sun.com/j2se/1.4.2/docs/api/java/util/TimeZone.html#getTimeZone%28java.lang.String% 29 . 如果您为 TZ dta 使用另一列,那么当您将数据读入 java 时使用 DateTime 对象,但使用 .withZoneRetainFields 在 DateTime 对象上设置时区来设置时区。

如果您只需要日期数据(无时间戳),则使用数据库中的日期类型而不需要时间。再次命名它。在这种情况下,使用来自 jodatime 的 DateMidnight 对象。

底线:利用数据库的类型系统和您使用的语言。学习它们并获得使用富有表现力的 api 和语言语法来处理您的问题的好处。

于 2008-10-01T04:19:30.423 回答
0

更新:Joda-Time 项目现在处于维护模式。它的团队建议迁移到 Java 中内置的java.time类。

乔达时间

Joda-Time 提供 3 个类来表示时间跨度:Interval、Duration 和 Period。

ISO 8601 标准指定了如何格式化表示DurationInterval的字符串。Joda-Time 解析和生成这样的字符串。

时区是一个重要的考虑因素。您的数据库应该以 UTC 格式存储其日期时间值。但是您的业务逻辑可能需要考虑时区。“一天”的开始取决于时区。顺便说一句,使用正确的时区名称而不是 3 或 4 个字母代码。

S.Lott的正确答案明智地建议使用半开逻辑,因为这通常最适合日期时间工作。时间跨度的开始是包容性的,而结束是排斥性的。Joda-Time 在其方法中使用半开逻辑。

将一周定义为大于或等于第 1 天且小于第 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 );

转储到控制台...</p>

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

运行时……</p>

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
于 2014-03-23T19:24:58.993 回答
-1

我以毫秒为单位存储所有日期。我根本不使用时间戳/日期时间字段。

所以,我必须尽可能长时间地操纵它。这意味着我不在我的 sql 查询中使用“之前”、“之后”、“现在”关键字。

于 2008-10-01T02:53:04.717 回答