0

I'm attempting to populate a UTC timestamp into a SQL table, but when I use Instant.now() to get the current UTC time, a conversion of Timestamp.from(instant) is writing local time zones into the table. Is there a way to write UTC into the table?

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

This results in local timezone opposed to UTC.

The JDBC driver is net.sourceforge.jtds.jdbc.Driver.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
Randy B
  • 31
  • 3
  • 1
    `ps.setObject(1, OffsetDateTime.now(ZoneOffset.UTC))` (or `LocalDateTime.now(ZoneOffset.UTC)` if you’re so unfortunate that the database column doesn’t carry the information that it’s in UTC). – Ole V.V. Feb 09 '23 at 19:44
  • *a conversion of Timestamp.from(instant) is writing local time zones into the table*. How? `Timestamp` doesn't contain time zone info.... – g00se Feb 09 '23 at 20:32
  • 1
    Thanks for the responses! The issue was my JVM was defaulting to local time. I set a VM option -Duser.timzone= "UTC" and it is now storing UTC. Database is UTC as well, so VM was the issue. – Randy B Feb 09 '23 at 21:30
  • 2
    Afaik `Instant.now` will be unaffected by `user.timezone` – g00se Feb 09 '23 at 22:46
  • 1
    You should not se the `Timestamp` class at all. In the old days it was used with SQL databases, but it was always poorly deigned, a true hack on top of the already bad `Date` class. Since JDBC 4.2 instead use `OffsetDateTime`, `Instant` or `LocalDateTime`. Stick to the modern API. `Timestamp` is but a troublesome detour. – Ole V.V. Feb 10 '23 at 04:56
  • 2
    Add a mention of (a) what database you are using, (b) *exactly* what data type is your column. – Basil Bourque Feb 10 '23 at 05:08
  • Certainly your 'solution' is *not* a solution and might lead to further problems. Needless to say, you shouldn't be running with the wrong time zone. – g00se Feb 10 '23 at 09:41

2 Answers2

2

OffsetDateTime

when I use Instant.now() to get the current UTC time,

Don’t use Instant for SQL database work.

In JDBC 4.2+, the specification maps OffsetDateTime class to columns of a type akin to the SQL standard type TIMESTAMP WITH TIME ZONE.

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

Neither Instant nor ZonedDateTime are mapped in JDBC. The SQL standard defines no such types equivalent to those classes.

By the way, for columns of a type akin to the SQL standard type TIMESTAMP WITHOUT TIME ZONE, use the LocalDateTime class.

Avoid legacy date-time classes

a conversion of Timestamp.from(instant)

Never use the terrible legacy date-time classes such as Timestamp. Use only their replacement: the modern java.time classes defined in JSR 310.

Write to the database:

myPreparedStatement.setObject … , odt ) ;

Retrieve:

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

Do not depend on default zone

You commented:

The issue was my JVM was defaulting to local time

You should write your Java code in such a way as to not care about the JVM’s current default time zone.

The code shown above is unaffected by the JVM’s current default time zone.

Example code

Here is a complete example.

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

When run:

-------------|  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
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • "You should write your Java code in such a way as to not care about the JVM’s current default time zone." - Agreed. This solution threw an exception for me: java.sql.SQLException: Unable to convert between java.time.OffsetDateTime and JAVA_OBJECT. I've proposed a solution that works (although the comments suggest Timestamp is 'bad' to use, its' the only solution that has worked for me outside of changing the JVM timezone (which I can't do in my situation) – Randy B Feb 10 '23 at 19:27
  • 1
    @RandyB I could guess that you have defined your column with an incorrect data type. But I cannot say for sure as you ignored [my Comment](https://stackoverflow.com/questions/75403642/java-preparedstatement-reading-local-timezone-when-using-timestamp-frominstant/75407104?noredirect=1#comment133054044_75403642) asking for you to edit your Question with (a) what database you are using, (b) exactly what data type is your column. So I added a complete example app using the H2 Database Engine to demonstrate that my suggested code runs correctly. – Basil Bourque Feb 10 '23 at 20:37
0

Changing the JVM timezone to UTC solved the issue of Timestamp, however, in this case I cannot update the server in which this code is running.

The solution that worked for me without needing to adjust JVM timezone is

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

This forces Timestamp to use UTC, although it seems rather robust and redundant. The other solutions proposed did not work for me.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
Randy B
  • 31
  • 3
  • Thanks for reporting back and answering your own question. Your solutions have several issues, though, sorry. Setting the JVM time zone is fragile since someone else may set it differently (and need to do so). Your code still uses `Timestamp`, a hack of a class that you can easily avoid using, which you should. You format patterns is wrong. Your resulting `Timestamp` will be wrong too, and there will probably be times that it cannot represent at all. And you are not picking up the full precision of `Instant.now()`, which is usually finer than milliseconds. – Ole V.V. Feb 10 '23 at 20:45
  • (https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Instant.html#now()) will query the system UTC clock to obtain the current instant." The fact that you're having such trouble with this suggests to me that your system clock is not set to UTC. This of course is a weakness. It should be set to UTC and you should leave the OS to do the local time. There should be absolutely no need to be converting to and from strings – g00se Feb 10 '23 at 21:06
  • I tried your suggestions but kept getting exceptions for setObject. The PreparedStatement setTimestamp takes in a Timestamp object otherwise I would avoid it entirely. My goal was to update existing code to put the Timestamp object corresponding to the UTC timezone. I would have to do a larger change it seems to support and different paramter (setObject) – Randy B Feb 10 '23 at 21:07
  • @g00se - That is true but i don't have permissions to update the system clock as the server supports multiple processes so I have to localize the timezone control to this command. – Randy B Feb 10 '23 at 21:08
  • You mean true in the sense that your system clock is *not* set to UTC? – g00se Feb 10 '23 at 21:15
  • Which JDBC driver are you using? And, as Basil Bourque already asked, what is the datatype of your database column? I am sorry, but when you don’t provide necessary information, also not when asked, it is close to impossible to give you the best help. – Ole V.V. Feb 10 '23 at 21:42
  • Yes, true that my system clock is not UTC. That is why Timestamp is defaulting to EST and persisting that to the table. I need to format Timestamp to UTC and that solved the issue. the JDBC driver is net.sourceforge.jtds.jdbc.Driver . I do not have control over the database settings so I'm limited to what I can do to solve this. The above solved the issue by locally setting Timestamp to use UTC and this was tested and works. I think this issue is resolved. – Randy B Feb 10 '23 at 22:02
  • That seems to explain. *jTDS is an open source 100% pure Java (type 4) JDBC 3.0 driver for Microsoft SQL Server ...* If you can get hold of a JDBC 4.2 driver, you will be much better off. I frankly didn’t think anyone used older JDBC drivers any more. – Ole V.V. Feb 10 '23 at 22:09
  • And there probably is a reason why you are not using Microsoft’s own JDBC driver for SQL Server, but I can’t guess what it is? [All supported versions of it also support JDBC 4.2](https://learn.microsoft.com/en-us/sql/connect/jdbc/microsoft-jdbc-driver-for-sql-server-support-matrix?view=sql-server-ver16). – Ole V.V. Feb 11 '23 at 12:28