由于Java SimpleDateFormat问题,Oracle DB中的时间数据(使用Zone)无法正常工作

Saving time data (with Zone) in Oracle DB not working because of Java SimpleDateFormat issue

我有一个字段定义为TIMESTAMP WITH TIME ZONE

要保存的值从US/Hawaii区域中的"09-23-2019 10:03:11 pm"开始。

这是我试图保存到数据库的内容(所有日期信息加上区域)

数据库以UTC格式存储时间信息。

到目前为止,日期存储在数据库中,因此如下所示:

1
2
3
4
DAYS
---------------------------------------------------------------------------
23-SEP-19 10.03.11.000000 PM -05:00
23-SEP-19 10.03.11.000000 PM -05:00

在处理过程中,它通过以下代码运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    dateStr: the date (as seen above)
    ZoneLoc: 'US/Hawaii'

    public Calendar convDateStrWithZoneTOCalendar(String dateStr,
            String ZoneLoc) throws Exception {

        // convert the string sent in from user (which uses AM/PM) to one that uses military time (24HR)
        // it
        String formattedDate = null;
        DateFormat readFormat = new SimpleDateFormat(this.getPattern());
        DateFormat writeFormat = new SimpleDateFormat("MM-dd-yyyy'T'HH:mm:ss'Z'");
        writeFormat.setTimeZone(TimeZone.getTimeZone(ZoneLoc));
        Date date = null;
        date = readFormat.parse(dateStr);
        formattedDate = writeFormat.format(date);

        // see if you can parse the date needed WITH the TimeZone
        Date d;
        SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy'T'HH:mm:ss'Z'");
        sdf.setTimeZone(TimeZone.getTimeZone(ZoneLoc));
        d = sdf.parse(formattedDate);
        Calendar cal = Calendar.getInstance();
        cal.setTime(d);

        system.out.println(" ZONELOC VALUE" + ZoneLoc);
        system.out.println(" RETURNED VALUE" + cal );

        return cal;
    }

返回的日历信息是:

ZONELOC VALUE IS US/Hawaii

RETURNED VALUE IS
java.util.GregorianCalendar[time=1577678591000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Chicago",offset=-21600000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=America/Chicago,offset=-21600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2019,MONTH=11,WEEK_OF_YEAR=1,WEEK_OF_MONTH=5,DAY_OF_MONTH=29,DAY_OF_YEAR=363,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=5,AM_PM=1,HOUR=10,HOUR_OF_DAY=22,MINUTE=3,SECOND=11,MILLISECOND=0,ZONE_OFFSET=-21600000,DST_OFFSET=0]

看起来好像返回值中没有设置美国/夏威夷。

我能做些什么来确保这个设置?

之后,我可以把它放在数据库中,看看设置是否会"坚持"而不返回到America/Chicago中。

更新@帕特里克H-谢谢你的意见。我按照您指定的模式进行了更改,并且能够保存数据。现在看起来是这样的:

2017-08-02 13:38:49 TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [26] as [TIMESTAMP] - [java.util.GregorianCalendar[time=1569294191000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Chicago",offset=-21600000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=America/Chicago,offset=-21600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2019,MONTH=8,WEEK_OF_YEAR=39,WEEK_OF_MONTH=4,DAY_OF_MONTH=23,DAY_OF_YEAR=266,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=10,HOUR_OF_DAY=22,MINUTE=3,SECOND=11,MILLISECOND=0,ZONE_OFFSET=-21600000,DST_OFFSET=3600000]]

数据库中的数据如下:

1
23-SEP-19 10.03.11.000000 PM -05:00

即使指定了US/Hawaii,该区域仍为America/Chicago。怎样才能让US/Hawaii坚持下去而不回到America/Chicago呢?


根据此输出:

1
java.util.GregorianCalendar[time=1569294191000,...

上面的时间值(也就是自unix epoch(1970-01-01T00:00Z以来的1569294191000毫秒)相当于芝加哥的09-23-2019 10:03 PM。这是因为readFormat使用系统的默认时区(可能是America/Chicago),只需检查TimeZone.getDefault()的值即可。

要解析输入09-23-2019 10:03:11 pm并将其视为夏威夷的本地时间,只需将相应的时区设置为SimpleDateFormat实例(在本例中,设置为readFormat,因为它需要知道输入日期在哪个时区—因为您没有设置任何时区,所以它使用系统的默认值)。您也不需要其他格式化程序(writeFormatsdf),只能使用一个格式化程序来获取相应的日期:

1
2
3
4
SimpleDateFormat parser = new SimpleDateFormat("MM-dd-yyyy hh:mm:ss a");
// the input is in Hawaii timezone
parser.setTimeZone(TimeZone.getTimeZone("US/Hawaii"));
Date date = parser.parse("09-23-2019 10:03:11 pm");

上面的date相当于夏威夷的晚上10:03。实际上,日期本身只包含从unix epoch开始的毫秒数(date.getTime()返回1569321191000),没有格式,也没有时区信息。

然后可以将其设置为Calendar实例(不要忘记设置日历的时区):

1
2
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("US/Hawaii"));
cal.setTime(date);

我用Oracle的Timestamp作为时区类型已经有一段时间了,但我认为这足以保存正确的值。日历的价值是:

java.util.GregorianCalendar[time=1569312191000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="US/Hawaii",offset=-36000000,dstSavings=0,useDaylight=false,transitions=7,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2019,MONTH=8,WEEK_OF_YEAR=39,WEEK_OF_MONTH=4,DAY_OF_MONTH=23,DAY_OF_YEAR=266,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=10,HOUR_OF_DAY=22,MINUTE=3,SECOND=11,MILLISECOND=0,ZONE_OFFSET=-36000000,DST_OFFSET=0]

号Java新的日期/时间API

旧类(dateCalendarSimpleDateFormat有许多问题和设计问题,它们正在被新的API所取代。

其中一个主要的问题是在不同的时区工作是多么的困难和混乱。

如果使用Java 8,请考虑使用新的JavaTimeAPI。与旧的API相比,它更容易、更少被窃听和更少出错。

如果您使用的是Java <=7,您可以使用StuteTeN后端,这是Java 8新的日期/时间类的一个很大的后端。对于Android,有三个etenabp(更多关于如何使用它)。

下面的代码适用于这两种情况。唯一的区别是包名称(在Java 8中是EDCOX1,20),在三重后端(或Android的TruteEnabp)是EDCOX1(21),但是类和方法名称是相同的。

要解析输入09-23-2019 10:03:11 pm,您可以使用DateTimeFormatter并将其解析为LocalDateTime—输入没有时区信息,因此我们只考虑日期和时间,然后我们可以将其转换为时区。

1
2
3
4
5
6
7
8
9
10
11
// parse the input
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // parse AM/PM and am/pm
    .parseCaseInsensitive()
    // input pattern
    .appendPattern("MM-dd-yyyy hh:mm:ss a")
    // use English locale for am/pm symbols
    .toFormatter(Locale.ENGLISH);
LocalDateTime dt = LocalDateTime.parse("09-23-2019 10:03:11 pm", fmt);
// convert to Hawaii timezone
ZonedDateTime hawaiiDate = dt.atZone(ZoneId.of("US/Hawaii"));

最新的JDBC驱动程序支持新的API(但仅针对Java 8,我猜),但是如果您仍然需要使用EDCOX1 OR 16,那么您可以很容易地将EDCOX1的26度转换为:

1
2
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("US/Hawaii"));
calendar.setTimeInMillis(hawaiiDate.toInstant().toEpochMilli());

在Java 8中,您也可以这样做:

1
Calendar calendar = GregorianCalendar.from(hawaiiDate);

如果需要与旧的CalendardateAPI的互操作性,则可以在内部使用新的API进行计算,并在需要时从API转换为API。


根据simpledateformat,我认为您的格式化字符串是错误的。您还可以在返回值中看到月份和日期是错误的。MONTH=11,DAY_OF_MONTH=29

这是您目前拥有的:江户十一〔一〕号

我认为格式字符串应该是:'dd-MMM-yy hh.mm.ss.SSSSSS a Z'

看起来时区问题也可能是因为里面有冒号。simpledateformat的文档表明,对于RFC822时区,它需要采用这种格式:-0500,您可能会发现使用常规时区组件更容易。