【问题标题】:LocalDate stored as DATE in MySQL returns different result在 MySQL 中存储为 DATE 的 LocalDate 返回不同的结果
【发布时间】:2018-07-19 02:34:14
【问题描述】:

我有一个 Java 类,其字段定义为:

  @Column
  @NotNull
  private LocalDate availabilityDate;

映射到定义为的列:

availability_date DATE NOT NULL

当我比较通过 Spring Data 保存到 MySQL 数据库之前和之后的日期时,我在 JUnit 中得到不同的结果:

Expecting:
  <[InventoryAvailability(size=000, availabilityDate=2018-02-07, inventoryContextId=3847, quantity=15, lastChange=2018-02-08T14:32:24.770+01:00[Europe/Berlin]),
    InventoryAvailability(size=001, availabilityDate=2018-02-07, inventoryContextId=3847, quantity=15, lastChange=2018-02-08T14:32:26.337+01:00[Europe/Berlin])]>
to contain exactly in any order:
  <[InventoryAvailability(size=001, availabilityDate=2018-02-08, inventoryContextId=3847, quantity=15, lastChange=2018-02-08T14:32:26.337+01:00[Europe/Berlin]),
    InventoryAvailability(size=000, availabilityDate=2018-02-08, inventoryContextId=3847, quantity=15, lastChange=2018-02-08T14:32:24.770+01:00[Europe/Berlin])]>

这怎么可能?我以为只有 TIMESTAMPS 有一个时区。我正在考虑将其存储为毫秒 LONG 或 UTC ZonedDateTime 作为解决方法,但我怀疑我不了解这里的重要内容。

【问题讨论】:

  • 好问题。我推测这可能与将日期转换为取决于 JDBC 驱动程序或 MySQL 中某处的时区的日期一样令人讨厌。当然,它应该以确保日期不会改变的方式来完成,但这并不是我们第一次听说软件中的错误......
  • 什么意思?我在问题中写了所有数据类型。

标签: java mysql date timezone


【解决方案1】:

我的回答是基于您使用 java.time.LocalDate 作为字段availabilityDate 的假设。我编写了简单的 Spring Boot 应用程序,它使用 Spring Data 和 MySQL 连接器(数据库版本为 5.7.20-log)来重现您的案例。

Test DateHolderRepositoryTest 失败,因为它不知道如何将 LocalDate 转换为数据库中的 Date。然后我添加了对 hibernate-java8 的依赖,测试变成了绿色(如https://www.thoughts-on-java.org/hibernate-5-date-and-time/ 中所述)。

所以我的假设是您的 JUnit 测试有问题,或者可能与实体的 equals 和 hashCode 方法实现有关(因为 Hamcrest 将在下面使用 equals 来比较类,并从数据库类和创建的类加载只是不同的实例,默认的 Object equals 方法将返回 false)。您的问题中的 availabilityDate=2018-02-08 显示没有时区的日期。

为了重现我的结果,我添加了我的测试应用程序的代码。我这里是我的 DateHolder 类,它包含 id 和 availabilityDate,并包含基于字段 id 的 hashCode 和 equals 实现:

package hello;

import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;

@Entity
public class DateHolder {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Integer id;

    @Column
    @NotNull
    private LocalDate availabilityDate;

    protected DateHolder() {}

    public DateHolder(LocalDate availabilityDate)
    {
        this.availabilityDate = availabilityDate;
    }

    @Override
    public String toString()
    {
        return String.format("DateHolder[id=%d, availabilityDate='%s']", id, availabilityDate);
    }

    public Integer getId()
    {
        return id;
    }

    public LocalDate getAvailabilityDate()
    {
        return availabilityDate;
    }

    @Override
    public int hashCode()
    {
        return id;
    }

    @Override
    public boolean equals(Object other)
    {
        if (other instanceof DateHolder)
        {
            return this.id == ((DateHolder)other).id;
        }
        else
        {
            return false;
        }
    }
}

这是我的 JPA 存储库:

package hello;
import org.springframework.data.repository.CrudRepository;

public interface DateHolderRepository extends CrudRepository<DateHolder, Long>
{
}

这是主要的 Spring Boot 应用程序类:

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args)
    {
        SpringApplication.run(Application.class);
    }
}

这是测试,它创建 2 个 DateHolders,将它们保存在数据库中,并将它们与数据库中的任何内容进行比较(它不考虑顺序)。它还测试从数据库加载的第一个实体的 toString() 方法的返回值。

package hello;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;

import java.time.LocalDate;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class DateHolderRepositoryTest
{
    @Autowired
    private DateHolderRepository repository;

    @Test
    public void testFindByLastName()
    {
        DateHolder dateHolder1 = new DateHolder(LocalDate.parse("2018-02-07"));
        DateHolder dateHolder2 = new DateHolder(LocalDate.parse("2018-02-08"));

        repository.save(dateHolder1);
        repository.save(dateHolder2);

        Iterable<DateHolder> dateHolders = repository.findAll();
        assertThat(dateHolders, containsInAnyOrder(dateHolder2, dateHolder1));

        assertThat(dateHolders.iterator().next().toString(), is("DateHolder[id=1, availabilityDate='2018-02-07']"));
    }
}

【讨论】:

  • 感谢您的详细回复。我想这个问题只有在语言环境不同的情况下才会出现。我的测试 MySQL 实例在 mac 上的 docker 容器中运行,我很确定您提到的故障模式不适用。我将所有内容都转换为 Instant 和 Timestamp,从而解决了问题。
  • 多么烦人的解决方案,@MichaelBöckling :-( 它真的应该与 LocalDate 一起使用,但不知道如何使其正常工作。:-(
  • 是的,一开始我不敢相信。问题是,我不希望我的代码依赖于正确的服务器/数据库语言环境。 MySQL 上的时间戳保证为 UTC,因此它们是万无一失的,但亲爱的主,为什么它甚至是必要的。这就是我通常使用 Postgres 的原因。
【解决方案2】:

只需使用适当的时区配置您的 MySQL 实例。如果您在容器上运行 MySQL,它可能会得到错误的时区,因此您会遇到这种奇怪的日期时间值。

例如,如果您的 JVM 使用 America/Sao_Paulo 时区,您可以将 --default-time-zone=America/Sao_Paulo 发送到您的 MySQL 配置。这样,您将确保 JVM 和 MySQL 位于同一时区,而不会影响您的 Java 应用程序。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-29
    • 2018-01-06
    • 1970-01-01
    • 2021-09-04
    • 2021-01-10
    相关资源
    最近更新 更多