JPA在MySQL数据库中保存错误的日期

3

我在我的MySQL数据库中有一个表,其中包含日期列:

+-------------------+---------------+------+-----+---------+----------------+
| Field             | Type          | Null | Key | Default | Extra          |
+-------------------+---------------+------+-----+---------+----------------+
| id                | bigint(20)    | NO   | PRI | NULL    | auto_increment |
| type              | varchar(50)   | NO   |     | NULL    |                |
| expiration        | date          | NO   |     | NULL    |                |

我正在使用MySQL和JPA来保存日期。我有一个功能,用户可以选择最终日期范围并获取所有日期。
请查看以下带有许多SYSO的代码以尝试了解正在发生的事情...
@Override
    protected DateTime nextReference(DateTime reference) {
        System.out.println("Reference: " + reference.toString("dd-MM-YYYY"));
        DateTime plus = reference.plusMonths(1);
        System.out.println("One month from now: " + plus.toString("dd-MM-YYYY"));

        DateTime result = plus.withDayOfMonth(reference.getDayOfMonth());
        System.out.println("Final: " + result.toString("dd-MM-YYYY"));

        return result;
    }

这部分,结果很好:

Reference: 10-01-2017
One month from now: 10-02-2017
Final: 10-02-2017
Reference: 10-02-2017
One month from now: 10-03-2017
Final: 10-03-2017
Reference: 10-03-2017
One month from now: 10-04-2017
Final: 10-04-2017
Reference: 10-04-2017
One month from now: 10-05-2017
Final: 10-05-2017
Reference: 10-05-2017
One month from now: 10-06-2017
Final: 10-06-2017
Reference: 10-06-2017
One month from now: 10-07-2017
Final: 10-07-2017
Reference: 10-07-2017
One month from now: 10-08-2017
Final: 10-08-2017
Reference: 10-08-2017
One month from now: 10-09-2017
Final: 10-09-2017
Reference: 10-09-2017
One month from now: 10-10-2017
Final: 10-10-2017
Reference: 10-10-2017
One month from now: 10-11-2017
Final: 10-11-2017
Reference: 10-11-2017
One month from now: 10-12-2017
Final: 10-12-2017
Reference: 10-12-2017
One month from now: 10-01-2018
Final: 10-01-2018

好的,现在让我们转向保存部分:
@Transactional
private void saveTransactions(List<Transaction> transactions) {
    for (Transaction t : transactions) {
        System.out.println("Saving: " + t.getExpiration().toString("dd-MM-YYYY"));
        Transaction saved = dao.save(t);
        System.out.println("Saved: " + saved.getExpiration().toString("dd-MM-YYYY"));
    }
}

如您所见,我在此添加了一些代码行以进行调试。在继续输出前,请查看 DAO:

public T save(T entity) {
    entityManager.persist(entity);
    return entity;
}

没什么大不了的……输出结果:
Saving: 10-02-2017
Saved: 10-02-2017
Saving: 10-03-2017
Saved: 10-03-2017
Saving: 10-04-2017
Saved: 10-04-2017
Saving: 10-05-2017
Saved: 10-05-2017
Saving: 10-06-2017
Saved: 10-06-2017
Saving: 10-07-2017
Saved: 10-07-2017
Saving: 10-08-2017
Saved: 10-08-2017
Saving: 10-09-2017
Saved: 10-09-2017
Saving: 10-10-2017
Saved: 10-10-2017
Saving: 10-11-2017
Saved: 10-11-2017
Saving: 10-12-2017
Saved: 10-12-2017

如你所见,这应该没问题吧?一切都在10号进行。

在我继续之前,请检查模型和转换器:

    //Attribute
    @Convert(converter = JpaDateConverter.class)
    private DateTime expiration;

//Converter

public class JpaDateConverter implements AttributeConverter<DateTime, Date> {

    @Override
    public Date convertToDatabaseColumn(DateTime objectValue) {
        return objectValue == null ? null : new Date(objectValue.getMillis());
    }

    @Override
    public DateTime convertToEntityAttribute(Date dataValue) {
        return dataValue == null ? null : new DateTime(dataValue);
    }

}

现在看一下我的数据库:
mysql> select expiration from tb_transaction where notes = 3 and year(expiration
) = 2017;
+------------+
| expiration |
+------------+
| 2017-01-10 |
| 2017-02-10 |
| 2017-03-09 |
| 2017-04-09 |
| 2017-05-09 |
| 2017-06-09 |
| 2017-07-09 |
| 2017-08-09 |
| 2017-09-09 |
| 2017-10-09 |
| 2017-11-10 |
| 2017-12-10 |
+------------+
12 rows in set (0.00 sec)

由于某种奇怪的神秘原因,一些日期被保存在9日而不是10日!!

MySQL驱动程序上没有警告、错误或其他提示。

请大家帮帮忙!

编辑 事务类:

@Entity
@Table(name = "tb_transaction")
public class Transaction implements Cloneable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(STRING)
    private TransactionType type;

    @Convert(converter = JpaDateConverter.class)
    private DateTime expiration;

3
可能是因为夏令时的原因。夏令时在三月和十一月发生,注意在这些时间你的日期会改变。 - Rick S
@MaciejKowalski,我更新了问题。你想表达的是这个意思吗? - Marco Noronha
1
在您的转换器类中,必须有一种方法可以通过仅获取DateTime的Date部分并忽略Time来将DateTime转换为Date。调用getMillis()会导致DST问题。 - Rick S
我按照你描述的方法进行了实验,结果和预期一样。我在Oracle 11g和MySQL上尝试了相同的结果。 - ujulu
@ujulu,你使用了转换器吗? - Marco Noronha
显示剩余4条评论
2个回答

5

简述

使用 JPA 2.2,因为它支持java.time

在Java中使用仅限日期的类来处理SQL中的仅限日期值。

LocalDate                           // Represent a date-only, without a time-of-day and without a time zone.
.now(                               // Get today's date…
    ZoneId.of( "Africa/Tunis" )     // …as seen in the wall-clock time used by the people of a particular region.
)                                   // Returns a `LocalDate` object.
.plusMonths( 1 )                    // Returns another `LocalDate` object, per immutable objects pattern.

java.time

JPA 2.2 现在支持现代的 java.time 类。不再需要使用 Joda-Time。

不要使用 java.sql.Date 这个类似乎表示只有日期,但实际上由于继承自 java.util.Date(尽管名称中包含了“Date”,但它实际上表示日期、时间和 UTC 的偏移量为零),所以有一个设置为 UTC 的时间。这些遗留类是一团糟。Sun、Oracle 和 JCP 社区多年前就放弃了这些类,转而采用 JSR 310,你也应该这样做。

LocalDate

LocalDate类表示仅包含日期值且没有时间和时区与UTC的偏移量的值。

时区在确定日期方面非常重要。对于任何给定的时刻,日期会因时区而在全球范围内变化。例如,在法国巴黎的午夜过后几分钟,是新的一天,而在加拿大魁北克蒙特利尔仍然是“昨天”。

如果没有指定时区,JVM会隐式应用其当前的默认时区。该默认值在运行时可能会随时更改,因此您的结果可能会有所不同。最好将所需/预期的时区明确指定为参数。如果很关键,请与用户确认时区。
指定正确格式的时区名称Continent / Region,例如America/MontrealAfrica/CasablancaPacific/Auckland。永远不要使用2-4个字母的缩写,例如ESTIST,因为它们不是真正的时区,不是标准化的,并且甚至不唯一!
ZoneId z = ZoneId.of( "America/Montreal" ) ;  
LocalDate today = LocalDate.now( z ) ;

如果想使用JVM的当前默认时区,请请求并将其作为参数传递。如果省略,代码会变得模糊不清,因为我们无法确定您是否打算使用默认值,或者像许多程序员一样,不知道这个问题。
ZoneId z = ZoneId.systemDefault() ;  // Get JVM’s current default time zone.

或者指定一个日期。您可以通过数字设置月份,1-12代表1月至12月。

LocalDate ld = LocalDate.of( 1986 , 2 , 23 ) ;  // Years use sane direct numbering (1986 means year 1986). Months use sane numbering, 1-12 for January-December.

或者更好的方法是使用预定义的Month枚举对象,每个月一个。提示:在整个代码库中使用这些Month对象而不是仅使用整数数字来使您的代码更加自我说明,确保有效值并提供类型安全性。对于YearYearMonth也是如此。
LocalDate ld = LocalDate.of( 1986 , Month.FEBRUARY , 23 ) ;

日期时间计算

显然,您想从一个日期开始,并得到一个月后的日期范围。

LocalDate monthLater = ld.plusMonths( 1 ) ;

JDBC 4.2

从JDBC 4.2开始,您的JDBC驱动程序需要支持一些关键的java.time类,例如LocalDate

日期范围

请注意以下链接中的ThreeTen-Extra。如果您需要处理日期范围,则可能会发现那里的LocalDateRange类非常方便。


关于 java.time

java.time 框架被内置在 Java 8 及以后的版本中。这些类取代了麻烦的旧的 遗留 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

要了解更多信息,请参阅 Oracle 教程。并在 Stack Overflow 上搜索许多示例和解释。规范为 JSR 310

Joda-Time项目现在处于维护模式,建议迁移到java.time类。

您可以直接使用java.time对象与数据库进行交换。使用符合JDBC 4.2或更高版本要求的JDBC驱动程序。无需字符串,无需java.sql.*类。

如何获取java.time类?

ThreeTen-Extra项目扩展了java.time的附加类。该项目是java.time可能未来添加的一个试验场。您可能会在这里找到一些有用的类,例如IntervalYearWeekYearQuarter更多


3

感谢@RickS的帮助,我们解决了这个问题。

PS:我很久以前就解决过这个问题,两年后又遇到了这个问题。哈哈

解决方案在转换器JpaDateConverter中,当你将DateTime对象转换为java.sql.Date时,请参考文档:

如果给定的毫秒值包含时间信息,驱动程序将把时间组件设置为默认时区(运行应用程序的Java虚拟机的时区)中对应于零GMT的时间。

因此,为了解决这个问题,我更改了服务器的时区:

dpkg-reconfigure tzdata

还有在MySQL中:

SET @@global.time_zone = '+00:00';

这个答案对我有用,我正在插入一个日期(例如LocalDate.of(1961, 3, 15)),并在MySQL数据库中获取前一天的日期(例如“1961-03-14”)。使用SET命令解决了我的问题。 - Alexandre DuBreuil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接