【问题标题】:How to prevent date change using H2 for a JUnit-Test?如何防止使用 H2 进行 JUnit-Test 的日期更改?
【发布时间】:2020-07-29 10:49:02
【问题描述】:

对于使用 H2 的 JUnit-Test,我正在保存一个具有 LocalDate 属性和值 LocalDate.parse("1900-01-01") 的实体。测试失败

    Expected: is <1900-01-01>
    but: was <1899-12-31>

LocalDate 不关心时区,所以我猜想更改是在 H2 数据库的保存操作期间进行的。

我已经尝试设置时区UTC,因为那应该是 H2 的时区,然后 H2 不应将其移交的日期解释为具有另一个必须转换为 UTC 的时区。但这似乎没有帮助。

    public static void main(final String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
        SpringApplication.run(BackendApplication.class, args);
    }

提前感谢您的建议和想法

【问题讨论】:

  • java.time.LocalDate 可以在任何时区安全地与来自 H2 的 DATE 数据类型一起使用,它们都不依赖它。但是,如果您的应用程序使用的库在内部使用旧版java.sql.Date,它们可能会受到 Java 8 之前的日期时间类的不同错误的影响;即使数据库对现代 JSR-310 类具有本机支持,许多持久性库也会对遗留类进行奇怪的转换。但是,这些错误通常不会影响 UTC 时区。您需要为您的问题添加更多详细信息。

标签: java spring date timezone h2


【解决方案1】:

无法重现您的测试失败

您的代码:

LocalDate.parse( "1900-01-01" )

...按照记录工作,产生 1900 年 1 月 1 日的值。

LocalDate ld = LocalDate.parse("1900-01-01") ;
System.out.println( ld ) ;

看到code run live at IdeOne.com

1900-01-01

➥ 你的代码肯定比你向我们展示的要多。

使用 H2 的示例应用

你说:

我猜想更改是在 H2 数据库的保存操作期间进行的。

让我们试试吧。这是一个使用 H2 版本 1.4.200 的完整示例应用程序。从带有 Java 14 的 macOS Mojave 上的 IntelliJ 2020.1 运行。

我们有一个表event_,其中有一个UUID 类型的id_ 列和一个LocalDate 类型的列when_。我们为值 1900-01-01 写入单个值。

如您所见,如果您运行此代码,我们会返回一个 LocalDate 对象,toString 方法为其返回 1900-01-01

package work.basil.example;

import org.h2.jdbcx.JdbcDataSource;

import java.sql.*;
import java.time.LocalDate;
import java.util.Objects;
import java.util.UUID;

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

    private void doIt ( )
    {
        JdbcDataSource dataSource = Objects.requireNonNull( new JdbcDataSource() );  // Implementation of `DataSource` bundled with H2.
        dataSource.setURL( "jdbc:h2:mem:localdate_example_db;DB_CLOSE_DELAY=-1" ); // Set `DB_CLOSE_DELAY` to `-1` to keep in-memory database in existence after connection closes.
        dataSource.setUser( "scott" );
        dataSource.setPassword( "tiger" );

        String sql = null;

        try (
                Connection conn = dataSource.getConnection() ;
        )
        {
            String tableName = "event_";
            sql = "CREATE TABLE " + tableName + " (\n" +
                    "  id_ UUID DEFAULT random_uuid() PRIMARY KEY ,\n" +
                    "  when_ DATE NOT NULL\n" +
                    ");";
            try ( Statement stmt = conn.createStatement() ; )
            {
                stmt.execute( sql );
            }

            // Insert row.
            sql = "INSERT INTO event_ ( when_ ) " + "VALUES ( ? ) ;";
            try (
                    PreparedStatement preparedStatement = conn.prepareStatement( sql ) ;
            )
            {
                LocalDate localDate = LocalDate.parse( "1900-01-01" );
                preparedStatement.setObject( 1 , localDate );  // Yesterday.
                preparedStatement.executeUpdate();
            }

            // Query all.
            sql = "SELECT * FROM event_ ;";
            try (
                    Statement stmt = conn.createStatement() ;
                    ResultSet rs = stmt.executeQuery( sql ) ;
            )
            {
                while ( rs.next() )
                {
                    //Retrieve by column name
                    UUID id = rs.getObject( "id_" , UUID.class );  // Pass the class to be type-safe, rather than casting returned value.
                    LocalDate localDate = rs.getObject( "when_" , LocalDate.class );  // Ditto, pass class for type-safety.

                    //Display values
                    System.out.println( "id_: " + id + " | when_: " + localDate );
                }
            }
        }
        catch ( SQLException e )
        {
            e.printStackTrace();
        }
    }
}

当示例应用运行时:

id_: 949830e0-77c8-49a3-8804-0972ff70ca2c |当_:1900-01-01


不要依赖默认时区

您的代码:

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

……部署时风险很大。设置时区会立即影响在该 JVM 中运行的所有应用程序的所有线程中的所有代码。您可能正在从其他应用程序或您自己应用程序的任何其他部分的脚下拉开地毯。

➥ 最好不要依赖 JVM 当前的默认时区。

要捕捉以 UTC 显示的当前时刻,只需调用 Instant.nowInstant 代表 UTC 中的时刻,分辨率为纳秒。此类替换java.util.Date

Instant instant = Instant.now() ;  // Capture the current moment in UTC.

对于数据库工作,使用更灵活的类OffsetDateTime。 JDBC 4.2 规范莫名其妙地需要支持OffsetDateTime,而不是更常用的InstantZonedDateTime

OffsetDateTime odt = OffsetDateTime.now( ZoneOffset.UTC ) ;  // Capture the current moment in UTC.

将其写入数据库。

myPreparedStatement.setObject( … , odt ) ;

检索。

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

当记录一个时刻(时间线上的一个特定点)时,例如OffsetDateTime 表示的那个时刻,你在数据库中的列必须是类似于 SQL 标准类型 TIMESTAMP WITH TIME ZONE 的数据类型(不是WITHOUT)。在 H2 中,要使用的数据类型就是 TIMESTAMP WITH TIME ZONE

【讨论】:

  • 在某些时区(例如欧洲/巴黎),使用PreparedStatement.setDate(…, java.sql.Date.valueOf(LocalDate.…)) 可以重现故障,但现代应用程序不应使用java.sql.Date。 H2 的文档还包含一个警告:h2database.com/html/datatypes.html#date_type
  • @EvgenijRyazanov 正是系统的时区(Windows 10)是使其可重现的因素之一;将其设置为 Europe/Paris 测试失败,将其设置为 Europe/Berlin 测试通过。
猜你喜欢
  • 2016-04-01
  • 2023-03-15
  • 1970-01-01
  • 1970-01-01
  • 2017-05-12
  • 2012-10-07
  • 1970-01-01
  • 1970-01-01
  • 2011-02-11
相关资源
最近更新 更多