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

JPA Saving wrong date in MySQL database

我的 MySQL 数据库中有一个 table,它有一个日期列:

+-------------------+---------------+------+-----+---------+----------------+
| 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 来保存日期。我有一个功能,用户可以 select 一个最终日期范围,它会得到所有的日期。

检查此代码(使用大量 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 驱动程序没有错误或什么都没有。

请大家帮忙!

编辑 交易Class:

@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;

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

PS: 这个问题我早就解决了,2年后又遇到了这个问题。哈哈

解决方案在转换器中:JpaDateConverter,当您将 DateTime Objetivo 转换为 java.sql.Date 时,根据文档:

If the given milliseconds value contains time information, the driver will set the time components to the time in the default time zone (the time zone of the Java virtual machine running the application) that corresponds to zero GMT.

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

dpkg-reconfigure tzdata

在MySQL中:

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

tl;博士

使用JPA 2.2支持java.time.

在 Java 中使用仅日期 class 来处理 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 classes。无需再使用 Joda-Time。

不要不要使用java.sql.Date那class假装 表示仅日期但实际上将时间设置为 UTC,因为从 java.util.Date 继承的糟糕设计决定(尽管名称表示日期 and 时间 UTC 本身的零偏移量)。这些遗留的 classes 真是一团糟。 Sun、Oracle 和 JCP 社区都在 class 几年前随着 JSR 310 的采用而放弃了这些论文,您也应该如此。

LocalDate

LocalDate class represents a date-only value without time-of-day and without time zone or offset-from-UTC.

时区对于确定日期至关重要。对于任何给定时刻,日期在全球范围内因地区而异。例如,Paris France is a new day while still “yesterday” in Montréal Québec.

午夜后几分钟

如果未指定时区,JVM 将隐式应用其当前默认时区。该默认值在运行时可能 change at any moment (!),因此您的结果可能会有所不同。最好明确指定 desired/expected 时区作为参数。如果关键,请与您的用户确认区域。

Continent/Region的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用 ESTIST 等 2-4 字母缩写,因为它们 不是 真实时区,未标准化,甚至不是唯一的(!)。

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 enum objects pre-defined, one for each month of the year. Tip: Use these Month objects throughout your codebase rather than a mere integer number to make your code more self-documenting, ensure valid values, and provide type-safety. Ditto for Year & YearMonth

LocalDate ld = LocalDate.of( 1986 , Month.FEBRUARY , 23 ) ;

日期时间数学

显然您想从一个日期开始,然后将一个月后作为日期范围。

LocalDate monthLater = ld.plusMonths( 1 ) ;

JDBC 4.2

从 JDBC 4.2 开始,您的 JDBC 驱动程序需要支持某些密钥 java.time classes比如LocalDate

日期范围

请注意以下 ThreeTen-Extra 的链接。如果您经常处理日期范围,您可能会发现 LocalDateRange class 会很方便。


关于java.time

java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

您可以直接与您的数据库交换 java.time 对象。使用 JDBC driver compliant with JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* classes.

在哪里获取java.time classes?

ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.