使用 Timestamp.from(Instant) 时读取本地时区的 Java PreparedStatement

Java PreparedStatement reading local timezone when using Timestamp.from(Instant)

提问人:Randy B 提问时间:2/10/2023 最后编辑:AnonymousRandy B 更新时间:2/11/2023 访问量:456

问:

我正在尝试将 UTC 时间戳填充到 SQL 表中,但是当我用于获取当前 UTC 时间时,转换是将本地时区写入表中。有没有办法将UTC写入表中?Instant.now()Timestamp.from(instant)

PreparedStatement ps = connection.prepareStatement(...)
ps.setString(1, Timestamp.from(Instant.now())

这会导致本地时区与 UTC 相反。

JDBC 驱动程序是 。net.sourceforge.jtds.jdbc.Driver

时间 戳 时区 UTC java.time.instant

评论

1赞 Anonymous 2/10/2023
ps.setObject(1, OffsetDateTime.now(ZoneOffset.UTC))(或者,如果您很不幸,数据库列没有携带它在 UTC 中的信息)。LocalDateTime.now(ZoneOffset.UTC)
1赞 Randy B 2/10/2023
感谢您的回复!问题是我的JVM默认为本地时间。我设置了一个 VM 选项 -Duser.timzone= “UTC”,它现在正在存储 UTC。数据库也是 UTC,所以 VM 是问题所在。
2赞 g00se 2/10/2023
Afaik 将不受以下因素的影响Instant.nowuser.timezone
1赞 Anonymous 2/10/2023
你根本不应该上课。在过去,它与SQL数据库一起使用,但它总是被设计得很差,这是在已经很糟糕的类之上的真正黑客。由于 JDBC 4.2 改用 ,则使用 或 。坚持使用现代 API。 不过是一条麻烦的弯路。TimestampDateOffsetDateTimeInstantLocalDateTimeTimestamp
2赞 Basil Bourque 2/10/2023
添加提及 (a) 您正在使用的数据库,(b) 您的列确切的数据类型。

答:

3赞 Basil Bourque 2/10/2023 #1

OffsetDateTime

当我使用 Instant.now() 获取当前 UTC 时间时,

不要用于 SQL 数据库工作。Instant

在 JDBC 4.2+ 中,规范将类映射到类似于 SQL 标准类型的列。OffsetDateTimeTIMESTAMP WITH TIME ZONE

OffsetDateTime odt = OffsetDateTime.now( ZoneOffset.UTC ) ;

在 JDBC 中既不映射也不映射。SQL 标准没有定义与这些类等效的此类类型。InstantZonedDateTime

顺便说一句,对于类似于 SQL 标准类型的列 ,请使用类。TIMESTAMP WITHOUT TIME ZONELocalDateTime

避免使用旧版日期时间类

Timestamp.from(instant) 的转换

永远不要使用可怕的旧日期时间类,例如 .仅使用它们的替代品:JSR 310 中定义的现代 java.time 类。Timestamp

写入数据库:

myPreparedStatement.setObject … , odt ) ;

取回:

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;

不依赖于默认区域

您评论道:

问题是我的JVM默认为本地时间

您应该以不关心 JVM 当前默认时区的方式编写 Java 代码。

上面显示的代码不受以下因素的影响 JVM 的当前默认时区。

示例代码

这是一个完整的示例。

package work.basil.example.db;

import javax.sql.DataSource;
import java.sql.*;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.UUID;

public class DemoH2InMem
{
    public static void main ( String[] args )
    {
        DemoH2InMem app = new DemoH2InMem();
        app.demo();
    }

    private void demo ( )
    {
        DataSource dataSource = this.fetchDataSource();
        this.createTable( dataSource );
        this.insertDummyData( dataSource );
        this.dump( dataSource );

//        Scanner scanner = new Scanner( System.in );
//        System.out.print( "Type anything to end program: " );
//        String anything = scanner.nextLine();
        System.out.println( "Demo done at " + Instant.now() );
    }

    private DataSource fetchDataSource ( )
    {
        org.h2.jdbcx.JdbcDataSource dataSource = new org.h2.jdbcx.JdbcDataSource();
        dataSource.setURL( "jdbc:h2:mem:demo_db;DB_CLOSE_DELAY=-1" );
        return dataSource;
    }

    private void createTable ( final DataSource dataSource )
    {
        String sql =
                """
                CREATE TABLE bogus_ (
                    id_ UUID PRIMARY KEY  ,
                    when_ TIMESTAMP WITH TIME ZONE
                )
                ;
                """;
        try (
                Connection conn = dataSource.getConnection() ;
                Statement stmt = conn.createStatement() ;
        )
        {
            stmt.execute( sql );
        }
        catch ( SQLException e ) { throw new RuntimeException( e ); }
    }

    private void insertDummyData ( final DataSource dataSource )
    {
        String sql =
                """
                INSERT INTO bogus_ ( id_ , when_ )
                VALUES ( ? , ? )
                ;
                """;
        try (
                Connection conn = dataSource.getConnection() ;
                PreparedStatement pstmt = conn.prepareStatement( sql ) ;
        )
        {
            pstmt.setObject( 1 , UUID.fromString( "97a9e379-4d8f-4d06-8bea-43560a72120b" ) );
            pstmt.setObject( 2 , OffsetDateTime.now( ZoneOffset.UTC ) );
            pstmt.executeUpdate();

            pstmt.setObject( 1 , UUID.fromString( "052ae129-d0ca-4fdf-9a06-c87d20a2d3f2" ) );
            pstmt.setObject( 2 , OffsetDateTime.now( ZoneOffset.UTC ) );
            pstmt.executeUpdate();
        }
        catch ( SQLException e ) { throw new RuntimeException( e ); }
    }

    private void dump ( final DataSource dataSource )
    {
        String sql =
                """
                SELECT * FROM bogus_
                ;
                """;
        try (
                Connection conn = dataSource.getConnection() ;
                Statement stmt = conn.createStatement() ;
                ResultSet resultSet = stmt.executeQuery( sql ) ;
        )
        {

            System.out.println( "-------------|  table start |---------------" );
            while ( resultSet.next() )
            {
                UUID uuid = resultSet.getObject( "id_" , UUID.class );
                OffsetDateTime when = resultSet.getObject( "when_" , OffsetDateTime.class );
                System.out.println( uuid + " | " + when );
            }
            System.out.println( "-------------|  table end  |---------------" );
        }
        catch ( SQLException e ) { throw new RuntimeException( e ); }
    }
}

运行时:

-------------|  table start |---------------
97a9e379-4d8f-4d06-8bea-43560a72120b | 2023-02-10T20:32:57.074979Z
052ae129-d0ca-4fdf-9a06-c87d20a2d3f2 | 2023-02-10T20:32:57.080153Z
-------------|  table end  |---------------
Demo done at 2023-02-10T20:32:57.092230Z

评论

0赞 Randy B 2/11/2023
“你应该以一种不关心 JVM 当前默认时区的方式编写 Java 代码。”这个解决方案为我抛出了一个异常:java.sql.SQLException:无法在 java.time.OffsetDateTime 和 JAVA_OBJECT 之间转换。我提出了一个有效的解决方案(尽管评论表明 Timestamp 使用起来“不好”,但它是除了更改 JVM 时区之外唯一对我有用的解决方案(在我的情况下我无法做到这一点)
1赞 Basil Bourque 2/11/2023
@RandyB 我可以猜到您使用不正确的数据类型定义了列。但我不能肯定地说,因为您忽略了我的评论,要求您使用 (a) 您正在使用的数据库,(b) 您的列到底是什么数据类型。因此,我添加了一个使用 H2 数据库引擎的完整示例应用,以演示我建议的代码是否正确运行。
0赞 Randy B 2/11/2023 #2

将 JVM 时区更改为 UTC 解决了时间戳的问题,但是,在这种情况下,我无法更新运行此代码的服务器。

无需调整 JVM 时区即可为我工作的解决方案是

statement.setTimestamp(1,
        Timestamp.valueOf(DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss.SSS")
                .format(Instant.now().atZone(ZoneId.of("UTC")))));

这迫使 Timestamp 使用 UTC,尽管它看起来相当健壮和冗余。提出的其他解决方案对我不起作用。

评论

0赞 Anonymous 2/11/2023
感谢您的汇报并回答您自己的问题。不过,您的解决方案有几个问题,对不起。设置 JVM 时区是很脆弱的,因为其他人可能会以不同的方式设置它(并且需要这样做)。你的代码仍然使用 ,一个你可以很容易地避免使用的类的黑客,你应该这样做。格式模式错误。你的结果也会是错误的,有时它可能根本无法代表。而且你没有获得 的全精度,这通常比毫秒更精细。TimestampTimestampInstant.now()
0赞 g00se 2/11/2023
(docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/...)将查询系统 UTC 时钟以获取当前时刻。你遇到这样的麻烦的事实向我表明你的系统时钟没有设置为UTC。这当然是一个弱点。它应该设置为 UTC,您应该离开操作系统以执行本地时间。绝对没有必要在字符串之间转换
0赞 Randy B 2/11/2023
我尝试了您的建议,但不断收到 setObject 的异常。PreparedStatement setTimestamp 接受 Timestamp 对象,否则我会完全避免它。我的目标是更新现有代码,以放置与UTC时区对应的Timestamp对象。我必须做一个更大的改变,它似乎支持和不同的参数(setObject)
0赞 Randy B 2/11/2023
@g00se - 这是真的,但我无权更新系统时钟,因为服务器支持多个进程,所以我必须将时区控制本地化为此命令。
0赞 g00se 2/11/2023
你的意思是说你的系统时钟没有设置为UTC吗?