计划给定的时间,但在 SQL Server 中使用不同的时区并使用 Java 客户端

Schedule a time given but works with different time zones in SQL Server and using Java client

提问人:redoc01 提问时间:11/9/2023 最后编辑:Bohemianredoc01 更新时间:11/15/2023 访问量:93

问:

我需要允许系统在本地安排一些时间,并在不同的时区正确显示此时间。

因此,如果我在英国安排一个下午 1 点到下午 3 点之间的时间,那么如果我在美国看到这个时间表,那么如您所知,美国提前 7 小时,因此美洲的时间表时间将是晚上 7 点到晚上 10 点

我之前在sql server中使用过datetimeoffset,它允许UTC日期使用不同的时区。

我真的需要使用sql server来解决这个问题。

那么,当用户选择开始日期和结束日期时,我该如何采用这种方法来获取时差呢?

我将使用的客户端是 java 来显示不同时区的开始和结束日期。

我在sql服务器中尝试过这个,但我使用了getutcdate。这是另一个项目在最初存储某个日期时初始化的,温已进行输入。但就像我上面说的,我需要用户在开始日期和结束日期之间选择日期。

请注意,存储日期的数据库提前 1 小时,因为它位于另一个国家/地区的服务器上。

java sql sql-server

评论

4赞 Dale K 11/9/2023
将您的日期存储为 UTC 日期,并使用用户区域设置显示它们?并在存储在数据库中之前将本地用户时间转换为 UTC。
3赞 Slaw 11/9/2023
在 Java 端使用 java.time.ZonedDateTime。与数据库通信时,在 UTC 和用户的时区之间进行转换。或者找到一种方法将时区存储在数据库中。另外,为了以防万一,请注意时偏移量不是一回事。
2赞 tquadrat 11/9/2023
使用日期/时间值存储时区!将时间转换为 UTC 只是第二好的解决方案,因为当您想在 9 点钟的壁挂时间运行某些内容时,当 DST 打开或关闭时,这应该不会改变。正如@Slaw所说,时区(欧洲/伦敦)、偏移量(+00:00)和区域时间(GMT)之间有一个重要的区别。您需要存储时区!!
3赞 Basil Bourque 11/9/2023
如果你的政客要改变你所在时区的规则,例如采用或放弃夏令时 (DST),你是否希望一天中的时间相应地改变?或者你想坚持同一个时刻,同一个时间点,不管政客们改变了什么规则?
2赞 Jim Garrison 11/9/2023
否,仅在输入(从用户的区域设置到 UTC)和输出(从 UTC 到用户的区域设置)上转换。用户的本地时间始终可以明确地转换为 UTC,并且给定的 UTC 时刻始终可以转换为任何时区的本地时间。

答:

7赞 Basil Bourque 11/9/2023 #1

您尚未指定是否要跟踪时间轴上的特定点,或者是否要跟踪一天中的时间,如果政客更改了您的时区规则,该时间会根据需要进行调整。第一种情况(如火箭发射)更简单,但我感觉你的意思是第二种情况(如预约医疗/牙科预约)。让我们来介绍一下。

固定时间点

为了安排火箭发射,飞行工程师研究天气预报。一旦他们选择了一个合适的发射时间,时间线上的那个点是固定的,不变的。如果政客们改变了时区的规则,发射就不会移动。即使周边地区的挂钟时间可能从 15:33 更改为 14:33,计划的时刻也保持不变。天气和火箭,不在乎一天中的时间,他们关心的是时间轴上的一个点。

如果要跟踪时间轴上的特定点,请使用标准 SQL 类型。在 MS SQL Server 中,这将是 datetimeoffset 类型。在 Java 中,通常使用 Instant,但对于 JDBC 数据库调用,请使用 OffsetDateTimeTIMESTAMP WITH TIME ZONE

数据类型 标准 SQL MS SQL 服务器 爪哇岛
时间线上的点 TIMESTAMP WITH TIME ZONE datetimeoffset 通常使用 Instant
在 JDBC 中,使用 OffsetDateTime
期间 VARCHAR varchar Duration

假设我们的火箭发射飞行指挥员已经确定发射时间为 2024 年 1 月 23 日下午 3:33,与 UTC 的时间子午线相差 0 小时-分钟-秒。在标准 ISO 8601 文本中,这将是 .末尾表示偏移量为零,发音为“Zulu”。2024-01-23T15:33:00ZZ

java.time 类在解析/生成文本时默认使用 ISO 8601 格式。

Instant launchWindowStart = Instant.parse( "2024-01-23T15:33:00Z" ) ; 

想象一下,发射窗口长达一小时。我们使用 Duration 类来表示未附加到时间线的时间跨度。

Duration launchWindowSpan = Duration.ofHours( 1 ) ;

我们可以确定结束时间:

Instant launchWindowEnd = launchWindowStart.plus( launchWindowSpan ) ;

在我们的数据库中,我们可以存储开始和跨度,或者开始和结束,或者三者兼而有之(尽管这三者都会违反规范化)。对于 JDBC,将对象调整为 ,因为 SQL 标准没有概念映射到 。InstantOffsetDateTimeInstant

OffsetDateTime odtLaunchWindowStart = launchWindowStart.atOffset( ZoneOffset.UTC ) ;
OffsetDateTime odtLaunchWindowEnd = launchWindowEnd.atOffset( ZoneOffset.UTC ) ;

myPreparedStatement.setObject( … , odtLaunchWindowStart ) ;
myPreparedStatement.setString( … , launchWindowSpan.toString() ) ;
myPreparedStatement.setObject( … , odtLaunchWindowEnd ) ;

检索。

Instant launchWindowStart = myResultSet.getObject( … , OffsetDateTime.class).toInstant() ;
Duration launchWindowSpan = Duration.parse( myResultSet.getString() ) ;
Instant launchWindowEnd = myResultSet.getObject( … , OffsetDateTime.class).toInstant() ;

移动时间点

对于约会,您需要将日期和时间与预期的时区分开存储。所以表中有两列。

对于MS SQL Server中的日期和时间,请使用type。在 Java 中,LocalDateTime.datetime2

对于时区,请存储区域的大洲/区域名称,例如 。在 Java 中,ZoneId.Europe/London

仅记录约会的开始时间,而不记录停止时间。存储持续时间,而不是停止时间。停止时间可能不是您期望的时间。例如,如果约会从下午 1 点开始,但恰好发生在“春季”时钟转换期间,则停止时间可能是下午 4 点,而不是您期望的下午 3 点,因此 2 小时的会议可能会在下午 1 点到 4 点运行。

SQL Standard 和 MS SQL Server 均未提供持续时间的数据类型。因此,以标准 ISO 8601 格式存储为文本。对于数据库中的持续时间,以标准 ISO 8601 格式存储为文本,其中标记开始,并将年-月-日与小时-分钟-秒分开。对于 Java,请使用 Duration 对象。两个小时的持续时间是 。PnYnMnDTnHnMnSPTPT2H

数据类型 标准 SQL MS SQL 服务器 爪哇岛
日期 + 时间 TIMESTAMP WITHOUT TIME ZONE datetime2 LocalDateTime
时区名称 VARCHAR varchar ZoneId
期间 VARCHAR varchar Duration

在英国下午1点至3点之间

举个例子,让我们从今天开始预订一周。

ZoneId zLondon = ZoneId.of( "Europe/London" ) ;
LocalDate today = LocalDate.now( zLondon ) ;
LocalDate apptDate = today.plusWeeks( 1 ) ;
LocalTime apptTime = LocalTime.of( 13 , 0 ) ;  // 1 PM.
LocalDateTime apptStart = LocalDateTime.of( apptDate , apptTime ) ;
Duration duration = Duration.ofHours( 2 ) ;

将这些内容写入 、 和 列。datetime2VARCHARVARCHAR

myPreparedStatement.setObject( … , apptStart ) ;
myPreparedStatement.setString( … , zLondon.toString() ) ;
myPreparedStatement.setString( … , duration.toString() ) ;

检索。

LocalDateTime apptStart = myResultSet.getObject( … , LocalDateTime.class ) ;
ZoneId zLondon = ZoneId.of( myResultSet.getString( … ) ) ;
Duration duration = Duration.parse( myResultSet.getString( … ) ) ;

在这个关键点上要非常清楚:&不代表一个时刻,不是时间线上的一个点。它们仅代表一个带有一天中时间的日期。他们故意缺乏任何时区或与UTC的偏移量的概念。因此,他们的价值观本质上是模棱两可的。LocalDateTimedatetime2

当需要制定时间表时,将时区应用于日期时间。非常重要:不要存储此值,仅将其用于动态、临时地构建计划,例如向用户展示。

ZonedDateTime zdtLondon = appt.atZone( zLondon ) ;

现在我们有一个时刻,时间线上的一个特定点。

您可以调整到其他时区。

ZoneId zEdmonton = ZoneId.of( "America/Edmonton" ) ;
ZonedDateTime zdtEdmonton = zdtLondon.atZoneSameInstant( zEdmonton ) ;

和 表示相同的同时时刻,时间轴上的同一点。通过两个不同的镜头来看待这一时刻,英国人民使用的挂钟和日历的镜头与加拿大阿尔伯塔省人民使用的挂钟和日历的镜头。zdtLondonzdtEdmonton

通常最好使用半开方法处理时间跨度。在《半开》中,开头是包容的,而结尾是排他性的。因此,从下午 1 点到下午 3 点的任命从第 13 小时的第一刻开始,然后一直持续到第 15 小时的第一刻,但不包括(大约,取决于上面提到的政治计时异常)。提示:在 SQL 中,不要在 Half-Open 查询中使用,因为 Fully Closed 也是如此(start 和 end 都包括在内)。BETWEENBETWEEN

为了表示整个时间跨度,java.time 框架没有定义任何此类。你可以定义你自己的。例如:

record SpanOfTimeZoned ( ZonedDateTime start , ZonedDateTime end ) {}

或者更实用:

public record SpanOfTimeZoned( ZonedDateTime start , ZonedDateTime end ) {
    public static SpanOfTimeZoned of ( ZonedDateTime start , Duration duration ) {
        return new SpanOfTimeZoned ( start , start.plus ( duration ) );
    }

    public Duration duration ( ) {
        return Duration.between ( this.start , this.end );
    }
}

用法:

SpanOfTimeZoned apptSpan = SpanOfTimeZoned.of( zdtEdmonton , duration ) ;

或者,您可能会发现将 ThreeTen-Extra 库添加到您的项目中很有用。该库提供 Interval 类,其中包含一对 java.time.Instant 对象。An 表示与 UTC 的时间子午线偏移量为零时分秒的时刻。Instant

Interval apptSpan = Interval.of( zdtEdmonton.toInstant() , duration ) ;

请注意,它始终采用 UTC(偏移量为零),而不是特定时区。当然,您可以始终应用时区来获取 .org.threeten.extra.IntervalZonedDateTime

ZonedDateTime apptStartLondon = apptSpan.getStart().atZone( zLondon ) ;

你说:

我将使用的客户端是 java 来显示不同时区的开始和结束日期。

在您的应用中,您需要从用户那里收集:

  • 日期
  • 开始时间
  • 时区
  • 持续时间(或根据用户提供的结束时间计算)

由此,您将在 Java 中生成:

  • LocalDateTime对象
  • ZoneId对象
  • Duration对象

听起来您希望在存储到数据库中之前从用户的时区转换为首选时区。请注意,这种转换是有风险的。例如,我们可以从埃德蒙顿时间调整为伦敦时间。但是,这种调整必须使用目前这两个区域的规则。在这一刻和那次约会之间的时间里,如果艾伯塔省的政客或英格兰的政客要改变他们的时区规则,那么从池塘另一边的人的角度来看,我们存储的约会似乎是错误的。政客们的这种改变似乎不太可能;历史证明并非如此。世界各地的政客都表现出了摆弄其管辖范围内时区规则的偏好。

LocalDateTime userLdt = LocalDateTime.of( … ) ;
ZoneId userZoneId = ZoneId.of( … ) ;
Duration duration = Duration.of( … ) ;

ZonedDateTime userZdt = userLdt.atZone( userZoneId ) ;
ZonedDateTime zdtLondon = userZdt.atZoneSameInstant( zLondon ) ;
LocalDateTime apptStart = zdtLondon.toLocalDateTime() ; 

此时,您可以存储 、 和,如本答案前面所述。apptStartzLondonduration

评论

0赞 Dale K 11/9/2023
为什么 varchar 用于持续时间?
2赞 Basil Bourque 11/9/2023
@DaleK 因为 SQL Standard 和 MS SQL Server 都没有提供持续时间的数据类型。因此,以标准 ISO 8601 格式存储为文本。SQL标准几乎没有涉及日期时间问题,甚至他们做得很差。
0赞 siggemannen 11/9/2023
人们应该注意,这样 sql server 就不会将 localdatetimes 重新组合到它自己的本地版本的 datetime2 中。约会确实非常困难,很好的答案!
0赞 Arvind Kumar Avinash 11/14/2023
很好的答案!我还投票赞成提出这个问题,因为它不值得结束。